@webpieces/ai-hook-rules 0.4.754 → 0.4.756

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.4.754",
3
+ "version": "0.4.756",
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",
@@ -25,7 +25,7 @@
25
25
  "directory": "packages/tooling/ai-hook-rules"
26
26
  },
27
27
  "dependencies": {
28
- "@webpieces/rules-config": "0.4.754",
28
+ "@webpieces/rules-config": "0.4.756",
29
29
  "inversify": "7.10.4",
30
30
  "reflect-metadata": "0.2.2"
31
31
  },
@@ -1,9 +1,10 @@
1
1
  import { EffectiveTree } from './effective-tree';
2
+ import { GovernedPath } from './target-tree';
2
3
  import { ToolKind, NormalizedToolInput, EditContext, FileContext, BashContext } from './types';
3
4
  export declare class BuiltContexts {
4
5
  readonly fileContext: FileContext;
5
6
  readonly editContexts: readonly EditContext[];
6
7
  constructor(fileContext: FileContext, editContexts: readonly EditContext[]);
7
8
  }
8
- export declare function buildContexts(toolKind: ToolKind, input: NormalizedToolInput, workspaceRoot: string): BuiltContexts;
9
+ export declare function buildContexts(toolKind: ToolKind, input: NormalizedToolInput, workspaceRoot: string, governed: GovernedPath): BuiltContexts;
9
10
  export declare function buildBashContext(command: string, tree: EffectiveTree): BashContext;
@@ -5,7 +5,6 @@ exports.buildContexts = buildContexts;
5
5
  exports.buildBashContext = buildBashContext;
6
6
  const tslib_1 = require("tslib");
7
7
  const fs = tslib_1.__importStar(require("fs"));
8
- const path = tslib_1.__importStar(require("path"));
9
8
  const strip_ts_noise_1 = require("./strip-ts-noise");
10
9
  const disable_directives_1 = require("./disable-directives");
11
10
  const types_1 = require("./types");
@@ -18,9 +17,9 @@ class BuiltContexts {
18
17
  }
19
18
  }
20
19
  exports.BuiltContexts = BuiltContexts;
21
- function buildContexts(toolKind, input, workspaceRoot) {
20
+ function buildContexts(toolKind, input, workspaceRoot, governed) {
22
21
  const filePath = input.filePath;
23
- const relativePath = path.relative(workspaceRoot, filePath);
22
+ const relativePath = governed.relativePath;
24
23
  const edits = input.edits;
25
24
  const currentFileLines = readCurrentFileLines(filePath);
26
25
  let linesAdded = 0;
@@ -32,7 +31,7 @@ function buildContexts(toolKind, input, workspaceRoot) {
32
31
  const projectedFileLines = toolKind === 'Write'
33
32
  ? countLines(edits.length > 0 ? edits[0].newString : '')
34
33
  : currentFileLines + linesAdded - linesRemoved;
35
- const fileContext = new types_1.FileContext(toolKind, filePath, relativePath, workspaceRoot, currentFileLines, linesAdded, linesRemoved, projectedFileLines);
34
+ const fileContext = new types_1.FileContext(toolKind, filePath, relativePath, workspaceRoot, currentFileLines, linesAdded, linesRemoved, projectedFileLines, governed.treeRoot, governed.treeRelativePath);
36
35
  const editContexts = edits.map((e, idx) => {
37
36
  const addedContent = e.newString;
38
37
  const stripped = (0, strip_ts_noise_1.stripTsNoise)(addedContent);
@@ -1 +1 @@
1
- {"version":3,"file":"build-context.js","sourceRoot":"","sources":["../../../../../../packages/tooling/ai-hook-rules/src/core/build-context.ts"],"names":[],"mappings":"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","sourcesContent":["import * as fs from 'fs';\nimport * as path from 'path';\n\nimport { EffectiveTree } from './effective-tree';\nimport { stripTsNoise } from './strip-ts-noise';\nimport { createIsLineDisabled } from './disable-directives';\nimport {\n ToolKind, NormalizedToolInput, NormalizedEdit,\n EditContext, FileContext, BashContext,\n} from './types';\n\nexport class BuiltContexts {\n readonly fileContext: FileContext;\n readonly editContexts: readonly EditContext[];\n\n constructor(fileContext: FileContext, editContexts: readonly EditContext[]) {\n this.fileContext = fileContext;\n this.editContexts = editContexts;\n }\n}\n\nexport function buildContexts(\n toolKind: ToolKind,\n input: NormalizedToolInput,\n workspaceRoot: string,\n): BuiltContexts {\n const filePath = input.filePath;\n const relativePath = path.relative(workspaceRoot, filePath);\n const edits = input.edits;\n\n const currentFileLines = readCurrentFileLines(filePath);\n let linesAdded = 0;\n let linesRemoved = 0;\n for (const e of edits) {\n linesAdded += countLines(e.newString);\n linesRemoved += countLines(e.oldString);\n }\n\n const projectedFileLines =\n toolKind === 'Write'\n ? countLines(edits.length > 0 ? edits[0].newString : '')\n : currentFileLines + linesAdded - linesRemoved;\n\n const fileContext = new FileContext(\n toolKind,\n filePath,\n relativePath,\n workspaceRoot,\n currentFileLines,\n linesAdded,\n linesRemoved,\n projectedFileLines,\n );\n\n const editContexts = edits.map((e, idx) => {\n const addedContent = e.newString;\n const stripped = stripTsNoise(addedContent);\n const isLineDisabled = createIsLineDisabled(addedContent);\n return new EditContext(\n toolKind,\n idx,\n edits.length,\n filePath,\n relativePath,\n workspaceRoot,\n addedContent,\n stripped,\n addedContent.split('\\n'),\n stripped.split('\\n'),\n e.oldString,\n isLineDisabled,\n );\n });\n\n return new BuiltContexts(fileContext, editContexts);\n}\n\n// The bash context is built from the RESOLVED tree, not the shell's cwd: `tree.root` is the worktree\n// the command acts on (what the guards must judge) while `tree.governedRoot` still owns the config.\n// webpieces-disable no-function-outside-class -- sibling of buildContexts() above; this module is a pair of module-scope context factories and a lone class for one of them would break its shape\nexport function buildBashContext(command: string, tree: EffectiveTree): BashContext {\n return new BashContext(command, tree.root, tree.effectiveCwd, tree.governedRoot);\n}\n\nfunction readCurrentFileLines(filePath: string): number {\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n const content = fs.readFileSync(filePath, 'utf8');\n return countLines(content);\n } catch (err: unknown) {\n // eslint-disable-next-line @webpieces/catch-error-pattern -- file-not-found is expected for new Write targets\n void err;\n return 0;\n }\n}\n\nfunction countLines(s: string): number {\n if (!s) return 0;\n return s.split('\\n').length;\n}\n"]}
1
+ {"version":3,"file":"build-context.js","sourceRoot":"","sources":["../../../../../../packages/tooling/ai-hook-rules/src/core/build-context.ts"],"names":[],"mappings":"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","sourcesContent":["import * as fs from 'fs';\n\nimport { EffectiveTree } from './effective-tree';\nimport { GovernedPath } from './target-tree';\nimport { stripTsNoise } from './strip-ts-noise';\nimport { createIsLineDisabled } from './disable-directives';\nimport {\n ToolKind, NormalizedToolInput, NormalizedEdit,\n EditContext, FileContext, BashContext,\n} from './types';\n\nexport class BuiltContexts {\n readonly fileContext: FileContext;\n readonly editContexts: readonly EditContext[];\n\n constructor(fileContext: FileContext, editContexts: readonly EditContext[]) {\n this.fileContext = fileContext;\n this.editContexts = editContexts;\n }\n}\n\nexport function buildContexts(\n toolKind: ToolKind,\n input: NormalizedToolInput,\n workspaceRoot: string,\n governed: GovernedPath,\n): BuiltContexts {\n const filePath = input.filePath;\n const relativePath = governed.relativePath;\n const edits = input.edits;\n\n const currentFileLines = readCurrentFileLines(filePath);\n let linesAdded = 0;\n let linesRemoved = 0;\n for (const e of edits) {\n linesAdded += countLines(e.newString);\n linesRemoved += countLines(e.oldString);\n }\n\n const projectedFileLines =\n toolKind === 'Write'\n ? countLines(edits.length > 0 ? edits[0].newString : '')\n : currentFileLines + linesAdded - linesRemoved;\n\n const fileContext = new FileContext(\n toolKind,\n filePath,\n relativePath,\n workspaceRoot,\n currentFileLines,\n linesAdded,\n linesRemoved,\n projectedFileLines,\n governed.treeRoot,\n governed.treeRelativePath,\n );\n\n const editContexts = edits.map((e, idx) => {\n const addedContent = e.newString;\n const stripped = stripTsNoise(addedContent);\n const isLineDisabled = createIsLineDisabled(addedContent);\n return new EditContext(\n toolKind,\n idx,\n edits.length,\n filePath,\n relativePath,\n workspaceRoot,\n addedContent,\n stripped,\n addedContent.split('\\n'),\n stripped.split('\\n'),\n e.oldString,\n isLineDisabled,\n );\n });\n\n return new BuiltContexts(fileContext, editContexts);\n}\n\n// The bash context is built from the RESOLVED tree, not the shell's cwd: `tree.root` is the worktree\n// the command acts on (what the guards must judge) while `tree.governedRoot` still owns the config.\n// webpieces-disable no-function-outside-class -- sibling of buildContexts() above; this module is a pair of module-scope context factories and a lone class for one of them would break its shape\nexport function buildBashContext(command: string, tree: EffectiveTree): BashContext {\n return new BashContext(command, tree.root, tree.effectiveCwd, tree.governedRoot);\n}\n\nfunction readCurrentFileLines(filePath: string): number {\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n const content = fs.readFileSync(filePath, 'utf8');\n return countLines(content);\n } catch (err: unknown) {\n // eslint-disable-next-line @webpieces/catch-error-pattern -- file-not-found is expected for new Write targets\n void err;\n return 0;\n }\n}\n\nfunction countLines(s: string): number {\n if (!s) return 0;\n return s.split('\\n').length;\n}\n"]}
@@ -49,6 +49,6 @@ function filterByExcludedPaths(rules, governed, ex) {
49
49
  */
50
50
  // webpieces-disable no-function-outside-class -- the bash-side constructor for the value above, beside it
51
51
  function bashGovernedPath(tree, workspaceRoot) {
52
- return new target_tree_1.GovernedPath(path.relative(workspaceRoot, tree.effectiveCwd), path.relative(tree.root, tree.effectiveCwd));
52
+ return new target_tree_1.GovernedPath(path.relative(workspaceRoot, tree.effectiveCwd), path.relative(tree.root, tree.effectiveCwd), tree.root);
53
53
  }
54
54
  //# sourceMappingURL=excluded-paths.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"excluded-paths.js","sourceRoot":"","sources":["../../../../../../packages/tooling/ai-hook-rules/src/core/excluded-paths.ts"],"names":[],"mappings":";;AA6BA,sDASC;AAYD,4CAKC;;AAvDD,mDAA6B;AAE7B,0DAA4E;AAG5E,6CAA2C;AAC3C,+CAA6C;AAG7C;;;;;;;GAOG;AAEH,uGAAuG;AACvG,wGAAwG;AACxG,4FAA4F;AAC5F,qEAAqE;AACrE,EAAE;AACF,kGAAkG;AAClG,wGAAwG;AACxG,yGAAyG;AACzG,wGAAwG;AACxG,2CAA2C;AAC3C,kOAAkO;AAClO,SAAgB,qBAAqB,CAAC,KAAsB,EAAE,QAAsB,EAAE,EAAgB;IAClG,mGAAmG;IACnG,8FAA8F;IAC9F,oGAAoG;IACpG,6FAA6F;IAC7F,wCAAwC;IACxC,IAAI,IAAA,kCAAmB,EAAC,QAAQ,CAAC,gBAAgB,CAAC;QAAE,OAAO,EAAE,CAAC;IAC9D,IAAI,EAAE,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAS,EAAW,EAAE,CAAC,IAAA,wBAAW,EAAC,CAAC,EAAE,QAAQ,CAAC,YAAY,CAAC,CAAC;QAAE,OAAO,EAAE,CAAC;IAC5F,OAAO,KAAK,CAAC;AACjB,CAAC;AAED;;;;;;;;GAQG;AACH,0GAA0G;AAC1G,SAAgB,gBAAgB,CAAC,IAAmB,EAAE,aAAqB;IACvE,OAAO,IAAI,0BAAY,CACnB,IAAI,CAAC,QAAQ,CAAC,aAAa,EAAE,IAAI,CAAC,YAAY,CAAC,EAC/C,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,IAAI,EAAE,IAAI,CAAC,YAAY,CAAC,CAC9C,CAAC;AACN,CAAC","sourcesContent":["import * as path from 'path';\n\nimport { ExcludePaths, isWebpiecesStateDir } from '@webpieces/rules-config';\n\nimport { EffectiveTree } from './effective-tree';\nimport { globMatches } from './load-rules';\nimport { GovernedPath } from './target-tree';\nimport type { Rule } from './types';\n\n/**\n * L1's FILTER — which rules have jurisdiction over one path — and the one helper that builds its\n * argument for the bash surface.\n *\n * Lifted out of runner.ts, which the file-size rule had outgrown. It is a natural seam rather than a\n * page-count fix: this is the only place `excludePaths` and the hard-coded `.webpieces/` skip are\n * consulted, and runner.ts is otherwise the four tool ENTRY POINTS and their L0/L1 preamble.\n */\n\n// Drop every rule excluded for this path (webpieces.config.json → excludePaths). ONE glob list: a path\n// listed there is hands-off for code-style rules and file-scoped guards alike, because webpieces either\n// governs a path or it does not. Per-rule carve-outs live in the rule's own `excludePaths`.\n// This is L1's FILTER (not a table row) — see guards/L1-location.md.\n//\n// It takes a GovernedPath and not a bare string because the two questions below are asked against\n// DIFFERENT roots, and answering both from the governed-root spelling is issue #851's secondary defect:\n// the excludePaths globs are authored against the governed root, while `.webpieces/` is the state dir OF\n// A TREE and a linked worktree has its own. One argument carrying both spellings is what stops a caller\n// picking the wrong one; see GovernedPath.\n// webpieces-disable no-function-outside-class -- L1's filter, a pure predicate over one path and one glob list; it is called from four module-scope entry points in runner.ts and a class here would be a namespace with no state\nexport function filterByExcludedPaths(rules: readonly Rule[], governed: GovernedPath, ex: ExcludePaths): readonly Rule[] {\n // webpieces' OWN gitignored state dir is never governed, config or no config. Ahead of the list on\n // purpose — see isWebpiecesStateDir for why it is code and not a seeded glob. Asked about the\n // OWNING tree's spelling, so `<primary>/.claude/worktrees/agent-X/.webpieces/...` is exempt for the\n // identical reason `<primary>/.webpieces/...` is: nothing under it is tracked, reviewable or\n // revertable in the tree it belongs to.\n if (isWebpiecesStateDir(governed.treeRelativePath)) return [];\n if (ex.paths.some((p: string): boolean => globMatches(p, governed.relativePath))) return [];\n return rules;\n}\n\n/**\n * The bash surface's GovernedPath, built from the tree EffectiveTreeResolver has already classified —\n * so it costs no extra git call on the hook's blocking path.\n *\n * Both spellings of the command's effective cwd: relative to the governed root for the excludePaths\n * globs, and relative to the OWNING tree for the `.webpieces/` skip — which for\n * `cd <worktree>/.webpieces && …` is the worktree's state dir, not the primary's. Both are '' for a\n * command with no leading `cd`, which matches no glob and is not the state dir.\n */\n// webpieces-disable no-function-outside-class -- the bash-side constructor for the value above, beside it\nexport function bashGovernedPath(tree: EffectiveTree, workspaceRoot: string): GovernedPath {\n return new GovernedPath(\n path.relative(workspaceRoot, tree.effectiveCwd),\n path.relative(tree.root, tree.effectiveCwd),\n );\n}\n"]}
1
+ {"version":3,"file":"excluded-paths.js","sourceRoot":"","sources":["../../../../../../packages/tooling/ai-hook-rules/src/core/excluded-paths.ts"],"names":[],"mappings":";;AA6BA,sDASC;AAYD,4CAMC;;AAxDD,mDAA6B;AAE7B,0DAA4E;AAG5E,6CAA2C;AAC3C,+CAA6C;AAG7C;;;;;;;GAOG;AAEH,uGAAuG;AACvG,wGAAwG;AACxG,4FAA4F;AAC5F,qEAAqE;AACrE,EAAE;AACF,kGAAkG;AAClG,wGAAwG;AACxG,yGAAyG;AACzG,wGAAwG;AACxG,2CAA2C;AAC3C,kOAAkO;AAClO,SAAgB,qBAAqB,CAAC,KAAsB,EAAE,QAAsB,EAAE,EAAgB;IAClG,mGAAmG;IACnG,8FAA8F;IAC9F,oGAAoG;IACpG,6FAA6F;IAC7F,wCAAwC;IACxC,IAAI,IAAA,kCAAmB,EAAC,QAAQ,CAAC,gBAAgB,CAAC;QAAE,OAAO,EAAE,CAAC;IAC9D,IAAI,EAAE,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAS,EAAW,EAAE,CAAC,IAAA,wBAAW,EAAC,CAAC,EAAE,QAAQ,CAAC,YAAY,CAAC,CAAC;QAAE,OAAO,EAAE,CAAC;IAC5F,OAAO,KAAK,CAAC;AACjB,CAAC;AAED;;;;;;;;GAQG;AACH,0GAA0G;AAC1G,SAAgB,gBAAgB,CAAC,IAAmB,EAAE,aAAqB;IACvE,OAAO,IAAI,0BAAY,CACnB,IAAI,CAAC,QAAQ,CAAC,aAAa,EAAE,IAAI,CAAC,YAAY,CAAC,EAC/C,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,IAAI,EAAE,IAAI,CAAC,YAAY,CAAC,EAC3C,IAAI,CAAC,IAAI,CACZ,CAAC;AACN,CAAC","sourcesContent":["import * as path from 'path';\n\nimport { ExcludePaths, isWebpiecesStateDir } from '@webpieces/rules-config';\n\nimport { EffectiveTree } from './effective-tree';\nimport { globMatches } from './load-rules';\nimport { GovernedPath } from './target-tree';\nimport type { Rule } from './types';\n\n/**\n * L1's FILTER — which rules have jurisdiction over one path — and the one helper that builds its\n * argument for the bash surface.\n *\n * Lifted out of runner.ts, which the file-size rule had outgrown. It is a natural seam rather than a\n * page-count fix: this is the only place `excludePaths` and the hard-coded `.webpieces/` skip are\n * consulted, and runner.ts is otherwise the four tool ENTRY POINTS and their L0/L1 preamble.\n */\n\n// Drop every rule excluded for this path (webpieces.config.json → excludePaths). ONE glob list: a path\n// listed there is hands-off for code-style rules and file-scoped guards alike, because webpieces either\n// governs a path or it does not. Per-rule carve-outs live in the rule's own `excludePaths`.\n// This is L1's FILTER (not a table row) — see guards/L1-location.md.\n//\n// It takes a GovernedPath and not a bare string because the two questions below are asked against\n// DIFFERENT roots, and answering both from the governed-root spelling is issue #851's secondary defect:\n// the excludePaths globs are authored against the governed root, while `.webpieces/` is the state dir OF\n// A TREE and a linked worktree has its own. One argument carrying both spellings is what stops a caller\n// picking the wrong one; see GovernedPath.\n// webpieces-disable no-function-outside-class -- L1's filter, a pure predicate over one path and one glob list; it is called from four module-scope entry points in runner.ts and a class here would be a namespace with no state\nexport function filterByExcludedPaths(rules: readonly Rule[], governed: GovernedPath, ex: ExcludePaths): readonly Rule[] {\n // webpieces' OWN gitignored state dir is never governed, config or no config. Ahead of the list on\n // purpose — see isWebpiecesStateDir for why it is code and not a seeded glob. Asked about the\n // OWNING tree's spelling, so `<primary>/.claude/worktrees/agent-X/.webpieces/...` is exempt for the\n // identical reason `<primary>/.webpieces/...` is: nothing under it is tracked, reviewable or\n // revertable in the tree it belongs to.\n if (isWebpiecesStateDir(governed.treeRelativePath)) return [];\n if (ex.paths.some((p: string): boolean => globMatches(p, governed.relativePath))) return [];\n return rules;\n}\n\n/**\n * The bash surface's GovernedPath, built from the tree EffectiveTreeResolver has already classified —\n * so it costs no extra git call on the hook's blocking path.\n *\n * Both spellings of the command's effective cwd: relative to the governed root for the excludePaths\n * globs, and relative to the OWNING tree for the `.webpieces/` skip — which for\n * `cd <worktree>/.webpieces && …` is the worktree's state dir, not the primary's. Both are '' for a\n * command with no leading `cd`, which matches no glob and is not the state dir.\n */\n// webpieces-disable no-function-outside-class -- the bash-side constructor for the value above, beside it\nexport function bashGovernedPath(tree: EffectiveTree, workspaceRoot: string): GovernedPath {\n return new GovernedPath(\n path.relative(workspaceRoot, tree.effectiveCwd),\n path.relative(tree.root, tree.effectiveCwd),\n tree.root,\n );\n}\n"]}
@@ -40,19 +40,19 @@ class ValidateTsInSrcRule extends rule_base_1.FileRuleBase {
40
40
  const excludePaths = this.config.excludePaths ?? DEFAULT_EXCLUDE_PATHS;
41
41
  const allowedRootFiles = this.config.allowedRootFiles ?? DEFAULT_ALLOWED_ROOT_FILES;
42
42
  // Holistic exclusion (Layer 1 + Layer 2): bare dir names + globs.
43
- if ((0, rules_config_1.isPathExcluded)(ctx.relativePath, excludePaths))
43
+ if ((0, rules_config_1.isPathExcluded)(ctx.targetRelativePath, excludePaths))
44
44
  return [];
45
- const relParts = ctx.relativePath.split(path.sep);
45
+ const relParts = ctx.targetRelativePath.split(path.sep);
46
46
  if (relParts.length === 1 && allowedRootFiles.indexOf(relParts[0] ?? '') >= 0)
47
47
  return [];
48
- const projectRoot = findProjectRoot(ctx.filePath, ctx.workspaceRoot);
48
+ const projectRoot = findProjectRoot(ctx.filePath, ctx.targetRoot);
49
49
  if (!projectRoot) {
50
- return [new types_1.Violation(1, ctx.relativePath, 'File is not inside any Nx project. Move it into a project\'s src/ directory.')];
50
+ return [new types_1.Violation(1, ctx.targetRelativePath, 'File is not inside any Nx project. Move it into a project\'s src/ directory.')];
51
51
  }
52
52
  const relToProject = path.relative(projectRoot, ctx.filePath);
53
53
  if (!relToProject.startsWith('src' + path.sep) && relToProject !== 'src') {
54
- const projectName = path.relative(ctx.workspaceRoot, projectRoot);
55
- return [new types_1.Violation(1, ctx.relativePath, `File is inside project \`${projectName}\` but outside its src/ directory. Move it into src/.`)];
54
+ const projectName = path.relative(ctx.targetRoot, projectRoot);
55
+ return [new types_1.Violation(1, ctx.targetRelativePath, `File is inside project \`${projectName}\` but outside its src/ directory. Move it into src/.`)];
56
56
  }
57
57
  return [];
58
58
  }
@@ -1 +1 @@
1
- {"version":3,"file":"validate-ts-in-src.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/ai-hook-rules/src/core/rules/validate-ts-in-src.ts"],"names":[],"mappings":"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","sourcesContent":["import * as fs from 'fs';\nimport * as path from 'path';\n\nimport { ValidateTsInSrcConfig, isPathExcluded, Option } from '@webpieces/rules-config';\n\nimport type { FileContext, Violation } from '../types';\nimport { Violation as V } from '../types';\nimport { FileRuleBase } from '../rule-base';\nimport { FixHint } from '../fix-hint';\n\nconst DEFAULT_EXCLUDE_PATHS = [\n 'node_modules', 'dist', '.nx', '.git',\n '**/*.d.ts', '**/jest.config.ts',\n];\nconst DEFAULT_ALLOWED_ROOT_FILES = ['jest.setup.ts'];\n\nfunction findProjectRoot(filePath: string, workspaceRoot: string): string | null {\n let dir = path.dirname(filePath);\n while (dir !== workspaceRoot && dir.startsWith(workspaceRoot)) {\n if (fs.existsSync(path.join(dir, 'project.json'))) return dir;\n dir = path.dirname(dir);\n }\n return null;\n}\n\nexport class ValidateTsInSrcRule extends FileRuleBase<ValidateTsInSrcConfig> {\n constructor(config: ValidateTsInSrcConfig) { super(config, 'validate-ts-in-src', 'validate-ts-in-src'); }\n\n readonly description = 'Every .ts file must belong to a project\\'s src/ directory.';\n override readonly files = ['**/*.ts', '**/*.tsx'];\n override readonly defaultOptions = {\n excludePaths: DEFAULT_EXCLUDE_PATHS,\n allowedRootFiles: DEFAULT_ALLOWED_ROOT_FILES,\n };\n readonly fixHint = new FixHint(\n 'TypeScript file is outside a project src/ directory.',\n 'Fix by one of:',\n [\n new Option('Move the file into an existing project\\'s src/ directory, or create a new project with project.json that owns the directory.', true),\n new Option('Add a dir or glob (e.g. \"**/codegen.ts\") to validate-ts-in-src.excludePaths in webpieces.config.json'),\n ],\n );\n\n check(ctx: FileContext): readonly Violation[] {\n if (ctx.tool !== 'Write') return [];\n\n const excludePaths = this.config.excludePaths ?? DEFAULT_EXCLUDE_PATHS;\n const allowedRootFiles = this.config.allowedRootFiles ?? DEFAULT_ALLOWED_ROOT_FILES;\n\n // Holistic exclusion (Layer 1 + Layer 2): bare dir names + globs.\n if (isPathExcluded(ctx.relativePath, excludePaths)) return [];\n\n const relParts = ctx.relativePath.split(path.sep);\n if (relParts.length === 1 && allowedRootFiles.indexOf(relParts[0] ?? '') >= 0) return [];\n\n const projectRoot = findProjectRoot(ctx.filePath, ctx.workspaceRoot);\n\n if (!projectRoot) {\n return [new V(\n 1,\n ctx.relativePath,\n 'File is not inside any Nx project. Move it into a project\\'s src/ directory.',\n )];\n }\n\n const relToProject = path.relative(projectRoot, ctx.filePath);\n if (!relToProject.startsWith('src' + path.sep) && relToProject !== 'src') {\n const projectName = path.relative(ctx.workspaceRoot, projectRoot);\n return [new V(\n 1,\n ctx.relativePath,\n `File is inside project \\`${projectName}\\` but outside its src/ directory. Move it into src/.`,\n )];\n }\n\n return [];\n }\n}\n"]}
1
+ {"version":3,"file":"validate-ts-in-src.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/ai-hook-rules/src/core/rules/validate-ts-in-src.ts"],"names":[],"mappings":"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","sourcesContent":["import * as fs from 'fs';\nimport * as path from 'path';\n\nimport { ValidateTsInSrcConfig, isPathExcluded, Option } from '@webpieces/rules-config';\n\nimport type { FileContext, Violation } from '../types';\nimport { Violation as V } from '../types';\nimport { FileRuleBase } from '../rule-base';\nimport { FixHint } from '../fix-hint';\n\nconst DEFAULT_EXCLUDE_PATHS = [\n 'node_modules', 'dist', '.nx', '.git',\n '**/*.d.ts', '**/jest.config.ts',\n];\nconst DEFAULT_ALLOWED_ROOT_FILES = ['jest.setup.ts'];\n\nfunction findProjectRoot(filePath: string, workspaceRoot: string): string | null {\n let dir = path.dirname(filePath);\n while (dir !== workspaceRoot && dir.startsWith(workspaceRoot)) {\n if (fs.existsSync(path.join(dir, 'project.json'))) return dir;\n dir = path.dirname(dir);\n }\n return null;\n}\n\nexport class ValidateTsInSrcRule extends FileRuleBase<ValidateTsInSrcConfig> {\n constructor(config: ValidateTsInSrcConfig) { super(config, 'validate-ts-in-src', 'validate-ts-in-src'); }\n\n readonly description = 'Every .ts file must belong to a project\\'s src/ directory.';\n override readonly files = ['**/*.ts', '**/*.tsx'];\n override readonly defaultOptions = {\n excludePaths: DEFAULT_EXCLUDE_PATHS,\n allowedRootFiles: DEFAULT_ALLOWED_ROOT_FILES,\n };\n readonly fixHint = new FixHint(\n 'TypeScript file is outside a project src/ directory.',\n 'Fix by one of:',\n [\n new Option('Move the file into an existing project\\'s src/ directory, or create a new project with project.json that owns the directory.', true),\n new Option('Add a dir or glob (e.g. \"**/codegen.ts\") to validate-ts-in-src.excludePaths in webpieces.config.json'),\n ],\n );\n\n check(ctx: FileContext): readonly Violation[] {\n if (ctx.tool !== 'Write') return [];\n\n const excludePaths = this.config.excludePaths ?? DEFAULT_EXCLUDE_PATHS;\n const allowedRootFiles = this.config.allowedRootFiles ?? DEFAULT_ALLOWED_ROOT_FILES;\n\n // Holistic exclusion (Layer 1 + Layer 2): bare dir names + globs.\n if (isPathExcluded(ctx.targetRelativePath, excludePaths)) return [];\n\n const relParts = ctx.targetRelativePath.split(path.sep);\n if (relParts.length === 1 && allowedRootFiles.indexOf(relParts[0] ?? '') >= 0) return [];\n\n const projectRoot = findProjectRoot(ctx.filePath, ctx.targetRoot);\n\n if (!projectRoot) {\n return [new V(\n 1,\n ctx.targetRelativePath,\n 'File is not inside any Nx project. Move it into a project\\'s src/ directory.',\n )];\n }\n\n const relToProject = path.relative(projectRoot, ctx.filePath);\n if (!relToProject.startsWith('src' + path.sep) && relToProject !== 'src') {\n const projectName = path.relative(ctx.targetRoot, projectRoot);\n return [new V(\n 1,\n ctx.targetRelativePath,\n `File is inside project \\`${projectName}\\` but outside its src/ directory. Move it into src/.`,\n )];\n }\n\n return [];\n }\n}\n"]}
@@ -119,7 +119,7 @@ function runInternal(toolKind, input, cwd, mode) {
119
119
  const outOfSync = checkConfigSync(rules.filter((r) => !(r instanceof match_rule_1.MatchRule)), loaded.rulesConfig);
120
120
  if (outOfSync)
121
121
  return outOfSync;
122
- const contexts = (0, build_context_1.buildContexts)(toolKind, input, workspaceRoot);
122
+ const contexts = (0, build_context_1.buildContexts)(toolKind, input, workspaceRoot, governed);
123
123
  const editGroups = runEditRules(rules, contexts.editContexts);
124
124
  const fileGroups = runFileRules(rules, contexts.fileContext);
125
125
  const allGroups = [...editGroups, ...fileGroups];
@@ -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 { loadAndValidate, LoadedConfig, WebpiecesRulesConfig, isHookGuard, HomeConfigService, RepoRootFinder, seedEntryForRule, CONFIG_FILENAME, renderRuleFailForAi } from '@webpieces/rules-config';\n\nimport { buildContexts, buildBashContext } from './build-context';\nimport { DeleteScopedRules } from './delete-scoped-rules';\nimport { VersionSyncGuard } from './version-sync';\nimport { EffectiveTree, EffectiveTreeResolver } from './effective-tree';\nimport { GovernedPath, TargetTreeResolver } from './target-tree';\nimport { bashGovernedPath, filterByExcludedPaths } from './excluded-paths';\nimport { ExcludedPathEscapeHint } from './excluded-path-escape';\nimport { gitFromSubdirBlock } from './force-to-root';\nimport { loadRules, loadMatchRules, loadKeylessBashRules, globMatches, GuardHintCommands } from './load-rules';\nimport { missingDirectoryBlock } from './missing-directory';\nimport { MatchRule } from './rules/match-rule';\nimport { branchStateHangTimeout, maybeRefreshMainSync } from './main-sync-timeout';\nimport { logGuardDecision, logL1Decision, GuardDecision, branchForLog, MatrixRef, Verdict, MATRIX_L0_ALLOW, MATRIX_L2_UNROWED } from './decision-log';\nimport { toError } from './to-error';\nimport { formatReport, READ_SUBJECT, BASH_SUBJECT } from './report';\nimport { ReadOnlyInspectionScan } from './read-only-inspection';\nimport { L0_CURE_ALLOW_JS, isAllowed, isRootManifest } from '../bin/shim';\nimport { AiType } from './agent-event';\nimport { L0_FAULT_CONFIG_MISSING, L0_FAULT_CONFIG_OUT_OF_SYNC, L0_FAULT_NONE } from './l0-fault-codes';\nimport { CONFIG_MISSING_REPORT, CONFIG_OUT_OF_SYNC_HEADER, writeGuardMatrixDoc, guardMatrixPointer } from './l0-matrix';\nimport { L1Classification, firstMatchingL1Row, L1_PRESTAGE_ROW } from './l1-rows';\nimport { withLocationMatrixPointer } from './l1-matrix-doc';\nimport {\n ToolKind, NormalizedToolInput, BlockedResult, HookMode,\n Rule, Violation, RuleGroup, RuleFailError, InformAiError,\n EditContext, FileContext, BashContext,\n} from './types';\n\n// Restrict loaded rules to the category this hook invocation runs. The two split hooks each pass a\n// disjoint category ('rules' = code-style, 'guards' = the hookGuards section); 'all' runs both (the\n// openclaw plugin adapter, a single before_tool_call hook). isHookGuard is the shared classifier in\n// @webpieces/rules-config.\n// isHookGuard is asked about the rule's CONFIG KEY, never its name. Since the collapse those differ\n// for every class behind a policy key — `feature-branch-guard` is a rule NAME whose key is\n// `branch-state-guard` — and asking about the name would classify all eight collapsed guards as\n// code-style rules, i.e. run them in the wrong hook and never in the guards hook at all.\nfunction filterByMode(rules: readonly Rule[], mode: HookMode): readonly Rule[] {\n if (mode === 'all') return rules;\n if (mode === 'guards') return rules.filter((r: Rule): boolean => isHookGuard(r.configKey));\n return rules.filter((r: Rule): boolean => !isHookGuard(r.configKey));\n}\n\n// The cwd a command actually runs from, after its own leading `cd`/`pushd` run. Thin delegate kept\n// for the callers (and specs) that only need the directory; the full tree classification — primary\n// clone vs linked worktree vs nested clone vs outside any repo — is EffectiveTreeResolver.resolve().\n// webpieces-disable no-function-outside-class -- sibling of the module-scope runner helpers; the whole file is functions and a lone class here would break its shape\nexport function effectiveBashCwd(command: string, cwd: string): string {\n return new EffectiveTreeResolver().effectiveCwd(command, cwd);\n}\n\n// The resolved gated-command strings the PR-lifecycle guards print, straight off the loaded\n// `commands.guardHints`. Handed to the rules at construction rather than injected into their config\n// entries under guard-name literals — the injection this replaces could miss a rename silently.\n// webpieces-disable no-function-outside-class -- sibling of the module-scope runner helpers; the whole file is functions and a lone class here would break its shape\nfunction guardHintsOf(loaded: LoadedConfig): GuardHintCommands {\n return new GuardHintCommands(loaded.commands.upsertPr, loaded.commands.mergeComplete);\n}\n\n// A git or gh invocation anywhere in the command (start, or after a ;/&&/|| separator or pipe).\nconst GIT_OR_GH_RE = /(?:^|[;&|]\\s*)(?:git|gh)\\b/;\nexport function isGitOrGhCommand(command: string): boolean {\n return GIT_OR_GH_RE.test(command);\n}\n\n// Fault C (webpieces.config.json missing) — the deny text lives in ./l0-matrix beside the rest of the\n// L0 fault table, so the message and the allowlist can never prescribe different cures. `cwd` is used\n// only to drop the matrix doc where the AI can read it (the config root does not exist yet here).\n//\n// THE ALLOWLIST ESCAPE IS THE CALLER'S (moved out 2026-09-01): behind an optional `command` it could\n// not see the harness, so the gated half of the list was unreachable under C while\n// CONFIG_MISSING_REPORT promised it. See l0FaultAllows. This has one job now: build the block.\n// webpieces-disable no-function-outside-class -- sibling of the module-scope runner helpers; the whole file is functions and a lone class here would break its shape\nfunction configMissingBlock(cwd: string): BlockedResult {\n const root = new RepoRootFinder().resolveRepoRoot(cwd);\n // Stamped with its L0 letter so the block is greppable as fault C wherever it is recorded — the\n // adapter carries it to the audit line rather than re-deriving it from the report text.\n return new BlockedResult(CONFIG_MISSING_REPORT + guardMatrixPointer(writeGuardMatrixDoc(root)), L0_FAULT_CONFIG_MISSING);\n}\n\nexport function run(\n toolKind: ToolKind,\n input: NormalizedToolInput,\n cwd: string,\n mode: HookMode = 'all',\n): BlockedResult | null {\n return runInternal(toolKind, input, cwd, mode);\n}\n\nfunction runInternal(\n toolKind: ToolKind,\n input: NormalizedToolInput,\n cwd: string,\n mode: HookMode,\n): BlockedResult | null {\n const loaded = loadAndValidate(cwd);\n if (loaded.configPath === null) return configMissingBlock(cwd);\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 // …and the same PASS for the machine-local `~/.webpieces/config.json` (a bad key there fails a `wp-*`\n // command with an instruction to edit it, so blocking the edit would wedge the agent inside the\n // failure) and for the two ROOT MANIFESTS a pin lives in — L1 row 8 PROMISED that edit and its cure IS\n // it. isRootManifest, never a basename: this returns EARLY, so basename would unrule every\n // package.json in the repo. It admits any tree's root (the config sits beside it), which is what a\n // worktree's own cure needs — the same tree `workspaceRoot` above already names.\n if (isRootManifest(input.filePath) || new HomeConfigService().isHomeConfigPath(input.filePath)) {\n return null;\n }\n\n // Built-in/custom rules PLUS the client-authored match-rules (content guards). Match-rules run only\n // in the file-edit path (they are code-style, so filterByMode keeps them out of the bash/guards path).\n const allRules = [...loadRules(loaded.rulesConfig, workspaceRoot, guardHintsOf(loaded)), ...loadMatchRules(loaded.matchRules)];\n const modeRules = filterByMode(allRules, mode);\n if (modeRules.length === 0) return null;\n\n // Suppress enforcement for files under this category's excludePaths (e.g. vendored repos under\n // repositories/**). Exclusion is all-or-nothing per category, so an excluded file drops the whole\n // rule set and is fully hands-off — no violations AND no config-sync nag on those files.\n const governed = new TargetTreeResolver().governedPath(input.filePath, workspaceRoot);\n const relativePath = governed.relativePath;\n const rules = new DeleteScopedRules().narrow(toolKind, filterByExcludedPaths(modeRules, governed, loaded.excludePaths));\n if (rules.length === 0) return null;\n\n // Config-sync applies only to built-in/custom rules; match-rules have their own validated section\n // (loadAndValidate already rejected an invalid `match-rules`), so they must not trip the sync nag.\n const outOfSync = checkConfigSync(rules.filter((r: Rule) => !(r instanceof MatchRule)), loaded.rulesConfig);\n if (outOfSync) return outOfSync;\n\n const contexts = buildContexts(toolKind, input, workspaceRoot);\n\n const editGroups = runEditRules(rules, contexts.editContexts);\n const fileGroups = runFileRules(rules, contexts.fileContext);\n const allGroups = [...editGroups, ...fileGroups];\n\n if (allGroups.length === 0) return null;\n\n const report = formatReport(relativePath, allGroups);\n return new BlockedResult(report);\n}\n\n// SINGLETON, deliberately: WebpiecesVersions memoizes per root, and the same two roots are asked for on\n// every Bash call. A fresh instance per call would spawn `git worktree list` and re-read two manifests\n// on the hook's blocking path each time.\nconst VERSION_SYNC = new VersionSyncGuard();\n\n// There is deliberately NO agent parameter. It existed only for CoordinatorWorktreeGuard, which asked\n// WHO was calling; that guard is gone and its replacement asks WHICH TREE the command acts on. Agent\n// identity was measured untrustworthy for that question anyway — a worktree-isolated agent auto-reaped\n// at a turn boundary silently resumes with its cwd on the primary clone — so a guard must never infer\n// a tree from who is asking.\n// webpieces-disable no-function-outside-class -- sibling of run()/runRead() in this module; the whole runner is module-scope functions and a lone class for this one entry point would break the file's shape\nexport function runBash(command: string, cwd: string, mode: HookMode, aiType: AiType): BlockedResult | null {\n return runBashInternal(command, cwd, mode, aiType);\n}\n\n// The name of the ONLY rule permitted to block a Read. Reads are the highest-blast-radius tool\n// there is, so this path is an explicit single-rule allowlist rather than the general rule loop.\nconst READ_SCOPED_GUARDS: ReadonlySet<string> = new Set(['read-stale-guard']);\n\n/**\n * The Read path. Deliberately NOT `run()`:\n *\n * - NO config-sync check. A rule present in code but missing from webpieces.config.json blocks\n * every Write/Edit/Bash by design — but applying that to Read would mean an upgrade that adds\n * any new rule instantly blocks the agent from reading the very config file it must edit to fix\n * it. Reads must never carry that failure mode.\n * - NO general rule loop. Only READ_SCOPED_GUARDS run, so no code-style rule can ever see a Read.\n * - Fails OPEN everywhere, including on a thrown rule (the caller catches and allows).\n *\n * Returns null (allow) unless the one guard fires.\n */\n// webpieces-disable no-function-outside-class -- sibling of run()/runBash() in this module; the whole runner is module-scope functions and a lone class for this one entry point would break the file's shape\nexport function runRead(filePath: string, cwd: string, mode: HookMode = 'all'): BlockedResult | null {\n // Code-style mode has nothing to say about a read.\n if (mode === 'rules') return null;\n\n const loaded = loadAndValidate(cwd);\n // No config → nothing to enforce. Unlike the edit path we do NOT block: an unconfigured repo\n // must still be readable.\n if (loaded.configPath === null) return null;\n\n const workspaceRoot = path.dirname(loaded.configPath);\n\n // Same git-repo-boundary governance as bash, through the SAME resolver: a read inside a different\n // clone is out of scope. (No command to parse here, so the shell cwd IS the effective cwd.)\n if (new EffectiveTreeResolver().resolve('', cwd, workspaceRoot).kind === 'foreign') return null;\n\n const governed = new TargetTreeResolver().governedPath(filePath, workspaceRoot);\n const relativePath = governed.relativePath;\n const all = loadRules(loaded.rulesConfig, workspaceRoot, guardHintsOf(loaded));\n const rules = filterByExcludedPaths(\n all.filter((r: Rule): boolean => READ_SCOPED_GUARDS.has(r.name)),\n governed,\n loaded.excludePaths,\n );\n if (rules.length === 0) return null;\n\n const ctx = new FileContext('Read', filePath, relativePath, workspaceRoot, 0, 0, 0, 0);\n const groups = runFileRules(rules, ctx);\n if (groups.length === 0) return null;\n\n return new BlockedResult(formatReport(relativePath, groups, READ_SUBJECT));\n}\n\n// L0 cure bypass — every command on THE L0 allowlist passes here, ahead of any config load. A\n// webpieces.config.json that is ahead of the installed validator (new rule tokens the published\n// binary doesn't know yet) makes loadAndValidate() throw and would deny `pnpm install` — the very\n// command that updates the validator (deadlock).\n//\n// This used to test INSTALLER_ALLOW_JS alone, which made the config faults (C = config missing,\n// Y = config out of sync) accept a bare `pnpm install` while denying `rm -rf node_modules && pnpm\n// install` — the one cure that works when node_modules is CORRUPT rather than merely stale. Same\n// intent, opposite verdict, for no reason anyone recorded. Each alternative is still anchored at both\n// ends, so `pnpm install && rm -rf /` still falls to the guards.\n//\n// It tests the CURE subset (L0_CURE_ALLOW_JS), not the whole list, and the distinction matters here\n// and nowhere else: this call site runs BEFORE any fault has been established, so whatever it accepts\n// is waved past the L1 guards on a perfectly healthy repo too. A repair command has to be waved past —\n// it runs before the config can be loaded at all. Read-only ORIENTATION does not: it fixes nothing, and\n// letting `git status` through here would delete force-to-root as a side effect of adding `pwd`. Under\n// an actual fault, l0FaultAllows() below consults the WHOLE list.\n// webpieces-disable no-function-outside-class -- sibling of the module-scope runner helpers; the whole file is functions and a lone class here would break its shape\nfunction isL0CureCommand(command: string): boolean {\n return L0_CURE_ALLOW_JS.test(command.trim());\n}\n\n// The FULL L0 list, asked only where this file has actually detected an L0 fault (C = config missing,\n// Y = config out of sync). D/X/U/K are decided in the shim, which greps the same union.\n// IT ASKS `isAllowed()`, not a regex: that is the ONE question both halves of L0 ask, and the only\n// place harness-gated entries are consulted. Testing `L0_ALLOW_JS` directly (which is what this did)\n// dropped the Codex read under C and Y while D/X/U/K honoured it — a per-fault carve-out again.\n// webpieces-disable no-function-outside-class -- sibling of the module-scope runner helpers; the whole file is functions and a lone class here would break its shape\nfunction l0FaultAllows(command: string, aiType: AiType): boolean {\n return isAllowed('Bash', command.trim(), '', aiType) !== null;\n}\n\n// The L0 cure bypass's audit line. Anchored at the repo root that owns `.webpieces` — RepoRootFinder\n// (config-walk-up first, then git toplevel) is the authority for that, and it is correct in a linked\n// worktree because each worktree checks out its own webpieces.config.json. This runs BEFORE\n// loadAndValidate, which is why it resolves the root itself rather than using workspaceRoot.\n// webpieces-disable no-function-outside-class -- sibling of the module-scope runner helpers; the whole file is functions and a lone class here would break its shape\nfunction logL0CureBypass(command: string, cwd: string): void {\n const root = new RepoRootFinder().resolveRepoRoot(cwd);\n logGuardDecision(root, new GuardDecision('-', 'Bash', command, branchForLog(root), 'ALLOW', 'L0 cure bypass (always allowed)', '-', L0_FAULT_NONE, MATRIX_L0_ALLOW));\n}\n\n/**\n * Load the config for the bash path — but do NOT let an unloadable config trap the tools needed to\n * repair it.\n *\n * loadAndValidate throws an InformAiError when webpieces.config.json is unparseable (a real syntax\n * error, or leftover `<<<<<<< HEAD` markers mid-merge) or fails validation. That throw propagates to\n * the hook adapter, which fails CLOSED and denies the command — correct for work, since a config that\n * did not load means no guards ran. But it denied `cat`/`grep`/`sed -n` on webpieces.config.json too,\n * i.e. it blocked the only way to see the problem it was reporting. Observed live, twice.\n *\n * So: on a load failure, a provably-inert INSPECTION command is allowed through (returns null, \"no\n * block\"), matching the escape hatch every other layer already grants this file. Everything else —\n * every write, every git/gh command, every build — still hits the same hard failure as before. The\n * bypass cannot be widened by accident; see ReadOnlyInspectionScan for how narrow \"inert\" is.\n *\n * Returns the loaded config, or null meaning \"allow this command without guards\".\n */\n// webpieces-disable no-function-outside-class -- sibling of the module-scope runner helpers; the whole file is functions and a lone class here would break its shape\nfunction loadConfigOrAllowInspection(command: string, cwd: string): LoadedConfig | null {\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions -- rethrown unchanged unless the command is provably inert\n try {\n return loadAndValidate(cwd);\n } catch (err: unknown) {\n const error = toError(err);\n if (error instanceof InformAiError && new ReadOnlyInspectionScan().isReadOnlyInspection(command)) {\n return null;\n }\n throw error;\n }\n}\n\n// Version skew: this worktree pins a different @webpieces than the MAIN tree that governs it. The guard\n// hooks are absolute, so the main tree's binary judges every tree — which is fine until the two trees\n// disagree about which release that should be. L1 row 8; guards/L1-location.md carries the table.\n// webpieces-disable no-function-outside-class -- sibling of the other module-scope runner helpers; the whole file is functions and a lone class here would break its shape\nfunction versionSkewBlock(command: string, tree: EffectiveTree): BlockedResult | null {\n const report = VERSION_SYNC.block(command, tree);\n return report === null ? null : new BlockedResult(report);\n}\n\n// Where the command lands in L1's five dimensions (K/A/R/G/P). The ONE place the runner's view of the\n// world is translated into the matrix's vocabulary — see L1Classification.forEnforcement for why\n// TreeKind 'outside' currently classifies as `p`.\n// webpieces-disable no-function-outside-class -- sibling of the other module-scope runner helpers; the whole file is functions and a lone class here would break its shape\nfunction l1Classify(command: string, tree: EffectiveTree): L1Classification {\n return L1Classification.forEnforcement(\n tree.kind,\n VERSION_SYNC.skewed(tree),\n new ReadOnlyInspectionScan().isReadOnlyInspection(command),\n isGitOrGhCommand(command),\n path.resolve(tree.effectiveCwd) === path.resolve(tree.root),\n );\n}\n\n// The structural L1 blocks, in order: the misplaced-`cd` PRE-STAGE, then version skew (row 8), then\n// force-to-root (row 5). None is a configurable rule — they are decided from the command text, the\n// resolved tree and the caller, so they run as one step here rather than as three near-identical\n// stanzas in runBashInternal.\n//\n// For the two TREE-BASED blocks the ORDER and the CHOICE are not written here: they come from L1_ROWS\n// (l1-rows.ts), the same array guards/L1-location.md is rendered from. Classify, take the first\n// matching row, dispatch on its blockId — so a row deleted from the array is a block that stops firing,\n// and the doc cannot describe a table the guard does not consult. The two report builders below still\n// own their own predicates and their deny strings; l1-matrix.spec.ts asserts the row lookup and those\n// predicates agree.\n//\n// misplacedCdBlock is deliberately OUTSIDE that lookup, and runs ahead of it, for the reason its own\n// docblock gives: it decides from command TEXT, before a tree has been resolved, and the other two\n// reason FROM the resolved tree. Classifying it would mean asking L1_ROWS a question whose answer the\n// classification itself depends on. It is the same shape as L2's `bareCheckoutOfMain` pre-stage. It is\n// therefore ROW 0 — \"pre-stage, decided from command text\" — rather than a row among 1-6, which keeps\n// it IN the table (the drift this table exists to prevent) without pretending it is classified over the\n// same five dimensions. `L1_PRESTAGE_ROW` is the number the doc prints and `row=` logs, so the two join.\n//\n// THIS FUNCTION IS THE ONE PLACE L1 REPORTS. It is the only scope holding the classification, the\n// matched row, the resolved tree and the agent at once — so logging here, BEFORE the early return,\n// is what finally records the outcomes that were previously invisible: the exempt row and the three\n// hand-downs wrote nothing at all, which is why \"L1 had no objection\" could not be observed and\n// \"show me every L1 decision\" had no answer. The three block helpers no longer log for themselves —\n// and, for the same reason, none of them knows about the matrix POINTER either: every deny is stamped\n// with it HERE, from the same scope and the same row number, so deny and log line cannot disagree.\n// webpieces-disable no-function-outside-class -- sibling of the other module-scope runner helpers; the whole file is functions and a lone class here would break its shape\nfunction l1LocationBlock(command: string, tree: EffectiveTree): BlockedResult | null {\n const misplacedCd = misplacedCdBlock(command, tree);\n if (misplacedCd !== null) {\n logL1(tree, command, 'BLOCK_AI_CURE', L1_PRESTAGE_ROW, 'cd-must-be-first', 'cd not leading/literal');\n return withLocationMatrixPointer(misplacedCd, tree.root, L1_PRESTAGE_ROW);\n }\n\n const row = firstMatchingL1Row(l1Classify(command, tree));\n const rowNum = String(row.num);\n if (row.blockId === null) {\n // ALLOW_EXEMPT stops here; ALLOW means \"no objection, handed down to L2\". Recording the\n // difference is the point — see Verdict. Neither claims the call RAN: the other parallel hook\n // may still have denied it.\n logL1(tree, command, row.action.kind === 'exempt' ? 'ALLOW_EXEMPT' : 'ALLOW', rowNum, '-', row.why);\n return null;\n }\n logL1(tree, command, 'BLOCK_AI_CURE', rowNum, row.blockId, row.why);\n if (row.blockId === 'missing-directory') return withLocationMatrixPointer(missingDirectoryBlock(command, tree), tree.root, rowNum);\n if (row.blockId === 'trinary-version-skew') return withLocationMatrixPointer(versionSkewBlock(command, tree), tree.root, rowNum);\n return withLocationMatrixPointer(gitFromSubdirBlock(command, tree, isGitOrGhCommand(command)), tree.root, rowNum);\n}\n\n// One L1 line, into L1's OWN stream. `row` is the number the generated doc prints, so a reader who sees\n// `row=5` can open the L1 matrix at row 5 — the deny for that same call names it by absolute path.\n// eslint-disable-next-line @typescript-eslint/max-params -- the six fields of one log line, and a class here would break this file's shape\n// webpieces-disable no-function-outside-class -- sibling of the module-scope runner helpers; the whole file is functions\nfunction logL1(tree: EffectiveTree, command: string, verdict: Verdict, row: string, rule: string, why: string): void {\n logL1Decision(tree.root, new GuardDecision(\n rule, 'Bash', command, branchForLog(tree.root), verdict, why, '-', L0_FAULT_NONE, new MatrixRef('L1', row),\n ));\n}\n\n/**\n * ONE legal shape for relocating a command: `cd <literal path> && <work>`. Anything else — a `cd` after\n * another command or after a `VAR=…` assignment, or a `cd \"$DIR\"` the guard cannot expand — is refused\n * here instead of being silently judged from the shell cwd.\n *\n * This changes no verdict: every command it refuses was ALREADY judged from the shell cwd, so nothing\n * it blocks was previously being allowed on the strength of its `cd`. What changes is that the agent\n * finds out. Two PRs of increasingly precise near-miss wording (#596, #597) still left the fact in a\n * paragraph appended to an unrelated block; the rule is simpler stated as a rule.\n *\n * FIRST in the L1 chain deliberately. version skew and force-to-root both reason from the\n * resolved tree, and if the `cd` did not resolve, that tree is not the one the agent thinks they are\n * in — so their remedies would be steering from a location the command does not actually run in.\n */\n// webpieces-disable no-function-outside-class -- sibling of the other module-scope runner helpers; the whole runner is module-scope functions\nfunction misplacedCdBlock(command: string, tree: EffectiveTree): BlockedResult | null {\n const reason = new EffectiveTreeResolver().misplacedCd(command);\n if (reason === null) return null;\n const report =\n `❌ A \\`cd\\` must come FIRST in the command, with a LITERAL path.\\n` +\n ` Why this one was rejected: ${reason}.\\n` +\n ` \\`cd <literal path> && <work>\\` is the ONE shape that moves where webpieces judges a command.\\n` +\n ` Any other \\`cd\\` cannot be resolved, so the command would be judged against ${tree.root}\\n` +\n ` — not where you think it runs. That used to happen silently; this block is that silence, spoken.\\n` +\n ` Fix Option 1: put the \\`cd\\` first, literal path, SAME command: cd /abs/path && <the rest>\\n` +\n ` Fix Option 2: drop the \\`cd\\` — ${tree.root} is where the command is judged anyway.\\n` +\n ` Fix Option 3: if the SECOND \\`cd\\` was there to point a tool at a directory, use that tool's own\\n` +\n ` directory flag instead — \\`git -C <dir>\\`, \\`tar -C <dir>\\`, \\`npm --prefix <dir>\\`,\\n` +\n ` \\`npm pack --pack-destination <dir>\\`, \\`pnpm --dir <dir>\\`. One \\`cd\\`, same result.\\n` +\n ` ONLY for a dir INSIDE this tree. \\`git -C <another tree>\\` is REFUSED to a subagent,\\n` +\n ` and is never the cure for a version skew — for that, tell the MAIN agent.\\n` +\n ` Fix Option 4: split it — run the work in one call, the \\`cd\\` in another (a \\`cd\\` alone still\\n` +\n ` moves nothing the guards judge; see EffectiveTree on why cwd cannot be assumed).`;\n return new BlockedResult(report);\n}\n\n/**\n * The KEYLESS bash guards, deliberately kept OUT of the config-driven rule set. They have no\n * webpieces.config.json entry at all, so passing them through checkConfigSync would make every\n * consumer's next Bash call a fault-Y block (\"this rule has no entry\") for a feature nobody opted into\n * — which is exactly what whole-repo-build-guard did on its first release. Each one decides for itself\n * whether it acts: whole-repo-build-guard reads ~/.webpieces/config.json for a machine-local OPT-IN\n * (absent means OFF); commit-message-substitution-guard and build-output-pipe-guard act unconditionally.\n * They still honour excludePaths, and they do not run in `rules` mode (code-style-only hook).\n */\n// webpieces-disable no-function-outside-class -- sibling of the module-scope runner helpers; the whole file is functions and a lone class here would break its shape\nfunction keylessBashRules(loaded: LoadedConfig, mode: HookMode, governed: GovernedPath): readonly Rule[] {\n if (mode === 'rules') return [];\n return filterByExcludedPaths(\n loadKeylessBashRules(loaded.prGate.buildCommand), governed, loaded.excludePaths,\n );\n}\n\n// webpieces-disable no-function-outside-class -- sibling of run()/runBash() in this module; the whole runner is module-scope functions and a lone class for this one entry point would break the file's shape\nfunction runBashInternal(command: string, cwd: string, mode: HookMode, aiType: AiType): BlockedResult | null {\n if (isL0CureCommand(command)) {\n logL0CureBypass(command, cwd);\n return null;\n }\n\n const loaded = loadConfigOrAllowInspection(command, cwd);\n // null = the config would not load AND this command only inspects → allow, see the helper.\n if (loaded === null) return null;\n if (loaded.configPath === null) return l0FaultAllows(command, aiType) ? null : configMissingBlock(cwd);\n\n const workspaceRoot = path.dirname(loaded.configPath);\n\n // WHICH TREE does this command act on? Not the shell's cwd: a `cd` OUT of the workspace is reset\n // by the harness and one INSIDE it persists, so neither can be assumed (see EffectiveTree).\n // ONE resolver answers this for the guards AND for force-to-root below, so the two can never\n // disagree about which tree you are in.\n const tree = new EffectiveTreeResolver().resolve(command, cwd, workspaceRoot);\n\n // Git-repo-boundary governance: the command runs inside a DIFFERENT git repo than this\n // webpieces.config governs (e.g. a clone under repositories/). Out of scope → allow, hands-off.\n // Intentional, not a silent hole. A LINKED WORKTREE of this repo is deliberately NOT foreign — it\n // is the same project, so the guards run against THAT tree's branch and cache.\n if (tree.kind === 'foreign') {\n logGuardDecision(workspaceRoot, new GuardDecision('-', 'Bash', command, branchForLog(workspaceRoot), 'ALLOW_EXEMPT', 'foreign git repo (out of scope)', '-', L0_FAULT_NONE, new MatrixRef('L1', '1')));\n return null;\n }\n\n // Honour excludePaths on the bash path too (not just Read/Edit): a command whose effective\n // cwd sits under an excluded tree (e.g. repositories/**) drops the whole guard set — matching how\n // runInternal/runRead treat file paths. The relative path is '' when there is no `cd` (root), which\n // matches no exclusion glob, so a plain command at the repo root is unaffected.\n const governedCwd = bashGovernedPath(tree, workspaceRoot);\n const rules = filterByExcludedPaths(\n filterByMode(loadRules(loaded.rulesConfig, workspaceRoot, guardHintsOf(loaded)), mode), governedCwd, loaded.excludePaths,\n );\n const keyless = keylessBashRules(loaded, mode, governedCwd);\n if (rules.length === 0 && keyless.length === 0) return null;\n\n const outOfSync = checkConfigSync(rules, loaded.rulesConfig); // fault Y — L0 list wins, as under C\n if (outOfSync) return l0FaultAllows(command, aiType) ? null : outOfSync;\n\n const locationBlock = l1LocationBlock(command, tree);\n if (locationBlock) return locationBlock;\n\n // Keep the feature-branch-guard cache warm on EVERY command (not just Write/Edit): the AI runs\n // far more bash than edits, so refreshing here means the guard's next file-edit check reads a\n // fresh status. Detached + fire-and-forget — never blocks the command. Only when the guard is\n // loaded (guards/all mode) and enabled, so a project that opted out never triggers git fetches.\n // Keyed on the JUDGED tree, so a worktree's cache is refreshed rather than the primary clone's.\n maybeRefreshMainSync(rules, tree.root, branchStateHangTimeout(loaded.rulesConfig));\n\n const groups = runBashRules([...rules, ...keyless], buildBashContext(command, tree));\n if (groups.length === 0) {\n // Record the ALLOW only for git/gh commands — the operations the bash guards actually reason\n // about (branch create, commit, push, merge, PR). Skipping ls/cat/grep keeps the audit log\n // focused (the whole point of the log is \"why did/didn't a guard fire?\"). Blocks are always\n // logged below.\n if (/\\b(?:git|gh)\\b/.test(command)) {\n logGuardDecision(tree.root, new GuardDecision('-', 'Bash', command, branchForLog(tree.root), 'ALLOW', 'no bash-guard block', '-', L0_FAULT_NONE, MATRIX_L2_UNROWED));\n }\n return null;\n }\n\n const ruleNames = groups.map((g: RuleGroup): string => g.ruleName).join(',');\n logGuardDecision(tree.root, new GuardDecision(ruleNames, 'Bash', command, branchForLog(tree.root), 'BLOCK_AI_CURE', 'bash-guard block', '-', L0_FAULT_NONE, MATRIX_L2_UNROWED));\n const report = new ExcludedPathEscapeHint(workspaceRoot, tree.effectiveCwd).render(command, loaded.excludePaths)\n + formatReport(commandLabel(command), groups, BASH_SUBJECT) + exemptTreesHint(groups, loaded.excludePaths.paths);\n return new BlockedResult(report);\n}\n\n// The bash report's subject line. It used to be the literal string `<bash>`, which told the agent\n// nothing; the command itself is what was blocked, so name it — truncated, because a heredoc-bearing\n// command can run to thousands of characters and the violation lines already carry the detail.\n// webpieces-disable no-function-outside-class -- sibling of the module-scope runner helpers; the whole file is functions and a lone class here would break its shape\nfunction commandLabel(command: string): string {\n const oneLine = command.replace(/\\s+/g, ' ').trim();\n const MAX = 100;\n return oneLine.length <= MAX ? oneLine : oneLine.slice(0, MAX) + '…';\n}\n\n// When a push/PR block fires AND the config exempts vendored/nested trees, surface the escape hatch the\n// AI cannot otherwise discover: git/gh run UNGUARDED inside those trees if it cd's there first (each is\n// governed by its own repo, not this one). Scoped to pr-creation-or-push-guard — for the other guards\n// \"cd into an exempt tree\" is not the remedy — and emitted only when such trees are actually configured,\n// so a repo without exemptions never sees the noise.\n//\n// The hint MUST state the precondition, not just the remedy. \"cd into it first\" is true only for a\n// LITERAL cd at the FRONT of the same command; `D=…; cd \"$D\"; git push` lands in the identical\n// directory and does NOT relocate the verdict. That used to be silent, then a near-miss paragraph\n// appended here; it is now misplacedCdBlock, which refuses the command outright — so by the time this\n// footer is reached, any `cd` in the command is already known to be leading and literal. The wording\n// still states the shape, because this is where an agent LEARNS it, before writing the next command.\n// webpieces-disable no-function-outside-class -- sibling of the module-scope runner helpers; the whole file is functions and a lone class here would break its shape\nfunction exemptTreesHint(groups: readonly RuleGroup[], exemptGuards: readonly string[]): string {\n if (exemptGuards.length === 0) return '';\n if (!groups.some((g: RuleGroup): boolean => g.ruleName === 'pr-creation-or-push-guard')) return '';\n\n return `\\n\\nℹ️ Working in a nested repo under one of these exempt trees (${exemptGuards.join(', ')})? `\n + `Put a LITERAL \\`cd\\` at the FRONT of the SAME command — \\`cd /abs/path/to/repo && git push\\` — and git/gh `\n + `run normally there: the webpieces guards do NOT govern them (each is its own repo).`\n + `\\n \\`cd <literal path> && <work>\\` is the ONE shape that moves where a command is judged. A \\`cd\\` `\n + `anywhere else in the line, or a non-literal target like \\`cd \"$DIR\"\\`, is refused outright rather than `\n + `judged from somewhere you did not intend.`;\n}\n\n// The set of CONFIG KEYS 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\n/**\n * Fault Y — a loaded rule whose CONFIG KEY has no entry.\n *\n * Compared on `configKey`, not `name`. Eight of the loaded rules are classes behind two policy keys, so\n * comparing names here would demand entries named `feature-branch-guard`, `pr-merge-guard` and six\n * others — keys the validator then REJECTS as retired. That is the two-enforcement-paths deadlock this\n * repo has already hit once (see the narration in guards.spec.ts), and it blocks every Bash/Write/Edit.\n *\n * De-duplicated on the key too, so one missing policy entry reports ONE paste-ready snippet rather than\n * four copies of the same one under four different headings.\n */\nfunction checkConfigSync(rules: readonly Rule[], config: WebpiecesRulesConfig): BlockedResult | null {\n const configured = configuredRuleNames(config);\n const seen = new Set<string>();\n const unconfiguredRules = rules.filter((r: Rule): boolean => {\n if (configured.has(r.configKey) || seen.has(r.configKey)) return false;\n seen.add(r.configKey);\n return true;\n });\n if (unconfiguredRules.length === 0) return null;\n\n // ONE action, no menu, no escalation — the config-validation invariant (guards/L0-tooling.md): every\n // config problem cures to \"make the file right\", and editing it is never denied. This message used\n // to tell the agent to interview the human about each rule; agents did not do it, so the block just\n // stalled. Each rule now ships a paste-ready entry at its recommended mode.\n //\n // Note this is the CONFIG-BEHIND-CODE direction. The opposite one — the config names a rule the\n // installed validator has no schema for — is unknownRuleError() in rules-config/validate-config.ts\n // and surfaces in the validation banner, not here.\n const lines = [\n // Fault Y's header lives in ./l0-matrix beside the rest of the L0 fault table (same reason as\n // CONFIG_MISSING_REPORT: one place states what this fault is and what cures it).\n CONFIG_OUT_OF_SYNC_HEADER,\n '',\n `Add an entry for each rule below to ${CONFIG_FILENAME}. Editing that file is ALWAYS allowed through`,\n 'the guard — including right now, while this block is up — so paste the entries and retry.',\n '',\n 'Each entry below is ready to paste at its recommended mode; adjust the option values if your',\n 'project needs different ones.',\n '',\n `Do NOT delete a rule from ${CONFIG_FILENAME} to silence it — an entry is REQUIRED for every rule,`,\n 'and \"mode\": \"OFF\" is how a rule is turned off.',\n '',\n ];\n\n for (const rule of unconfiguredRules) {\n lines.push(`--- ${rule.configKey} ---`);\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 // The SAME entry the installer would seed: recommended mode, both hatches, and every other\n // schema-required field — so pasting it satisfies the loader in one pass.\n lines.push(`Entry to add to ${CONFIG_FILENAME}:`);\n lines.push(` \"${rule.configKey}\": ${JSON.stringify(seedEntryForRule(rule.configKey))}`);\n lines.push('');\n }\n\n // Fault Y, stamped for the audit trail — see configMissingBlock for why the producer names it.\n return new BlockedResult(lines.join('\\n'), L0_FAULT_CONFIG_OUT_OF_SYNC);\n}\n\n// N-legs pattern: each rule runs independently so one rule can never abort the others. A rule may\n// EITHER return Violation[] OR throw — both accumulate here into visible violations the AI sees:\n// - a thrown RuleFailError → an expected, well-formed violation (its line/snippet/fixOptions kept);\n// - a thrown plain Error → a \"crashed\" violation (a bug, surfaced not swallowed).\nexport function runRuleCheck(rule: Rule, ctx: EditContext | FileContext | BashContext): readonly Violation[] {\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n return rule.check(ctx);\n } catch (err: unknown) {\n const error = toError(err);\n if (error instanceof RuleFailError) {\n return [violationFromRuleFail(error)];\n }\n return [new Violation(0, '', `Rule '${rule.name}' crashed: ${error.message}`)];\n }\n}\n\n// A thrown RuleFailError carries its own AI-facing message + optional location and cures. Fold the\n// cures into the message because Violation has no fixHint field (RuleGroup's fixHint comes from the\n// rule definition, not a per-throw value). The \"Fix Option N:\"/\"(preferred)\" labels come from the ONE\n// framework-owned renderer, exactly as report.ts renders a rule's static FixHint — a rule never\n// hand-numbers its own cures.\nfunction violationFromRuleFail(error: RuleFailError): Violation {\n return new Violation(error.line ?? 0, error.snippet ?? '', renderRuleFailForAi(error));\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, LoadedConfig, WebpiecesRulesConfig, isHookGuard, HomeConfigService, RepoRootFinder, seedEntryForRule, CONFIG_FILENAME, renderRuleFailForAi } from '@webpieces/rules-config';\n\nimport { buildContexts, buildBashContext } from './build-context';\nimport { DeleteScopedRules } from './delete-scoped-rules';\nimport { VersionSyncGuard } from './version-sync';\nimport { EffectiveTree, EffectiveTreeResolver } from './effective-tree';\nimport { GovernedPath, TargetTreeResolver } from './target-tree';\nimport { bashGovernedPath, filterByExcludedPaths } from './excluded-paths';\nimport { ExcludedPathEscapeHint } from './excluded-path-escape';\nimport { gitFromSubdirBlock } from './force-to-root';\nimport { loadRules, loadMatchRules, loadKeylessBashRules, globMatches, GuardHintCommands } from './load-rules';\nimport { missingDirectoryBlock } from './missing-directory';\nimport { MatchRule } from './rules/match-rule';\nimport { branchStateHangTimeout, maybeRefreshMainSync } from './main-sync-timeout';\nimport { logGuardDecision, logL1Decision, GuardDecision, branchForLog, MatrixRef, Verdict, MATRIX_L0_ALLOW, MATRIX_L2_UNROWED } from './decision-log';\nimport { toError } from './to-error';\nimport { formatReport, READ_SUBJECT, BASH_SUBJECT } from './report';\nimport { ReadOnlyInspectionScan } from './read-only-inspection';\nimport { L0_CURE_ALLOW_JS, isAllowed, isRootManifest } from '../bin/shim';\nimport { AiType } from './agent-event';\nimport { L0_FAULT_CONFIG_MISSING, L0_FAULT_CONFIG_OUT_OF_SYNC, L0_FAULT_NONE } from './l0-fault-codes';\nimport { CONFIG_MISSING_REPORT, CONFIG_OUT_OF_SYNC_HEADER, writeGuardMatrixDoc, guardMatrixPointer } from './l0-matrix';\nimport { L1Classification, firstMatchingL1Row, L1_PRESTAGE_ROW } from './l1-rows';\nimport { withLocationMatrixPointer } from './l1-matrix-doc';\nimport {\n ToolKind, NormalizedToolInput, BlockedResult, HookMode,\n Rule, Violation, RuleGroup, RuleFailError, InformAiError,\n EditContext, FileContext, BashContext,\n} from './types';\n\n// Restrict loaded rules to the category this hook invocation runs. The two split hooks each pass a\n// disjoint category ('rules' = code-style, 'guards' = the hookGuards section); 'all' runs both (the\n// openclaw plugin adapter, a single before_tool_call hook). isHookGuard is the shared classifier in\n// @webpieces/rules-config.\n// isHookGuard is asked about the rule's CONFIG KEY, never its name. Since the collapse those differ\n// for every class behind a policy key — `feature-branch-guard` is a rule NAME whose key is\n// `branch-state-guard` — and asking about the name would classify all eight collapsed guards as\n// code-style rules, i.e. run them in the wrong hook and never in the guards hook at all.\nfunction filterByMode(rules: readonly Rule[], mode: HookMode): readonly Rule[] {\n if (mode === 'all') return rules;\n if (mode === 'guards') return rules.filter((r: Rule): boolean => isHookGuard(r.configKey));\n return rules.filter((r: Rule): boolean => !isHookGuard(r.configKey));\n}\n\n// The cwd a command actually runs from, after its own leading `cd`/`pushd` run. Thin delegate kept\n// for the callers (and specs) that only need the directory; the full tree classification — primary\n// clone vs linked worktree vs nested clone vs outside any repo — is EffectiveTreeResolver.resolve().\n// webpieces-disable no-function-outside-class -- sibling of the module-scope runner helpers; the whole file is functions and a lone class here would break its shape\nexport function effectiveBashCwd(command: string, cwd: string): string {\n return new EffectiveTreeResolver().effectiveCwd(command, cwd);\n}\n\n// The resolved gated-command strings the PR-lifecycle guards print, straight off the loaded\n// `commands.guardHints`. Handed to the rules at construction rather than injected into their config\n// entries under guard-name literals — the injection this replaces could miss a rename silently.\n// webpieces-disable no-function-outside-class -- sibling of the module-scope runner helpers; the whole file is functions and a lone class here would break its shape\nfunction guardHintsOf(loaded: LoadedConfig): GuardHintCommands {\n return new GuardHintCommands(loaded.commands.upsertPr, loaded.commands.mergeComplete);\n}\n\n// A git or gh invocation anywhere in the command (start, or after a ;/&&/|| separator or pipe).\nconst GIT_OR_GH_RE = /(?:^|[;&|]\\s*)(?:git|gh)\\b/;\nexport function isGitOrGhCommand(command: string): boolean {\n return GIT_OR_GH_RE.test(command);\n}\n\n// Fault C (webpieces.config.json missing) — the deny text lives in ./l0-matrix beside the rest of the\n// L0 fault table, so the message and the allowlist can never prescribe different cures. `cwd` is used\n// only to drop the matrix doc where the AI can read it (the config root does not exist yet here).\n//\n// THE ALLOWLIST ESCAPE IS THE CALLER'S (moved out 2026-09-01): behind an optional `command` it could\n// not see the harness, so the gated half of the list was unreachable under C while\n// CONFIG_MISSING_REPORT promised it. See l0FaultAllows. This has one job now: build the block.\n// webpieces-disable no-function-outside-class -- sibling of the module-scope runner helpers; the whole file is functions and a lone class here would break its shape\nfunction configMissingBlock(cwd: string): BlockedResult {\n const root = new RepoRootFinder().resolveRepoRoot(cwd);\n // Stamped with its L0 letter so the block is greppable as fault C wherever it is recorded — the\n // adapter carries it to the audit line rather than re-deriving it from the report text.\n return new BlockedResult(CONFIG_MISSING_REPORT + guardMatrixPointer(writeGuardMatrixDoc(root)), L0_FAULT_CONFIG_MISSING);\n}\n\nexport function run(\n toolKind: ToolKind,\n input: NormalizedToolInput,\n cwd: string,\n mode: HookMode = 'all',\n): BlockedResult | null {\n return runInternal(toolKind, input, cwd, mode);\n}\n\nfunction runInternal(\n toolKind: ToolKind,\n input: NormalizedToolInput,\n cwd: string,\n mode: HookMode,\n): BlockedResult | null {\n const loaded = loadAndValidate(cwd);\n if (loaded.configPath === null) return configMissingBlock(cwd);\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 // …and the same PASS for the machine-local `~/.webpieces/config.json` (a bad key there fails a `wp-*`\n // command with an instruction to edit it, so blocking the edit would wedge the agent inside the\n // failure) and for the two ROOT MANIFESTS a pin lives in — L1 row 8 PROMISED that edit and its cure IS\n // it. isRootManifest, never a basename: this returns EARLY, so basename would unrule every\n // package.json in the repo. It admits any tree's root (the config sits beside it), which is what a\n // worktree's own cure needs — the same tree `workspaceRoot` above already names.\n if (isRootManifest(input.filePath) || new HomeConfigService().isHomeConfigPath(input.filePath)) {\n return null;\n }\n\n // Built-in/custom rules PLUS the client-authored match-rules (content guards). Match-rules run only\n // in the file-edit path (they are code-style, so filterByMode keeps them out of the bash/guards path).\n const allRules = [...loadRules(loaded.rulesConfig, workspaceRoot, guardHintsOf(loaded)), ...loadMatchRules(loaded.matchRules)];\n const modeRules = filterByMode(allRules, mode);\n if (modeRules.length === 0) return null;\n\n // Suppress enforcement for files under this category's excludePaths (e.g. vendored repos under\n // repositories/**). Exclusion is all-or-nothing per category, so an excluded file drops the whole\n // rule set and is fully hands-off — no violations AND no config-sync nag on those files.\n const governed = new TargetTreeResolver().governedPath(input.filePath, workspaceRoot);\n const relativePath = governed.relativePath;\n const rules = new DeleteScopedRules().narrow(toolKind, filterByExcludedPaths(modeRules, governed, loaded.excludePaths));\n if (rules.length === 0) return null;\n\n // Config-sync applies only to built-in/custom rules; match-rules have their own validated section\n // (loadAndValidate already rejected an invalid `match-rules`), so they must not trip the sync nag.\n const outOfSync = checkConfigSync(rules.filter((r: Rule) => !(r instanceof MatchRule)), loaded.rulesConfig);\n if (outOfSync) return outOfSync;\n\n const contexts = buildContexts(toolKind, input, workspaceRoot, governed);\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\n// SINGLETON, deliberately: WebpiecesVersions memoizes per root, and the same two roots are asked for on\n// every Bash call. A fresh instance per call would spawn `git worktree list` and re-read two manifests\n// on the hook's blocking path each time.\nconst VERSION_SYNC = new VersionSyncGuard();\n\n// There is deliberately NO agent parameter. It existed only for CoordinatorWorktreeGuard, which asked\n// WHO was calling; that guard is gone and its replacement asks WHICH TREE the command acts on. Agent\n// identity was measured untrustworthy for that question anyway — a worktree-isolated agent auto-reaped\n// at a turn boundary silently resumes with its cwd on the primary clone — so a guard must never infer\n// a tree from who is asking.\n// webpieces-disable no-function-outside-class -- sibling of run()/runRead() in this module; the whole runner is module-scope functions and a lone class for this one entry point would break the file's shape\nexport function runBash(command: string, cwd: string, mode: HookMode, aiType: AiType): BlockedResult | null {\n return runBashInternal(command, cwd, mode, aiType);\n}\n\n// The name of the ONLY rule permitted to block a Read. Reads are the highest-blast-radius tool\n// there is, so this path is an explicit single-rule allowlist rather than the general rule loop.\nconst READ_SCOPED_GUARDS: ReadonlySet<string> = new Set(['read-stale-guard']);\n\n/**\n * The Read path. Deliberately NOT `run()`:\n *\n * - NO config-sync check. A rule present in code but missing from webpieces.config.json blocks\n * every Write/Edit/Bash by design — but applying that to Read would mean an upgrade that adds\n * any new rule instantly blocks the agent from reading the very config file it must edit to fix\n * it. Reads must never carry that failure mode.\n * - NO general rule loop. Only READ_SCOPED_GUARDS run, so no code-style rule can ever see a Read.\n * - Fails OPEN everywhere, including on a thrown rule (the caller catches and allows).\n *\n * Returns null (allow) unless the one guard fires.\n */\n// webpieces-disable no-function-outside-class -- sibling of run()/runBash() in this module; the whole runner is module-scope functions and a lone class for this one entry point would break the file's shape\nexport function runRead(filePath: string, cwd: string, mode: HookMode = 'all'): BlockedResult | null {\n // Code-style mode has nothing to say about a read.\n if (mode === 'rules') return null;\n\n const loaded = loadAndValidate(cwd);\n // No config → nothing to enforce. Unlike the edit path we do NOT block: an unconfigured repo\n // must still be readable.\n if (loaded.configPath === null) return null;\n\n const workspaceRoot = path.dirname(loaded.configPath);\n\n // Same git-repo-boundary governance as bash, through the SAME resolver: a read inside a different\n // clone is out of scope. (No command to parse here, so the shell cwd IS the effective cwd.)\n if (new EffectiveTreeResolver().resolve('', cwd, workspaceRoot).kind === 'foreign') return null;\n\n const governed = new TargetTreeResolver().governedPath(filePath, workspaceRoot);\n const relativePath = governed.relativePath;\n const all = loadRules(loaded.rulesConfig, workspaceRoot, guardHintsOf(loaded));\n const rules = filterByExcludedPaths(\n all.filter((r: Rule): boolean => READ_SCOPED_GUARDS.has(r.name)),\n governed,\n loaded.excludePaths,\n );\n if (rules.length === 0) return null;\n\n const ctx = new FileContext('Read', filePath, relativePath, workspaceRoot, 0, 0, 0, 0);\n const groups = runFileRules(rules, ctx);\n if (groups.length === 0) return null;\n\n return new BlockedResult(formatReport(relativePath, groups, READ_SUBJECT));\n}\n\n// L0 cure bypass — every command on THE L0 allowlist passes here, ahead of any config load. A\n// webpieces.config.json that is ahead of the installed validator (new rule tokens the published\n// binary doesn't know yet) makes loadAndValidate() throw and would deny `pnpm install` — the very\n// command that updates the validator (deadlock).\n//\n// This used to test INSTALLER_ALLOW_JS alone, which made the config faults (C = config missing,\n// Y = config out of sync) accept a bare `pnpm install` while denying `rm -rf node_modules && pnpm\n// install` — the one cure that works when node_modules is CORRUPT rather than merely stale. Same\n// intent, opposite verdict, for no reason anyone recorded. Each alternative is still anchored at both\n// ends, so `pnpm install && rm -rf /` still falls to the guards.\n//\n// It tests the CURE subset (L0_CURE_ALLOW_JS), not the whole list, and the distinction matters here\n// and nowhere else: this call site runs BEFORE any fault has been established, so whatever it accepts\n// is waved past the L1 guards on a perfectly healthy repo too. A repair command has to be waved past —\n// it runs before the config can be loaded at all. Read-only ORIENTATION does not: it fixes nothing, and\n// letting `git status` through here would delete force-to-root as a side effect of adding `pwd`. Under\n// an actual fault, l0FaultAllows() below consults the WHOLE list.\n// webpieces-disable no-function-outside-class -- sibling of the module-scope runner helpers; the whole file is functions and a lone class here would break its shape\nfunction isL0CureCommand(command: string): boolean {\n return L0_CURE_ALLOW_JS.test(command.trim());\n}\n\n// The FULL L0 list, asked only where this file has actually detected an L0 fault (C = config missing,\n// Y = config out of sync). D/X/U/K are decided in the shim, which greps the same union.\n// IT ASKS `isAllowed()`, not a regex: that is the ONE question both halves of L0 ask, and the only\n// place harness-gated entries are consulted. Testing `L0_ALLOW_JS` directly (which is what this did)\n// dropped the Codex read under C and Y while D/X/U/K honoured it — a per-fault carve-out again.\n// webpieces-disable no-function-outside-class -- sibling of the module-scope runner helpers; the whole file is functions and a lone class here would break its shape\nfunction l0FaultAllows(command: string, aiType: AiType): boolean {\n return isAllowed('Bash', command.trim(), '', aiType) !== null;\n}\n\n// The L0 cure bypass's audit line. Anchored at the repo root that owns `.webpieces` — RepoRootFinder\n// (config-walk-up first, then git toplevel) is the authority for that, and it is correct in a linked\n// worktree because each worktree checks out its own webpieces.config.json. This runs BEFORE\n// loadAndValidate, which is why it resolves the root itself rather than using workspaceRoot.\n// webpieces-disable no-function-outside-class -- sibling of the module-scope runner helpers; the whole file is functions and a lone class here would break its shape\nfunction logL0CureBypass(command: string, cwd: string): void {\n const root = new RepoRootFinder().resolveRepoRoot(cwd);\n logGuardDecision(root, new GuardDecision('-', 'Bash', command, branchForLog(root), 'ALLOW', 'L0 cure bypass (always allowed)', '-', L0_FAULT_NONE, MATRIX_L0_ALLOW));\n}\n\n/**\n * Load the config for the bash path — but do NOT let an unloadable config trap the tools needed to\n * repair it.\n *\n * loadAndValidate throws an InformAiError when webpieces.config.json is unparseable (a real syntax\n * error, or leftover `<<<<<<< HEAD` markers mid-merge) or fails validation. That throw propagates to\n * the hook adapter, which fails CLOSED and denies the command — correct for work, since a config that\n * did not load means no guards ran. But it denied `cat`/`grep`/`sed -n` on webpieces.config.json too,\n * i.e. it blocked the only way to see the problem it was reporting. Observed live, twice.\n *\n * So: on a load failure, a provably-inert INSPECTION command is allowed through (returns null, \"no\n * block\"), matching the escape hatch every other layer already grants this file. Everything else —\n * every write, every git/gh command, every build — still hits the same hard failure as before. The\n * bypass cannot be widened by accident; see ReadOnlyInspectionScan for how narrow \"inert\" is.\n *\n * Returns the loaded config, or null meaning \"allow this command without guards\".\n */\n// webpieces-disable no-function-outside-class -- sibling of the module-scope runner helpers; the whole file is functions and a lone class here would break its shape\nfunction loadConfigOrAllowInspection(command: string, cwd: string): LoadedConfig | null {\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions -- rethrown unchanged unless the command is provably inert\n try {\n return loadAndValidate(cwd);\n } catch (err: unknown) {\n const error = toError(err);\n if (error instanceof InformAiError && new ReadOnlyInspectionScan().isReadOnlyInspection(command)) {\n return null;\n }\n throw error;\n }\n}\n\n// Version skew: this worktree pins a different @webpieces than the MAIN tree that governs it. The guard\n// hooks are absolute, so the main tree's binary judges every tree — which is fine until the two trees\n// disagree about which release that should be. L1 row 8; guards/L1-location.md carries the table.\n// webpieces-disable no-function-outside-class -- sibling of the other module-scope runner helpers; the whole file is functions and a lone class here would break its shape\nfunction versionSkewBlock(command: string, tree: EffectiveTree): BlockedResult | null {\n const report = VERSION_SYNC.block(command, tree);\n return report === null ? null : new BlockedResult(report);\n}\n\n// Where the command lands in L1's five dimensions (K/A/R/G/P). The ONE place the runner's view of the\n// world is translated into the matrix's vocabulary — see L1Classification.forEnforcement for why\n// TreeKind 'outside' currently classifies as `p`.\n// webpieces-disable no-function-outside-class -- sibling of the other module-scope runner helpers; the whole file is functions and a lone class here would break its shape\nfunction l1Classify(command: string, tree: EffectiveTree): L1Classification {\n return L1Classification.forEnforcement(\n tree.kind,\n VERSION_SYNC.skewed(tree),\n new ReadOnlyInspectionScan().isReadOnlyInspection(command),\n isGitOrGhCommand(command),\n path.resolve(tree.effectiveCwd) === path.resolve(tree.root),\n );\n}\n\n// The structural L1 blocks, in order: the misplaced-`cd` PRE-STAGE, then version skew (row 8), then\n// force-to-root (row 5). None is a configurable rule — they are decided from the command text, the\n// resolved tree and the caller, so they run as one step here rather than as three near-identical\n// stanzas in runBashInternal.\n//\n// For the two TREE-BASED blocks the ORDER and the CHOICE are not written here: they come from L1_ROWS\n// (l1-rows.ts), the same array guards/L1-location.md is rendered from. Classify, take the first\n// matching row, dispatch on its blockId — so a row deleted from the array is a block that stops firing,\n// and the doc cannot describe a table the guard does not consult. The two report builders below still\n// own their own predicates and their deny strings; l1-matrix.spec.ts asserts the row lookup and those\n// predicates agree.\n//\n// misplacedCdBlock is deliberately OUTSIDE that lookup, and runs ahead of it, for the reason its own\n// docblock gives: it decides from command TEXT, before a tree has been resolved, and the other two\n// reason FROM the resolved tree. Classifying it would mean asking L1_ROWS a question whose answer the\n// classification itself depends on. It is the same shape as L2's `bareCheckoutOfMain` pre-stage. It is\n// therefore ROW 0 — \"pre-stage, decided from command text\" — rather than a row among 1-6, which keeps\n// it IN the table (the drift this table exists to prevent) without pretending it is classified over the\n// same five dimensions. `L1_PRESTAGE_ROW` is the number the doc prints and `row=` logs, so the two join.\n//\n// THIS FUNCTION IS THE ONE PLACE L1 REPORTS. It is the only scope holding the classification, the\n// matched row, the resolved tree and the agent at once — so logging here, BEFORE the early return,\n// is what finally records the outcomes that were previously invisible: the exempt row and the three\n// hand-downs wrote nothing at all, which is why \"L1 had no objection\" could not be observed and\n// \"show me every L1 decision\" had no answer. The three block helpers no longer log for themselves —\n// and, for the same reason, none of them knows about the matrix POINTER either: every deny is stamped\n// with it HERE, from the same scope and the same row number, so deny and log line cannot disagree.\n// webpieces-disable no-function-outside-class -- sibling of the other module-scope runner helpers; the whole file is functions and a lone class here would break its shape\nfunction l1LocationBlock(command: string, tree: EffectiveTree): BlockedResult | null {\n const misplacedCd = misplacedCdBlock(command, tree);\n if (misplacedCd !== null) {\n logL1(tree, command, 'BLOCK_AI_CURE', L1_PRESTAGE_ROW, 'cd-must-be-first', 'cd not leading/literal');\n return withLocationMatrixPointer(misplacedCd, tree.root, L1_PRESTAGE_ROW);\n }\n\n const row = firstMatchingL1Row(l1Classify(command, tree));\n const rowNum = String(row.num);\n if (row.blockId === null) {\n // ALLOW_EXEMPT stops here; ALLOW means \"no objection, handed down to L2\". Recording the\n // difference is the point — see Verdict. Neither claims the call RAN: the other parallel hook\n // may still have denied it.\n logL1(tree, command, row.action.kind === 'exempt' ? 'ALLOW_EXEMPT' : 'ALLOW', rowNum, '-', row.why);\n return null;\n }\n logL1(tree, command, 'BLOCK_AI_CURE', rowNum, row.blockId, row.why);\n if (row.blockId === 'missing-directory') return withLocationMatrixPointer(missingDirectoryBlock(command, tree), tree.root, rowNum);\n if (row.blockId === 'trinary-version-skew') return withLocationMatrixPointer(versionSkewBlock(command, tree), tree.root, rowNum);\n return withLocationMatrixPointer(gitFromSubdirBlock(command, tree, isGitOrGhCommand(command)), tree.root, rowNum);\n}\n\n// One L1 line, into L1's OWN stream. `row` is the number the generated doc prints, so a reader who sees\n// `row=5` can open the L1 matrix at row 5 — the deny for that same call names it by absolute path.\n// eslint-disable-next-line @typescript-eslint/max-params -- the six fields of one log line, and a class here would break this file's shape\n// webpieces-disable no-function-outside-class -- sibling of the module-scope runner helpers; the whole file is functions\nfunction logL1(tree: EffectiveTree, command: string, verdict: Verdict, row: string, rule: string, why: string): void {\n logL1Decision(tree.root, new GuardDecision(\n rule, 'Bash', command, branchForLog(tree.root), verdict, why, '-', L0_FAULT_NONE, new MatrixRef('L1', row),\n ));\n}\n\n/**\n * ONE legal shape for relocating a command: `cd <literal path> && <work>`. Anything else — a `cd` after\n * another command or after a `VAR=…` assignment, or a `cd \"$DIR\"` the guard cannot expand — is refused\n * here instead of being silently judged from the shell cwd.\n *\n * This changes no verdict: every command it refuses was ALREADY judged from the shell cwd, so nothing\n * it blocks was previously being allowed on the strength of its `cd`. What changes is that the agent\n * finds out. Two PRs of increasingly precise near-miss wording (#596, #597) still left the fact in a\n * paragraph appended to an unrelated block; the rule is simpler stated as a rule.\n *\n * FIRST in the L1 chain deliberately. version skew and force-to-root both reason from the\n * resolved tree, and if the `cd` did not resolve, that tree is not the one the agent thinks they are\n * in — so their remedies would be steering from a location the command does not actually run in.\n */\n// webpieces-disable no-function-outside-class -- sibling of the other module-scope runner helpers; the whole runner is module-scope functions\nfunction misplacedCdBlock(command: string, tree: EffectiveTree): BlockedResult | null {\n const reason = new EffectiveTreeResolver().misplacedCd(command);\n if (reason === null) return null;\n const report =\n `❌ A \\`cd\\` must come FIRST in the command, with a LITERAL path.\\n` +\n ` Why this one was rejected: ${reason}.\\n` +\n ` \\`cd <literal path> && <work>\\` is the ONE shape that moves where webpieces judges a command.\\n` +\n ` Any other \\`cd\\` cannot be resolved, so the command would be judged against ${tree.root}\\n` +\n ` — not where you think it runs. That used to happen silently; this block is that silence, spoken.\\n` +\n ` Fix Option 1: put the \\`cd\\` first, literal path, SAME command: cd /abs/path && <the rest>\\n` +\n ` Fix Option 2: drop the \\`cd\\` — ${tree.root} is where the command is judged anyway.\\n` +\n ` Fix Option 3: if the SECOND \\`cd\\` was there to point a tool at a directory, use that tool's own\\n` +\n ` directory flag instead — \\`git -C <dir>\\`, \\`tar -C <dir>\\`, \\`npm --prefix <dir>\\`,\\n` +\n ` \\`npm pack --pack-destination <dir>\\`, \\`pnpm --dir <dir>\\`. One \\`cd\\`, same result.\\n` +\n ` ONLY for a dir INSIDE this tree. \\`git -C <another tree>\\` is REFUSED to a subagent,\\n` +\n ` and is never the cure for a version skew — for that, tell the MAIN agent.\\n` +\n ` Fix Option 4: split it — run the work in one call, the \\`cd\\` in another (a \\`cd\\` alone still\\n` +\n ` moves nothing the guards judge; see EffectiveTree on why cwd cannot be assumed).`;\n return new BlockedResult(report);\n}\n\n/**\n * The KEYLESS bash guards, deliberately kept OUT of the config-driven rule set. They have no\n * webpieces.config.json entry at all, so passing them through checkConfigSync would make every\n * consumer's next Bash call a fault-Y block (\"this rule has no entry\") for a feature nobody opted into\n * — which is exactly what whole-repo-build-guard did on its first release. Each one decides for itself\n * whether it acts: whole-repo-build-guard reads ~/.webpieces/config.json for a machine-local OPT-IN\n * (absent means OFF); commit-message-substitution-guard and build-output-pipe-guard act unconditionally.\n * They still honour excludePaths, and they do not run in `rules` mode (code-style-only hook).\n */\n// webpieces-disable no-function-outside-class -- sibling of the module-scope runner helpers; the whole file is functions and a lone class here would break its shape\nfunction keylessBashRules(loaded: LoadedConfig, mode: HookMode, governed: GovernedPath): readonly Rule[] {\n if (mode === 'rules') return [];\n return filterByExcludedPaths(\n loadKeylessBashRules(loaded.prGate.buildCommand), governed, loaded.excludePaths,\n );\n}\n\n// webpieces-disable no-function-outside-class -- sibling of run()/runBash() in this module; the whole runner is module-scope functions and a lone class for this one entry point would break the file's shape\nfunction runBashInternal(command: string, cwd: string, mode: HookMode, aiType: AiType): BlockedResult | null {\n if (isL0CureCommand(command)) {\n logL0CureBypass(command, cwd);\n return null;\n }\n\n const loaded = loadConfigOrAllowInspection(command, cwd);\n // null = the config would not load AND this command only inspects → allow, see the helper.\n if (loaded === null) return null;\n if (loaded.configPath === null) return l0FaultAllows(command, aiType) ? null : configMissingBlock(cwd);\n\n const workspaceRoot = path.dirname(loaded.configPath);\n\n // WHICH TREE does this command act on? Not the shell's cwd: a `cd` OUT of the workspace is reset\n // by the harness and one INSIDE it persists, so neither can be assumed (see EffectiveTree).\n // ONE resolver answers this for the guards AND for force-to-root below, so the two can never\n // disagree about which tree you are in.\n const tree = new EffectiveTreeResolver().resolve(command, cwd, workspaceRoot);\n\n // Git-repo-boundary governance: the command runs inside a DIFFERENT git repo than this\n // webpieces.config governs (e.g. a clone under repositories/). Out of scope → allow, hands-off.\n // Intentional, not a silent hole. A LINKED WORKTREE of this repo is deliberately NOT foreign — it\n // is the same project, so the guards run against THAT tree's branch and cache.\n if (tree.kind === 'foreign') {\n logGuardDecision(workspaceRoot, new GuardDecision('-', 'Bash', command, branchForLog(workspaceRoot), 'ALLOW_EXEMPT', 'foreign git repo (out of scope)', '-', L0_FAULT_NONE, new MatrixRef('L1', '1')));\n return null;\n }\n\n // Honour excludePaths on the bash path too (not just Read/Edit): a command whose effective\n // cwd sits under an excluded tree (e.g. repositories/**) drops the whole guard set — matching how\n // runInternal/runRead treat file paths. The relative path is '' when there is no `cd` (root), which\n // matches no exclusion glob, so a plain command at the repo root is unaffected.\n const governedCwd = bashGovernedPath(tree, workspaceRoot);\n const rules = filterByExcludedPaths(\n filterByMode(loadRules(loaded.rulesConfig, workspaceRoot, guardHintsOf(loaded)), mode), governedCwd, loaded.excludePaths,\n );\n const keyless = keylessBashRules(loaded, mode, governedCwd);\n if (rules.length === 0 && keyless.length === 0) return null;\n\n const outOfSync = checkConfigSync(rules, loaded.rulesConfig); // fault Y — L0 list wins, as under C\n if (outOfSync) return l0FaultAllows(command, aiType) ? null : outOfSync;\n\n const locationBlock = l1LocationBlock(command, tree);\n if (locationBlock) return locationBlock;\n\n // Keep the feature-branch-guard cache warm on EVERY command (not just Write/Edit): the AI runs\n // far more bash than edits, so refreshing here means the guard's next file-edit check reads a\n // fresh status. Detached + fire-and-forget — never blocks the command. Only when the guard is\n // loaded (guards/all mode) and enabled, so a project that opted out never triggers git fetches.\n // Keyed on the JUDGED tree, so a worktree's cache is refreshed rather than the primary clone's.\n maybeRefreshMainSync(rules, tree.root, branchStateHangTimeout(loaded.rulesConfig));\n\n const groups = runBashRules([...rules, ...keyless], buildBashContext(command, tree));\n if (groups.length === 0) {\n // Record the ALLOW only for git/gh commands — the operations the bash guards actually reason\n // about (branch create, commit, push, merge, PR). Skipping ls/cat/grep keeps the audit log\n // focused (the whole point of the log is \"why did/didn't a guard fire?\"). Blocks are always\n // logged below.\n if (/\\b(?:git|gh)\\b/.test(command)) {\n logGuardDecision(tree.root, new GuardDecision('-', 'Bash', command, branchForLog(tree.root), 'ALLOW', 'no bash-guard block', '-', L0_FAULT_NONE, MATRIX_L2_UNROWED));\n }\n return null;\n }\n\n const ruleNames = groups.map((g: RuleGroup): string => g.ruleName).join(',');\n logGuardDecision(tree.root, new GuardDecision(ruleNames, 'Bash', command, branchForLog(tree.root), 'BLOCK_AI_CURE', 'bash-guard block', '-', L0_FAULT_NONE, MATRIX_L2_UNROWED));\n const report = new ExcludedPathEscapeHint(workspaceRoot, tree.effectiveCwd).render(command, loaded.excludePaths)\n + formatReport(commandLabel(command), groups, BASH_SUBJECT) + exemptTreesHint(groups, loaded.excludePaths.paths);\n return new BlockedResult(report);\n}\n\n// The bash report's subject line. It used to be the literal string `<bash>`, which told the agent\n// nothing; the command itself is what was blocked, so name it — truncated, because a heredoc-bearing\n// command can run to thousands of characters and the violation lines already carry the detail.\n// webpieces-disable no-function-outside-class -- sibling of the module-scope runner helpers; the whole file is functions and a lone class here would break its shape\nfunction commandLabel(command: string): string {\n const oneLine = command.replace(/\\s+/g, ' ').trim();\n const MAX = 100;\n return oneLine.length <= MAX ? oneLine : oneLine.slice(0, MAX) + '…';\n}\n\n// When a push/PR block fires AND the config exempts vendored/nested trees, surface the escape hatch the\n// AI cannot otherwise discover: git/gh run UNGUARDED inside those trees if it cd's there first (each is\n// governed by its own repo, not this one). Scoped to pr-creation-or-push-guard — for the other guards\n// \"cd into an exempt tree\" is not the remedy — and emitted only when such trees are actually configured,\n// so a repo without exemptions never sees the noise.\n//\n// The hint MUST state the precondition, not just the remedy. \"cd into it first\" is true only for a\n// LITERAL cd at the FRONT of the same command; `D=…; cd \"$D\"; git push` lands in the identical\n// directory and does NOT relocate the verdict. That used to be silent, then a near-miss paragraph\n// appended here; it is now misplacedCdBlock, which refuses the command outright — so by the time this\n// footer is reached, any `cd` in the command is already known to be leading and literal. The wording\n// still states the shape, because this is where an agent LEARNS it, before writing the next command.\n// webpieces-disable no-function-outside-class -- sibling of the module-scope runner helpers; the whole file is functions and a lone class here would break its shape\nfunction exemptTreesHint(groups: readonly RuleGroup[], exemptGuards: readonly string[]): string {\n if (exemptGuards.length === 0) return '';\n if (!groups.some((g: RuleGroup): boolean => g.ruleName === 'pr-creation-or-push-guard')) return '';\n\n return `\\n\\nℹ️ Working in a nested repo under one of these exempt trees (${exemptGuards.join(', ')})? `\n + `Put a LITERAL \\`cd\\` at the FRONT of the SAME command — \\`cd /abs/path/to/repo && git push\\` — and git/gh `\n + `run normally there: the webpieces guards do NOT govern them (each is its own repo).`\n + `\\n \\`cd <literal path> && <work>\\` is the ONE shape that moves where a command is judged. A \\`cd\\` `\n + `anywhere else in the line, or a non-literal target like \\`cd \"$DIR\"\\`, is refused outright rather than `\n + `judged from somewhere you did not intend.`;\n}\n\n// The set of CONFIG KEYS 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\n/**\n * Fault Y — a loaded rule whose CONFIG KEY has no entry.\n *\n * Compared on `configKey`, not `name`. Eight of the loaded rules are classes behind two policy keys, so\n * comparing names here would demand entries named `feature-branch-guard`, `pr-merge-guard` and six\n * others — keys the validator then REJECTS as retired. That is the two-enforcement-paths deadlock this\n * repo has already hit once (see the narration in guards.spec.ts), and it blocks every Bash/Write/Edit.\n *\n * De-duplicated on the key too, so one missing policy entry reports ONE paste-ready snippet rather than\n * four copies of the same one under four different headings.\n */\nfunction checkConfigSync(rules: readonly Rule[], config: WebpiecesRulesConfig): BlockedResult | null {\n const configured = configuredRuleNames(config);\n const seen = new Set<string>();\n const unconfiguredRules = rules.filter((r: Rule): boolean => {\n if (configured.has(r.configKey) || seen.has(r.configKey)) return false;\n seen.add(r.configKey);\n return true;\n });\n if (unconfiguredRules.length === 0) return null;\n\n // ONE action, no menu, no escalation — the config-validation invariant (guards/L0-tooling.md): every\n // config problem cures to \"make the file right\", and editing it is never denied. This message used\n // to tell the agent to interview the human about each rule; agents did not do it, so the block just\n // stalled. Each rule now ships a paste-ready entry at its recommended mode.\n //\n // Note this is the CONFIG-BEHIND-CODE direction. The opposite one — the config names a rule the\n // installed validator has no schema for — is unknownRuleError() in rules-config/validate-config.ts\n // and surfaces in the validation banner, not here.\n const lines = [\n // Fault Y's header lives in ./l0-matrix beside the rest of the L0 fault table (same reason as\n // CONFIG_MISSING_REPORT: one place states what this fault is and what cures it).\n CONFIG_OUT_OF_SYNC_HEADER,\n '',\n `Add an entry for each rule below to ${CONFIG_FILENAME}. Editing that file is ALWAYS allowed through`,\n 'the guard — including right now, while this block is up — so paste the entries and retry.',\n '',\n 'Each entry below is ready to paste at its recommended mode; adjust the option values if your',\n 'project needs different ones.',\n '',\n `Do NOT delete a rule from ${CONFIG_FILENAME} to silence it — an entry is REQUIRED for every rule,`,\n 'and \"mode\": \"OFF\" is how a rule is turned off.',\n '',\n ];\n\n for (const rule of unconfiguredRules) {\n lines.push(`--- ${rule.configKey} ---`);\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 // The SAME entry the installer would seed: recommended mode, both hatches, and every other\n // schema-required field — so pasting it satisfies the loader in one pass.\n lines.push(`Entry to add to ${CONFIG_FILENAME}:`);\n lines.push(` \"${rule.configKey}\": ${JSON.stringify(seedEntryForRule(rule.configKey))}`);\n lines.push('');\n }\n\n // Fault Y, stamped for the audit trail — see configMissingBlock for why the producer names it.\n return new BlockedResult(lines.join('\\n'), L0_FAULT_CONFIG_OUT_OF_SYNC);\n}\n\n// N-legs pattern: each rule runs independently so one rule can never abort the others. A rule may\n// EITHER return Violation[] OR throw — both accumulate here into visible violations the AI sees:\n// - a thrown RuleFailError → an expected, well-formed violation (its line/snippet/fixOptions kept);\n// - a thrown plain Error → a \"crashed\" violation (a bug, surfaced not swallowed).\nexport function runRuleCheck(rule: Rule, ctx: EditContext | FileContext | BashContext): readonly Violation[] {\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n return rule.check(ctx);\n } catch (err: unknown) {\n const error = toError(err);\n if (error instanceof RuleFailError) {\n return [violationFromRuleFail(error)];\n }\n return [new Violation(0, '', `Rule '${rule.name}' crashed: ${error.message}`)];\n }\n}\n\n// A thrown RuleFailError carries its own AI-facing message + optional location and cures. Fold the\n// cures into the message because Violation has no fixHint field (RuleGroup's fixHint comes from the\n// rule definition, not a per-throw value). The \"Fix Option N:\"/\"(preferred)\" labels come from the ONE\n// framework-owned renderer, exactly as report.ts renders a rule's static FixHint — a rule never\n// hand-numbers its own cures.\nfunction violationFromRuleFail(error: RuleFailError): Violation {\n return new Violation(error.line ?? 0, error.snippet ?? '', renderRuleFailForAi(error));\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"]}
@@ -78,5 +78,7 @@ export declare class TargetTreeResolver {
78
78
  export declare class GovernedPath {
79
79
  readonly relativePath: string;
80
80
  readonly treeRelativePath: string;
81
- constructor(relativePath: string, treeRelativePath: string);
81
+ /** The git tree that owns the target path; policy still belongs to the separately loaded governed root. */
82
+ readonly treeRoot: string;
83
+ constructor(relativePath: string, treeRelativePath: string, treeRoot: string);
82
84
  }
@@ -68,7 +68,7 @@ class TargetTreeResolver {
68
68
  // subtractions are real-against-real. Mixing one resolved side with one unresolved one produces
69
69
  // a `../../../..` climb out of the filesystem — which reads as "outside the workspace" and
70
70
  // would quietly hand every path the wrong verdict on a symlinked checkout.
71
- return new GovernedPath(path.relative(tree.governedRoot, absolute), path.relative(tree.root, absolute));
71
+ return new GovernedPath(path.relative(tree.governedRoot, absolute), path.relative(tree.root, absolute), tree.root);
72
72
  }
73
73
  // The nearest ancestor directory of `targetPath` that EXISTS — the deepest directory git can be
74
74
  // asked about — with symlinks resolved. Bounded by the filesystem root (path.dirname is its own
@@ -136,9 +136,12 @@ exports.TargetTreeResolver = TargetTreeResolver;
136
136
  class GovernedPath {
137
137
  relativePath;
138
138
  treeRelativePath;
139
- constructor(relativePath, treeRelativePath) {
139
+ /** The git tree that owns the target path; policy still belongs to the separately loaded governed root. */
140
+ treeRoot;
141
+ constructor(relativePath, treeRelativePath, treeRoot) {
140
142
  this.relativePath = relativePath;
141
143
  this.treeRelativePath = treeRelativePath;
144
+ this.treeRoot = treeRoot;
142
145
  }
143
146
  }
144
147
  exports.GovernedPath = GovernedPath;
@@ -1 +1 @@
1
- {"version":3,"file":"target-tree.js","sourceRoot":"","sources":["../../../../../../packages/tooling/ai-hook-rules/src/core/target-tree.ts"],"names":[],"mappings":"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","sourcesContent":["import * as fs from 'fs';\nimport * as path from 'path';\n\nimport { EffectiveTree, EffectiveTreeResolver } from './effective-tree';\n\n/**\n * WHICH TREE owns the FILE a Write/Edit/Read is aimed at? The file-path counterpart to\n * EffectiveTreeResolver, which answers the same question for a Bash command's effective cwd.\n *\n * WHY IT HAS TO EXIST (issue #851). The bash half of this question has been answered by git since\n * effective-tree.ts was written — `isLinkedWorktree` is `--git-dir !== --git-common-dir`, and its header\n * states the principle in capitals: *PLACEMENT IS NOT IDENTITY*. The FILE half never consumed that\n * answer. Every file-scoped guard judged `ctx.workspaceRoot`, which is the walk-up from the SESSION's\n * cwd to whichever `webpieces.config.json` governs it — the PRIMARY clone, for a main-session edit into\n * an agent worktree. Because Claude Code checks a worktree out INSIDE the repo at\n * `<repo>/.claude/worktrees/agent-XXXX`, that walk-up answers \"primary\" for a path that is manifestly\n * not the primary's, and nothing downstream re-examined it.\n *\n * Measured live on 2026-09-03: an Edit of one import line inside a worktree on a CLEAN branch was\n * refused, citing `pnpm-lock.yaml` and `pnpm-workspace.yaml` — the PRIMARY clone's branch's conflict\n * files. The main-sync cache is already keyed by branch and already held the correct, non-conflicting\n * entry for the worktree's branch, one key over. Only the lookup key was wrong. So this class exists to\n * make the lookup ask the same authority the bash half asks, rather than to introduce a second one.\n *\n * IT ASKS EffectiveTreeResolver, and adds NOTHING of its own beyond finding a directory to ask about.\n * That is deliberate and it is the whole design: two resolvers WILL disagree about which tree a path is\n * in, and this module exists precisely because two layers already did. There is no `.claude/worktrees`\n * string match here and there must never be one — matching on placement is the bug, not the fix.\n *\n * The only thing worth naming is the walk-up: a Write legitimately names a file, and directories, that\n * do not exist yet, and git cannot be asked about a directory that is not there. So we climb to the\n * nearest EXISTING ancestor and ask about that. A parent directory is in the same worktree as the child\n * it is about to contain — that is a property of the filesystem, not a guess.\n */\nexport class TargetTreeResolver {\n constructor(private readonly trees: EffectiveTreeResolver = new EffectiveTreeResolver()) {}\n\n /**\n * The tree that owns `targetPath`, classified exactly as a Bash command's cwd would be.\n *\n * `governedRoot` is the fallback for every case with no better answer — an empty path, a path with\n * no existing ancestor at all — and is also what `EffectiveTree.governedRoot` keeps meaning: whose\n * config and excludePaths apply. It is NOT the tree to judge; `EffectiveTree.root` is.\n */\n resolve(targetPath: string, governedRoot: string): EffectiveTree {\n // REAL paths on BOTH sides, or git's answers cannot be compared with each other. Identity here\n // is `commonDir(target) === commonDir(governed)`, and git prints an ABSOLUTE, symlink-resolved\n // path from a linked worktree while printing a bare relative `.git` from the primary clone. So\n // on any repo reached through a symlink — `/var/folders/...` on macOS, which is `/private/var`,\n // and every `os.tmpdir()` path under it — the two strings differ for the same `.git` and the\n // repo's OWN worktree classifies as `foreign`, i.e. every guard silently off. Resolving both\n // sides first is what makes the comparison an answer about git rather than about spelling.\n const governed = this.realDir(governedRoot);\n return this.trees.resolve('', this.nearestExistingDir(targetPath, governed), governed);\n }\n\n /**\n * The two relative spellings of one target path, resolved together so a caller cannot accidentally\n * use the wrong one. See GovernedPath for why there are two.\n */\n governedPath(targetPath: string, governedRoot: string): GovernedPath {\n const absolute = this.realAbsolute(targetPath);\n const tree = this.resolve(targetPath, governedRoot);\n // `tree.root` and `tree.governedRoot` are already resolved by resolve() above, so both\n // subtractions are real-against-real. Mixing one resolved side with one unresolved one produces\n // a `../../../..` climb out of the filesystem — which reads as \"outside the workspace\" and\n // would quietly hand every path the wrong verdict on a symlinked checkout.\n return new GovernedPath(\n path.relative(tree.governedRoot, absolute),\n path.relative(tree.root, absolute),\n );\n }\n\n // The nearest ancestor directory of `targetPath` that EXISTS — the deepest directory git can be\n // asked about — with symlinks resolved. Bounded by the filesystem root (path.dirname is its own\n // fixed point at `/`), so the loop terminates without a counter.\n private nearestExistingDir(targetPath: string, governedRoot: string): string {\n if (targetPath === '') return governedRoot;\n let dir = path.dirname(path.resolve(targetPath));\n for (;;) {\n if (fs.existsSync(dir)) return fs.realpathSync(dir);\n const parent = path.dirname(dir);\n if (parent === dir) return governedRoot;\n dir = parent;\n }\n }\n\n /**\n * `targetPath` made absolute AND symlink-resolved, including when it does not exist yet.\n *\n * `fs.realpathSync` throws on a missing path, and a Write's target is missing by definition, so the\n * resolvable HEAD of the path (its nearest existing ancestor) is resolved and the not-yet-existing\n * TAIL is re-appended verbatim. That keeps it comparable with `tree.root`, which is git's answer and\n * always real.\n */\n private realAbsolute(targetPath: string): string {\n const absolute = path.resolve(targetPath);\n const tail: string[] = [];\n let dir = absolute;\n for (;;) {\n if (fs.existsSync(dir)) return path.join(fs.realpathSync(dir), ...tail);\n const parent = path.dirname(dir);\n if (parent === dir) return absolute;\n tail.unshift(path.basename(dir));\n dir = parent;\n }\n }\n\n // A directory made real when it is there, and merely absolute when it is not.\n private realDir(dir: string): string {\n return fs.existsSync(dir) ? fs.realpathSync(dir) : path.resolve(dir);\n }\n}\n\n/**\n * One target path, in the TWO relative spellings the hook path needs — data-only, per CLAUDE.md.\n *\n * They are different questions and conflating them is issue #851's secondary defect:\n *\n * `relativePath` — relative to the GOVERNED root. What `excludePaths`' globs are written against,\n * and what the violation report prints.\n * `treeRelativePath` — relative to the tree that OWNS the file. What `isWebpiecesStateDir` must be\n * asked about, because `.webpieces/` is the tooling's state dir *of a tree*, and\n * a worktree has its own. Governed-root-relative, a reviewer subagent's\n * `<primary>/.claude/worktrees/agent-X/.webpieces/pr-review/<branch>/review.json`\n * reads as `.claude/...` and was NOT exempt — while\n * `<primary>/.webpieces/worktrees/agent-X/pr-review/.../review-1.json`, the same\n * kind of file one directory over, was. `wp-review-upsert-pr` REQUIRES that file\n * before `wp-finish-upsert-pr` will open a PR, so the guard could forbid a file\n * the gate demands.\n *\n * In the primary clone the two strings are identical, which is why the gap was invisible for so long.\n */\nexport class GovernedPath {\n readonly relativePath: string;\n readonly treeRelativePath: string;\n\n constructor(relativePath: string, treeRelativePath: string) {\n this.relativePath = relativePath;\n this.treeRelativePath = treeRelativePath;\n }\n}\n"]}
1
+ {"version":3,"file":"target-tree.js","sourceRoot":"","sources":["../../../../../../packages/tooling/ai-hook-rules/src/core/target-tree.ts"],"names":[],"mappings":"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","sourcesContent":["import * as fs from 'fs';\nimport * as path from 'path';\n\nimport { EffectiveTree, EffectiveTreeResolver } from './effective-tree';\n\n/**\n * WHICH TREE owns the FILE a Write/Edit/Read is aimed at? The file-path counterpart to\n * EffectiveTreeResolver, which answers the same question for a Bash command's effective cwd.\n *\n * WHY IT HAS TO EXIST (issue #851). The bash half of this question has been answered by git since\n * effective-tree.ts was written — `isLinkedWorktree` is `--git-dir !== --git-common-dir`, and its header\n * states the principle in capitals: *PLACEMENT IS NOT IDENTITY*. The FILE half never consumed that\n * answer. Every file-scoped guard judged `ctx.workspaceRoot`, which is the walk-up from the SESSION's\n * cwd to whichever `webpieces.config.json` governs it — the PRIMARY clone, for a main-session edit into\n * an agent worktree. Because Claude Code checks a worktree out INSIDE the repo at\n * `<repo>/.claude/worktrees/agent-XXXX`, that walk-up answers \"primary\" for a path that is manifestly\n * not the primary's, and nothing downstream re-examined it.\n *\n * Measured live on 2026-09-03: an Edit of one import line inside a worktree on a CLEAN branch was\n * refused, citing `pnpm-lock.yaml` and `pnpm-workspace.yaml` — the PRIMARY clone's branch's conflict\n * files. The main-sync cache is already keyed by branch and already held the correct, non-conflicting\n * entry for the worktree's branch, one key over. Only the lookup key was wrong. So this class exists to\n * make the lookup ask the same authority the bash half asks, rather than to introduce a second one.\n *\n * IT ASKS EffectiveTreeResolver, and adds NOTHING of its own beyond finding a directory to ask about.\n * That is deliberate and it is the whole design: two resolvers WILL disagree about which tree a path is\n * in, and this module exists precisely because two layers already did. There is no `.claude/worktrees`\n * string match here and there must never be one — matching on placement is the bug, not the fix.\n *\n * The only thing worth naming is the walk-up: a Write legitimately names a file, and directories, that\n * do not exist yet, and git cannot be asked about a directory that is not there. So we climb to the\n * nearest EXISTING ancestor and ask about that. A parent directory is in the same worktree as the child\n * it is about to contain — that is a property of the filesystem, not a guess.\n */\nexport class TargetTreeResolver {\n constructor(private readonly trees: EffectiveTreeResolver = new EffectiveTreeResolver()) {}\n\n /**\n * The tree that owns `targetPath`, classified exactly as a Bash command's cwd would be.\n *\n * `governedRoot` is the fallback for every case with no better answer — an empty path, a path with\n * no existing ancestor at all — and is also what `EffectiveTree.governedRoot` keeps meaning: whose\n * config and excludePaths apply. It is NOT the tree to judge; `EffectiveTree.root` is.\n */\n resolve(targetPath: string, governedRoot: string): EffectiveTree {\n // REAL paths on BOTH sides, or git's answers cannot be compared with each other. Identity here\n // is `commonDir(target) === commonDir(governed)`, and git prints an ABSOLUTE, symlink-resolved\n // path from a linked worktree while printing a bare relative `.git` from the primary clone. So\n // on any repo reached through a symlink — `/var/folders/...` on macOS, which is `/private/var`,\n // and every `os.tmpdir()` path under it — the two strings differ for the same `.git` and the\n // repo's OWN worktree classifies as `foreign`, i.e. every guard silently off. Resolving both\n // sides first is what makes the comparison an answer about git rather than about spelling.\n const governed = this.realDir(governedRoot);\n return this.trees.resolve('', this.nearestExistingDir(targetPath, governed), governed);\n }\n\n /**\n * The two relative spellings of one target path, resolved together so a caller cannot accidentally\n * use the wrong one. See GovernedPath for why there are two.\n */\n governedPath(targetPath: string, governedRoot: string): GovernedPath {\n const absolute = this.realAbsolute(targetPath);\n const tree = this.resolve(targetPath, governedRoot);\n // `tree.root` and `tree.governedRoot` are already resolved by resolve() above, so both\n // subtractions are real-against-real. Mixing one resolved side with one unresolved one produces\n // a `../../../..` climb out of the filesystem — which reads as \"outside the workspace\" and\n // would quietly hand every path the wrong verdict on a symlinked checkout.\n return new GovernedPath(\n path.relative(tree.governedRoot, absolute),\n path.relative(tree.root, absolute),\n tree.root,\n );\n }\n\n // The nearest ancestor directory of `targetPath` that EXISTS — the deepest directory git can be\n // asked about — with symlinks resolved. Bounded by the filesystem root (path.dirname is its own\n // fixed point at `/`), so the loop terminates without a counter.\n private nearestExistingDir(targetPath: string, governedRoot: string): string {\n if (targetPath === '') return governedRoot;\n let dir = path.dirname(path.resolve(targetPath));\n for (;;) {\n if (fs.existsSync(dir)) return fs.realpathSync(dir);\n const parent = path.dirname(dir);\n if (parent === dir) return governedRoot;\n dir = parent;\n }\n }\n\n /**\n * `targetPath` made absolute AND symlink-resolved, including when it does not exist yet.\n *\n * `fs.realpathSync` throws on a missing path, and a Write's target is missing by definition, so the\n * resolvable HEAD of the path (its nearest existing ancestor) is resolved and the not-yet-existing\n * TAIL is re-appended verbatim. That keeps it comparable with `tree.root`, which is git's answer and\n * always real.\n */\n private realAbsolute(targetPath: string): string {\n const absolute = path.resolve(targetPath);\n const tail: string[] = [];\n let dir = absolute;\n for (;;) {\n if (fs.existsSync(dir)) return path.join(fs.realpathSync(dir), ...tail);\n const parent = path.dirname(dir);\n if (parent === dir) return absolute;\n tail.unshift(path.basename(dir));\n dir = parent;\n }\n }\n\n // A directory made real when it is there, and merely absolute when it is not.\n private realDir(dir: string): string {\n return fs.existsSync(dir) ? fs.realpathSync(dir) : path.resolve(dir);\n }\n}\n\n/**\n * One target path, in the TWO relative spellings the hook path needs — data-only, per CLAUDE.md.\n *\n * They are different questions and conflating them is issue #851's secondary defect:\n *\n * `relativePath` — relative to the GOVERNED root. What `excludePaths`' globs are written against,\n * and what the violation report prints.\n * `treeRelativePath` — relative to the tree that OWNS the file. What `isWebpiecesStateDir` must be\n * asked about, because `.webpieces/` is the tooling's state dir *of a tree*, and\n * a worktree has its own. Governed-root-relative, a reviewer subagent's\n * `<primary>/.claude/worktrees/agent-X/.webpieces/pr-review/<branch>/review.json`\n * reads as `.claude/...` and was NOT exempt — while\n * `<primary>/.webpieces/worktrees/agent-X/pr-review/.../review-1.json`, the same\n * kind of file one directory over, was. `wp-review-upsert-pr` REQUIRES that file\n * before `wp-finish-upsert-pr` will open a PR, so the guard could forbid a file\n * the gate demands.\n *\n * In the primary clone the two strings are identical, which is why the gap was invisible for so long.\n */\nexport class GovernedPath {\n readonly relativePath: string;\n readonly treeRelativePath: string;\n /** The git tree that owns the target path; policy still belongs to the separately loaded governed root. */\n readonly treeRoot: string;\n\n constructor(relativePath: string, treeRelativePath: string, treeRoot: string) {\n this.relativePath = relativePath;\n this.treeRelativePath = treeRelativePath;\n this.treeRoot = treeRoot;\n }\n}\n"]}
@@ -99,12 +99,16 @@ export declare class FileContext {
99
99
  readonly filePath: string;
100
100
  readonly relativePath: string;
101
101
  readonly workspaceRoot: string;
102
+ /** The git tree that owns filePath. workspaceRoot remains the root that owns policy/config. */
103
+ readonly targetRoot: string;
104
+ /** filePath relative to targetRoot, independent of its governed-root spelling. */
105
+ readonly targetRelativePath: string;
102
106
  readonly currentFileLines: number;
103
107
  readonly linesAdded: number;
104
108
  readonly linesRemoved: number;
105
109
  readonly projectedFileLines: number;
106
110
  options: RuleOptions;
107
- constructor(tool: ToolKind, filePath: string, relativePath: string, workspaceRoot: string, currentFileLines: number, linesAdded: number, linesRemoved: number, projectedFileLines: number);
111
+ constructor(tool: ToolKind, filePath: string, relativePath: string, workspaceRoot: string, currentFileLines: number, linesAdded: number, linesRemoved: number, projectedFileLines: number, targetRoot?: string, targetRelativePath?: string);
108
112
  }
109
113
  /**
110
114
  * The shape of a custom rule loaded from a `rulesDir` (a plain object returned by require()).
package/src/core/types.js CHANGED
@@ -162,16 +162,22 @@ class FileContext {
162
162
  filePath;
163
163
  relativePath;
164
164
  workspaceRoot;
165
+ /** The git tree that owns filePath. workspaceRoot remains the root that owns policy/config. */
166
+ targetRoot;
167
+ /** filePath relative to targetRoot, independent of its governed-root spelling. */
168
+ targetRelativePath;
165
169
  currentFileLines;
166
170
  linesAdded;
167
171
  linesRemoved;
168
172
  projectedFileLines;
169
173
  options;
170
- constructor(tool, filePath, relativePath, workspaceRoot, currentFileLines, linesAdded, linesRemoved, projectedFileLines) {
174
+ constructor(tool, filePath, relativePath, workspaceRoot, currentFileLines, linesAdded, linesRemoved, projectedFileLines, targetRoot = workspaceRoot, targetRelativePath = relativePath) {
171
175
  this.tool = tool;
172
176
  this.filePath = filePath;
173
177
  this.relativePath = relativePath;
174
178
  this.workspaceRoot = workspaceRoot;
179
+ this.targetRoot = targetRoot;
180
+ this.targetRelativePath = targetRelativePath;
175
181
  this.currentFileLines = currentFileLines;
176
182
  this.linesAdded = linesAdded;
177
183
  this.linesRemoved = linesRemoved;
@@ -1 +1 @@
1
- {"version":3,"file":"types.js","sourceRoot":"","sources":["../../../../../../packages/tooling/ai-hook-rules/src/core/types.ts"],"names":[],"mappings":"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","sourcesContent":["// ResolvedConfig / ResolvedRuleConfig / RuleOptions now live in @webpieces/rules-config\n// so ai-hooks and the Nx validate-code executor share one loader and one config file.\nimport { RuleOptions } from '@webpieces/rules-config';\nexport { ResolvedConfig, ResolvedRuleConfig, RuleOptions, InformAiError, RuleFailError } from '@webpieces/rules-config';\nimport { FixHint } from './fix-hint';\nimport { L0_FAULT_NONE } from './l0-fault-codes';\n\n// 'Read' is a first-class member because read-stale-guard is a file-scoped guard that runs on the\n// Read fast path. It is deliberately NOT in HANDLED_FILE_TOOLS (adapters/claude-code-adapter.ts), so\n// ClaudeCodeAdapter.kindOf() maps it to the 'Read' routing kind and Read never enters the edit/file\n// rule pipeline — only the one guard that asks for it. Nothing switches exhaustively on this union;\n// it is carried for logging.\n//\n// 'Delete' arrives ONLY from a Codex `apply_patch` carrying a `*** Delete File:` directive — no Claude\n// Code tool produces it. Every existing rule DEFAULTS TO NOT FIRING on it (see DeleteScopedRules in\n// ./delete-scoped-rules.ts): a rule written to judge the bytes an edit ADDS has nothing to say about a file going\n// away, and inventing a verdict for it would be guessing. Rules for which a delete IS meaningful — a\n// barrel export disappearing, a doc that documents a deleted file — opt in there, deliberately.\nexport type ToolKind = 'Write' | 'Edit' | 'MultiEdit' | 'Read' | 'Delete';\nexport type RuleScope = 'edit' | 'file' | 'bash';\n// Which category of built-in rules a hook invocation runs: code-style 'rules', git/PR/branch\n// 'guards' (the hookGuards section), or 'all' (both categories — used by the openclaw plugin adapter,\n// which is a single before_tool_call hook rather than two split PreToolUse hooks).\nexport type HookMode = 'rules' | 'guards' | 'all';\nexport type IsLineDisabled = (lineNum: number, ruleName: string) => boolean;\n\nexport class Violation {\n readonly line: number;\n readonly snippet: string;\n // Optional per-occurrence override for the `→` line. When omitted, the report falls back\n // to the rule's `FixHint.violation`. Dynamic rules (param name, line count, marker path,\n // branch/PR) pass a specific message here; static rules omit it.\n readonly message: string | undefined;\n editIndex: number | undefined;\n editCount: number | undefined;\n\n constructor(line: number, snippet: string, message?: string) {\n this.line = line;\n this.snippet = snippet;\n this.message = message;\n this.editIndex = undefined;\n this.editCount = undefined;\n }\n}\n\nexport class NormalizedEdit {\n readonly oldString: string;\n readonly newString: string;\n\n constructor(oldString: string, newString: string) {\n this.oldString = oldString;\n this.newString = newString;\n }\n}\n\nexport class NormalizedToolInput {\n readonly filePath: string;\n readonly edits: readonly NormalizedEdit[];\n\n constructor(filePath: string, edits: readonly NormalizedEdit[]) {\n this.filePath = filePath;\n this.edits = edits;\n }\n}\n\nexport class NormalizedBashInput {\n readonly command: string;\n\n constructor(command: string) {\n this.command = command;\n }\n}\n\nexport class EditContext {\n readonly tool: ToolKind;\n readonly editIndex: number;\n readonly editCount: number;\n readonly filePath: string;\n readonly relativePath: string;\n readonly workspaceRoot: string;\n readonly addedContent: string;\n readonly strippedContent: string;\n readonly lines: readonly string[];\n readonly strippedLines: readonly string[];\n readonly removedContent: string;\n readonly isLineDisabled: IsLineDisabled;\n options: RuleOptions;\n\n constructor(\n tool: ToolKind,\n editIndex: number,\n editCount: number,\n filePath: string,\n relativePath: string,\n workspaceRoot: string,\n addedContent: string,\n strippedContent: string,\n lines: readonly string[],\n strippedLines: readonly string[],\n removedContent: string,\n isLineDisabled: IsLineDisabled,\n ) {\n this.tool = tool;\n this.editIndex = editIndex;\n this.editCount = editCount;\n this.filePath = filePath;\n this.relativePath = relativePath;\n this.workspaceRoot = workspaceRoot;\n this.addedContent = addedContent;\n this.strippedContent = strippedContent;\n this.lines = lines;\n this.strippedLines = strippedLines;\n this.removedContent = removedContent;\n this.isLineDisabled = isLineDisabled;\n this.options = {};\n }\n}\n\n// A heredoc and its body: `<<EOF` / `<<'EOF'` / `<<-EOF` through the terminator line.\nconst HEREDOC_BODY = /<<-?\\s*(['\"]?)(\\w+)\\1[\\s\\S]*?^\\t*\\2\\s*$/gm;\n\n// A single- or double-quoted span.\nconst QUOTED_SPAN = /'([^']*)'|\"([^\"]*)\"/g;\n\n// Shell syntax that a quoted span is explicitly NOT: separators, pipes, subshells, redirection,\n// expansion. Content carrying any of these is data (a jq filter, a regex, a glob), and unquoting it\n// would hand the segment scanner syntax the shell never sees. See stripProse.\nconst SHELL_METACHARACTER = /[|;&()<>$`]/;\n\nexport class BashContext {\n readonly tool: 'Bash';\n readonly command: string;\n /**\n * `command` with heredoc bodies and prose-in-quotes removed — what a guard should MATCH on, while\n * `command` stays raw for the violation message (a human needs to see what they actually typed).\n *\n * A commit message that merely MENTIONS a blocked command is not that command. This repo's whole\n * subject matter is the git workflow, so its commit messages, docs and the guards' own fix-hint\n * text are full of `git push` / `gh pr …` strings; matching raw text blocks writing about the\n * tooling. A guard that cries wolf is a guard someone eventually turns OFF, which costs far more\n * than the theoretical bypass this opens (`sh -c '<blocked command>'` stops matching). That trade\n * is deliberate: these guards exist to catch a FORGETFUL agent, not an adversarial one.\n *\n * ONLY for blocklist-shaped guards (\"if the command matches X, block\"), where stripping can only\n * ever block LESS. Never use it in an allowlist-shaped guard such as merged-branch-bash-guard,\n * where stripping could turn a permitted recovery command into a blocked one and deadlock the\n * session — there, match on the raw `command`.\n */\n readonly commandCode: string;\n /**\n * The tree this command is JUDGED against — the root of the worktree it actually acts on, which\n * is NOT the shell's cwd whenever the command carries a leading `cd <path> &&` (see\n * EffectiveTreeResolver). Guards read git state, the main-sync cache and the decision log from\n * here, so a command aimed at a linked worktree is judged on that worktree's branch, never the\n * primary clone's.\n */\n readonly workspaceRoot: string;\n /** The directory the command really runs in (post-`cd`). Relative paths resolve against THIS. */\n readonly effectiveCwd: string;\n /** The root owning webpieces.config.json. Equals workspaceRoot except in a linked worktree. */\n readonly governedRoot: string;\n options: RuleOptions;\n\n constructor(command: string, workspaceRoot: string, effectiveCwd?: string, governedRoot?: string) {\n this.tool = 'Bash';\n this.command = command;\n this.commandCode = this.stripProse(command);\n this.workspaceRoot = workspaceRoot;\n this.effectiveCwd = effectiveCwd ?? workspaceRoot;\n this.governedRoot = governedRoot ?? workspaceRoot;\n this.options = {};\n }\n\n /**\n * A quoted span WITHOUT whitespace is kept: `git checkout \"main\"` is a real command with a quoted\n * argument, not prose. One containing whitespace is a sentence, and becomes a single space.\n *\n * ONE exception, and it is a false positive that actually fired: a whitespace-free span carrying\n * SHELL METACHARACTERS is dropped too. Keeping the content strips only the quotes, so a jq filter —\n * `--jq '[.[]|select(.title|test(\"x\"))]|length'` — was handed to the guards as bare shell syntax.\n * Every guard then split it on its `|`, `(` and `;` and saw a segment reading exactly `test`, which\n * whole-repo-build-guard classified as the workspace-wide test script and BLOCKED. There was no\n * build anywhere in that command; it polled npm and GitHub.\n *\n * Quoting is precisely how a shell says \"this is DATA, not syntax\", so honouring the quotes is the\n * correct reading. And because commandCode only ever feeds blocklist-shaped guards, dropping more\n * can only ever block LESS — never turn an allowed command into a refused one.\n */\n private stripProse(command: string): string {\n const withoutHeredocs = command.replace(HEREDOC_BODY, ' ');\n return withoutHeredocs.replace(QUOTED_SPAN, (match: string, single?: string, double?: string): string => {\n const content = single ?? double ?? '';\n if (/\\s/.test(content)) return ' ';\n return SHELL_METACHARACTER.test(content) ? ' ' : content;\n });\n }\n}\n\nexport class FileContext {\n readonly tool: ToolKind;\n readonly filePath: string;\n readonly relativePath: string;\n readonly workspaceRoot: string;\n readonly currentFileLines: number;\n readonly linesAdded: number;\n readonly linesRemoved: number;\n readonly projectedFileLines: number;\n options: RuleOptions;\n\n constructor(\n tool: ToolKind,\n filePath: string,\n relativePath: string,\n workspaceRoot: string,\n currentFileLines: number,\n linesAdded: number,\n linesRemoved: number,\n projectedFileLines: number,\n ) {\n this.tool = tool;\n this.filePath = filePath;\n this.relativePath = relativePath;\n this.workspaceRoot = workspaceRoot;\n this.currentFileLines = currentFileLines;\n this.linesAdded = linesAdded;\n this.linesRemoved = linesRemoved;\n this.projectedFileLines = projectedFileLines;\n this.options = {};\n }\n}\n\n/**\n * The shape of a custom rule loaded from a `rulesDir` (a plain object returned by require()).\n * It carries only metadata + a `check` method — no on/off logic of its own.\n */\nexport interface PlainRule {\n readonly name: string;\n readonly description: string;\n readonly scope: RuleScope;\n readonly files: readonly string[];\n readonly defaultOptions: RuleOptions;\n readonly fixHint: FixHint;\n check(ctx: EditContext | FileContext | BashContext): readonly Violation[];\n}\n\n/**\n * The runtime contract the runner iterates. Both the built-in rule classes (which extend\n * EditRuleBase/FileRuleBase/BashRuleBase) and the custom-rule adapter satisfy this: they add\n * `shouldRun()` (mode + escape-hatch decision) on top of the PlainRule metadata.\n */\nexport interface Rule extends PlainRule {\n /**\n * The webpieces.config.json key whose entry configures this rule — NOT necessarily `name`.\n *\n * `name` is the operator identity (what a decision-log line and a deny report carry); `configKey`\n * is the switch. They differ wherever several classes implement one POLICY: all four branch-state\n * guards read `branch-state-guard`, all four PR-lifecycle guards read `pr-lifecycle-guard`. Every\n * consumer that asks \"which section does this live in?\" (filterByMode) or \"does this have an\n * entry?\" (the fault-Y config-sync check) must read THIS field, because asking for `name` there\n * demands entries under keys the validator rejects as retired.\n */\n readonly configKey: string;\n shouldRun(): boolean;\n}\n\nexport class RuleGroup {\n readonly ruleName: string;\n readonly ruleDescription: string;\n readonly fixHint: FixHint;\n readonly violations: readonly Violation[];\n\n constructor(\n ruleName: string,\n ruleDescription: string,\n fixHint: FixHint,\n violations: readonly Violation[],\n ) {\n this.ruleName = ruleName;\n this.ruleDescription = ruleDescription;\n this.fixHint = fixHint;\n this.violations = violations;\n }\n}\n\nexport class BlockedResult {\n readonly report: string;\n /**\n * The L0 fault this block IS, in the codebook's own letter (see core/l0-fault-codes.ts), or `-` for\n * an ordinary rule block.\n *\n * It rides on the result rather than being re-derived from the report text because the block is\n * decided deep in the runner (fault C in configMissingBlock, fault Y in checkConfigSync) and\n * STAMPED by the adapter at the terminal boundary, several frames up. Scraping the report for a\n * fault would be a second answer to a question the producer already knows.\n */\n readonly fault: string;\n\n constructor(report: string, fault: string = L0_FAULT_NONE) {\n this.report = report;\n this.fault = fault;\n }\n}\n\n"]}
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../../../../../../packages/tooling/ai-hook-rules/src/core/types.ts"],"names":[],"mappings":"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","sourcesContent":["// ResolvedConfig / ResolvedRuleConfig / RuleOptions now live in @webpieces/rules-config\n// so ai-hooks and the Nx validate-code executor share one loader and one config file.\nimport { RuleOptions } from '@webpieces/rules-config';\nexport { ResolvedConfig, ResolvedRuleConfig, RuleOptions, InformAiError, RuleFailError } from '@webpieces/rules-config';\nimport { FixHint } from './fix-hint';\nimport { L0_FAULT_NONE } from './l0-fault-codes';\n\n// 'Read' is a first-class member because read-stale-guard is a file-scoped guard that runs on the\n// Read fast path. It is deliberately NOT in HANDLED_FILE_TOOLS (adapters/claude-code-adapter.ts), so\n// ClaudeCodeAdapter.kindOf() maps it to the 'Read' routing kind and Read never enters the edit/file\n// rule pipeline — only the one guard that asks for it. Nothing switches exhaustively on this union;\n// it is carried for logging.\n//\n// 'Delete' arrives ONLY from a Codex `apply_patch` carrying a `*** Delete File:` directive — no Claude\n// Code tool produces it. Every existing rule DEFAULTS TO NOT FIRING on it (see DeleteScopedRules in\n// ./delete-scoped-rules.ts): a rule written to judge the bytes an edit ADDS has nothing to say about a file going\n// away, and inventing a verdict for it would be guessing. Rules for which a delete IS meaningful — a\n// barrel export disappearing, a doc that documents a deleted file — opt in there, deliberately.\nexport type ToolKind = 'Write' | 'Edit' | 'MultiEdit' | 'Read' | 'Delete';\nexport type RuleScope = 'edit' | 'file' | 'bash';\n// Which category of built-in rules a hook invocation runs: code-style 'rules', git/PR/branch\n// 'guards' (the hookGuards section), or 'all' (both categories — used by the openclaw plugin adapter,\n// which is a single before_tool_call hook rather than two split PreToolUse hooks).\nexport type HookMode = 'rules' | 'guards' | 'all';\nexport type IsLineDisabled = (lineNum: number, ruleName: string) => boolean;\n\nexport class Violation {\n readonly line: number;\n readonly snippet: string;\n // Optional per-occurrence override for the `→` line. When omitted, the report falls back\n // to the rule's `FixHint.violation`. Dynamic rules (param name, line count, marker path,\n // branch/PR) pass a specific message here; static rules omit it.\n readonly message: string | undefined;\n editIndex: number | undefined;\n editCount: number | undefined;\n\n constructor(line: number, snippet: string, message?: string) {\n this.line = line;\n this.snippet = snippet;\n this.message = message;\n this.editIndex = undefined;\n this.editCount = undefined;\n }\n}\n\nexport class NormalizedEdit {\n readonly oldString: string;\n readonly newString: string;\n\n constructor(oldString: string, newString: string) {\n this.oldString = oldString;\n this.newString = newString;\n }\n}\n\nexport class NormalizedToolInput {\n readonly filePath: string;\n readonly edits: readonly NormalizedEdit[];\n\n constructor(filePath: string, edits: readonly NormalizedEdit[]) {\n this.filePath = filePath;\n this.edits = edits;\n }\n}\n\nexport class NormalizedBashInput {\n readonly command: string;\n\n constructor(command: string) {\n this.command = command;\n }\n}\n\nexport class EditContext {\n readonly tool: ToolKind;\n readonly editIndex: number;\n readonly editCount: number;\n readonly filePath: string;\n readonly relativePath: string;\n readonly workspaceRoot: string;\n readonly addedContent: string;\n readonly strippedContent: string;\n readonly lines: readonly string[];\n readonly strippedLines: readonly string[];\n readonly removedContent: string;\n readonly isLineDisabled: IsLineDisabled;\n options: RuleOptions;\n\n constructor(\n tool: ToolKind,\n editIndex: number,\n editCount: number,\n filePath: string,\n relativePath: string,\n workspaceRoot: string,\n addedContent: string,\n strippedContent: string,\n lines: readonly string[],\n strippedLines: readonly string[],\n removedContent: string,\n isLineDisabled: IsLineDisabled,\n ) {\n this.tool = tool;\n this.editIndex = editIndex;\n this.editCount = editCount;\n this.filePath = filePath;\n this.relativePath = relativePath;\n this.workspaceRoot = workspaceRoot;\n this.addedContent = addedContent;\n this.strippedContent = strippedContent;\n this.lines = lines;\n this.strippedLines = strippedLines;\n this.removedContent = removedContent;\n this.isLineDisabled = isLineDisabled;\n this.options = {};\n }\n}\n\n// A heredoc and its body: `<<EOF` / `<<'EOF'` / `<<-EOF` through the terminator line.\nconst HEREDOC_BODY = /<<-?\\s*(['\"]?)(\\w+)\\1[\\s\\S]*?^\\t*\\2\\s*$/gm;\n\n// A single- or double-quoted span.\nconst QUOTED_SPAN = /'([^']*)'|\"([^\"]*)\"/g;\n\n// Shell syntax that a quoted span is explicitly NOT: separators, pipes, subshells, redirection,\n// expansion. Content carrying any of these is data (a jq filter, a regex, a glob), and unquoting it\n// would hand the segment scanner syntax the shell never sees. See stripProse.\nconst SHELL_METACHARACTER = /[|;&()<>$`]/;\n\nexport class BashContext {\n readonly tool: 'Bash';\n readonly command: string;\n /**\n * `command` with heredoc bodies and prose-in-quotes removed — what a guard should MATCH on, while\n * `command` stays raw for the violation message (a human needs to see what they actually typed).\n *\n * A commit message that merely MENTIONS a blocked command is not that command. This repo's whole\n * subject matter is the git workflow, so its commit messages, docs and the guards' own fix-hint\n * text are full of `git push` / `gh pr …` strings; matching raw text blocks writing about the\n * tooling. A guard that cries wolf is a guard someone eventually turns OFF, which costs far more\n * than the theoretical bypass this opens (`sh -c '<blocked command>'` stops matching). That trade\n * is deliberate: these guards exist to catch a FORGETFUL agent, not an adversarial one.\n *\n * ONLY for blocklist-shaped guards (\"if the command matches X, block\"), where stripping can only\n * ever block LESS. Never use it in an allowlist-shaped guard such as merged-branch-bash-guard,\n * where stripping could turn a permitted recovery command into a blocked one and deadlock the\n * session — there, match on the raw `command`.\n */\n readonly commandCode: string;\n /**\n * The tree this command is JUDGED against — the root of the worktree it actually acts on, which\n * is NOT the shell's cwd whenever the command carries a leading `cd <path> &&` (see\n * EffectiveTreeResolver). Guards read git state, the main-sync cache and the decision log from\n * here, so a command aimed at a linked worktree is judged on that worktree's branch, never the\n * primary clone's.\n */\n readonly workspaceRoot: string;\n /** The directory the command really runs in (post-`cd`). Relative paths resolve against THIS. */\n readonly effectiveCwd: string;\n /** The root owning webpieces.config.json. Equals workspaceRoot except in a linked worktree. */\n readonly governedRoot: string;\n options: RuleOptions;\n\n constructor(command: string, workspaceRoot: string, effectiveCwd?: string, governedRoot?: string) {\n this.tool = 'Bash';\n this.command = command;\n this.commandCode = this.stripProse(command);\n this.workspaceRoot = workspaceRoot;\n this.effectiveCwd = effectiveCwd ?? workspaceRoot;\n this.governedRoot = governedRoot ?? workspaceRoot;\n this.options = {};\n }\n\n /**\n * A quoted span WITHOUT whitespace is kept: `git checkout \"main\"` is a real command with a quoted\n * argument, not prose. One containing whitespace is a sentence, and becomes a single space.\n *\n * ONE exception, and it is a false positive that actually fired: a whitespace-free span carrying\n * SHELL METACHARACTERS is dropped too. Keeping the content strips only the quotes, so a jq filter —\n * `--jq '[.[]|select(.title|test(\"x\"))]|length'` — was handed to the guards as bare shell syntax.\n * Every guard then split it on its `|`, `(` and `;` and saw a segment reading exactly `test`, which\n * whole-repo-build-guard classified as the workspace-wide test script and BLOCKED. There was no\n * build anywhere in that command; it polled npm and GitHub.\n *\n * Quoting is precisely how a shell says \"this is DATA, not syntax\", so honouring the quotes is the\n * correct reading. And because commandCode only ever feeds blocklist-shaped guards, dropping more\n * can only ever block LESS — never turn an allowed command into a refused one.\n */\n private stripProse(command: string): string {\n const withoutHeredocs = command.replace(HEREDOC_BODY, ' ');\n return withoutHeredocs.replace(QUOTED_SPAN, (match: string, single?: string, double?: string): string => {\n const content = single ?? double ?? '';\n if (/\\s/.test(content)) return ' ';\n return SHELL_METACHARACTER.test(content) ? ' ' : content;\n });\n }\n}\n\nexport class FileContext {\n readonly tool: ToolKind;\n readonly filePath: string;\n readonly relativePath: string;\n readonly workspaceRoot: string;\n /** The git tree that owns filePath. workspaceRoot remains the root that owns policy/config. */\n readonly targetRoot: string;\n /** filePath relative to targetRoot, independent of its governed-root spelling. */\n readonly targetRelativePath: string;\n readonly currentFileLines: number;\n readonly linesAdded: number;\n readonly linesRemoved: number;\n readonly projectedFileLines: number;\n options: RuleOptions;\n\n constructor(\n tool: ToolKind,\n filePath: string,\n relativePath: string,\n workspaceRoot: string,\n currentFileLines: number,\n linesAdded: number,\n linesRemoved: number,\n projectedFileLines: number,\n targetRoot: string = workspaceRoot,\n targetRelativePath: string = relativePath,\n ) {\n this.tool = tool;\n this.filePath = filePath;\n this.relativePath = relativePath;\n this.workspaceRoot = workspaceRoot;\n this.targetRoot = targetRoot;\n this.targetRelativePath = targetRelativePath;\n this.currentFileLines = currentFileLines;\n this.linesAdded = linesAdded;\n this.linesRemoved = linesRemoved;\n this.projectedFileLines = projectedFileLines;\n this.options = {};\n }\n}\n\n/**\n * The shape of a custom rule loaded from a `rulesDir` (a plain object returned by require()).\n * It carries only metadata + a `check` method — no on/off logic of its own.\n */\nexport interface PlainRule {\n readonly name: string;\n readonly description: string;\n readonly scope: RuleScope;\n readonly files: readonly string[];\n readonly defaultOptions: RuleOptions;\n readonly fixHint: FixHint;\n check(ctx: EditContext | FileContext | BashContext): readonly Violation[];\n}\n\n/**\n * The runtime contract the runner iterates. Both the built-in rule classes (which extend\n * EditRuleBase/FileRuleBase/BashRuleBase) and the custom-rule adapter satisfy this: they add\n * `shouldRun()` (mode + escape-hatch decision) on top of the PlainRule metadata.\n */\nexport interface Rule extends PlainRule {\n /**\n * The webpieces.config.json key whose entry configures this rule — NOT necessarily `name`.\n *\n * `name` is the operator identity (what a decision-log line and a deny report carry); `configKey`\n * is the switch. They differ wherever several classes implement one POLICY: all four branch-state\n * guards read `branch-state-guard`, all four PR-lifecycle guards read `pr-lifecycle-guard`. Every\n * consumer that asks \"which section does this live in?\" (filterByMode) or \"does this have an\n * entry?\" (the fault-Y config-sync check) must read THIS field, because asking for `name` there\n * demands entries under keys the validator rejects as retired.\n */\n readonly configKey: string;\n shouldRun(): boolean;\n}\n\nexport class RuleGroup {\n readonly ruleName: string;\n readonly ruleDescription: string;\n readonly fixHint: FixHint;\n readonly violations: readonly Violation[];\n\n constructor(\n ruleName: string,\n ruleDescription: string,\n fixHint: FixHint,\n violations: readonly Violation[],\n ) {\n this.ruleName = ruleName;\n this.ruleDescription = ruleDescription;\n this.fixHint = fixHint;\n this.violations = violations;\n }\n}\n\nexport class BlockedResult {\n readonly report: string;\n /**\n * The L0 fault this block IS, in the codebook's own letter (see core/l0-fault-codes.ts), or `-` for\n * an ordinary rule block.\n *\n * It rides on the result rather than being re-derived from the report text because the block is\n * decided deep in the runner (fault C in configMissingBlock, fault Y in checkConfigSync) and\n * STAMPED by the adapter at the terminal boundary, several frames up. Scraping the report for a\n * fault would be a second answer to a question the producer already knows.\n */\n readonly fault: string;\n\n constructor(report: string, fault: string = L0_FAULT_NONE) {\n this.report = report;\n this.fault = fault;\n }\n}\n"]}