@webpieces/ai-hook-rules 0.4.454 → 0.4.455

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.454",
3
+ "version": "0.4.455",
4
4
  "description": "Pluggable write-time validation framework for AI coding agents (@webpieces/ai-hook-rules). Claude Code PreToolUse + openclaw before_tool_call adapters share one rule engine.",
5
5
  "type": "commonjs",
6
6
  "main": "./src/index.js",
@@ -32,7 +32,7 @@
32
32
  "directory": "packages/tooling/ai-hook-rules"
33
33
  },
34
34
  "dependencies": {
35
- "@webpieces/rules-config": "0.4.454"
35
+ "@webpieces/rules-config": "0.4.455"
36
36
  },
37
37
  "publishConfig": {
38
38
  "access": "public"
@@ -4,16 +4,20 @@ import { BashRuleBase } from '../rule-base';
4
4
  import { FixHint } from '../fix-hint';
5
5
  export declare class PrMergeGuardRule extends BashRuleBase<PrMergeGuardConfig> {
6
6
  constructor(config: PrMergeGuardConfig);
7
- readonly description = "After merging a PR, require switching to main, pulling, and deleting the local branch.";
7
+ readonly description = "Redirect a hand-rolled `gh pr merge` to `pnpm wp-land-pr`, then require branch cleanup.";
8
8
  private currentBranch;
9
9
  private treeKind;
10
10
  private worktreePath;
11
11
  private readonly recovery;
12
12
  get fixHint(): FixHint;
13
13
  /**
14
- * The accepted cleanups differ by tree, so the "already cleaning up" detection has to as well:
15
- * in a linked worktree `git checkout main` FATALS, so demanding it there would be demanding an
16
- * impossible command. A worktree is cleaned up by `git worktree remove` + `git branch -D`.
14
+ * Any hand-rolled `gh pr merge` is blocked outright there is no "but I cleaned up afterwards"
15
+ * escape, because the damage is the commit message that merge writes into main, which cleanup
16
+ * cannot undo. `pnpm wp-land-pr` is the way through; its own `gh pr merge` runs as a child
17
+ * process this hook never sees, exactly like the other gated commands.
18
+ *
19
+ * The tree kind still decides which cleanup steps the hint prints: in a linked worktree
20
+ * `git checkout main` FATALS, so demanding it there would be demanding an impossible command.
17
21
  */
18
22
  check(ctx: BashContext): readonly Violation[];
19
23
  }
@@ -12,7 +12,7 @@ function truncate(s) {
12
12
  }
13
13
  class PrMergeGuardRule extends rule_base_1.BashRuleBase {
14
14
  constructor(config) { super(config, 'pr-merge-guard'); }
15
- description = 'After merging a PR, require switching to main, pulling, and deleting the local branch.';
15
+ description = 'Redirect a hand-rolled `gh pr merge` to `pnpm wp-land-pr`, then require branch cleanup.';
16
16
  // Substituted with the real branch name in check(); the getter reads it. Placeholder until then.
17
17
  currentBranch = '<current-branch>';
18
18
  // The tree we are standing in, resolved in check() so fixHint renders the ONE cleanup that
@@ -28,33 +28,36 @@ class PrMergeGuardRule extends rule_base_1.BashRuleBase {
28
28
  // deletes ONLY provably-dead branches — and it reaps every OTHER dead branch at the same time,
29
29
  // which is the moment that actually keeps the local branch list bounded.
30
30
  get fixHint() {
31
- return new fix_hint_1.FixHint('After merging a PR you must clean up the branch (and the worktree, if it has one).', ['Run `gh pr merge --squash`, then:', '']
31
+ return new fix_hint_1.FixHint('Land the PR with `pnpm wp-land-pr`, never a hand-rolled `gh pr merge`.', [
32
+ 'A bare `gh pr merge` writes the WRONG commit message into main. GitHub then falls back to',
33
+ 'the repo\'s squash_merge_commit_title/message settings, which commonly means the internal',
34
+ '"Squash merge of <branch>" subject — and NEVER the compact risk/flags body, because no',
35
+ 'value of those settings can produce it. Only an explicit `--subject`/`--body-file` can,',
36
+ 'and `wp-land-pr` passes exactly the pair that `wp-finish-upsert-pr` already rendered:',
37
+ '',
38
+ ' pnpm wp-land-pr',
39
+ '',
40
+ 'It squash-merges when the PR is mergeable, and enables auto-merge with the same',
41
+ 'subject/body when the checks are still running. Then clean up:',
42
+ '',
43
+ ]
32
44
  .concat(this.recovery.cleanupSteps(this.treeKind, this.currentBranch, this.worktreePath))
33
45
  .concat(['', "Add this to your memory so you don't forget next time and waste tokens."])
34
46
  .join('\n'));
35
47
  }
36
48
  /**
37
- * The accepted cleanups differ by tree, so the "already cleaning up" detection has to as well:
38
- * in a linked worktree `git checkout main` FATALS, so demanding it there would be demanding an
39
- * impossible command. A worktree is cleaned up by `git worktree remove` + `git branch -D`.
49
+ * Any hand-rolled `gh pr merge` is blocked outright there is no "but I cleaned up afterwards"
50
+ * escape, because the damage is the commit message that merge writes into main, which cleanup
51
+ * cannot undo. `pnpm wp-land-pr` is the way through; its own `gh pr merge` runs as a child
52
+ * process this hook never sees, exactly like the other gated commands.
53
+ *
54
+ * The tree kind still decides which cleanup steps the hint prints: in a linked worktree
55
+ * `git checkout main` FATALS, so demanding it there would be demanding an impossible command.
40
56
  */
41
57
  check(ctx) {
42
58
  if (!/gh\s+pr\s+merge/.test(ctx.command))
43
59
  return [];
44
- // `wp-cleanup` counts as a delete: it is the command the branch-flavoured cleanupSteps now
45
- // hand out, and it deletes the just-merged branch (plus every other dead one). A literal
46
- // `git branch -d` still satisfies the guard too — narrower, but not wrong.
47
- const hasDelete = /git\s+branch\s+-[dD]|wp-cleanup/.test(ctx.command);
48
- const hasCheckout = /git\s+(checkout|switch)\s+main/.test(ctx.command);
49
- const hasWorktreeRemove = /git\s+worktree\s+remove\b/.test(ctx.command);
50
60
  this.treeKind = this.recovery.kindOf(ctx.workspaceRoot);
51
- if (this.treeKind === 'worktree') {
52
- if (hasWorktreeRemove && hasDelete)
53
- return [];
54
- }
55
- else if (hasCheckout && hasDelete) {
56
- return [];
57
- }
58
61
  this.currentBranch = (0, child_process_1.execSync)('git rev-parse --abbrev-ref HEAD', {
59
62
  cwd: ctx.workspaceRoot,
60
63
  encoding: 'utf8',
@@ -1 +1 @@
1
- {"version":3,"file":"pr-merge-guard.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/ai-hook-rules/src/core/rules/pr-merge-guard.ts"],"names":[],"mappings":";;;AAAA,iDAAyC;AAKzC,oCAA0C;AAC1C,4CAA4C;AAC5C,0CAAsC;AACtC,mDAAyD;AAEzD,SAAS,QAAQ,CAAC,CAAS;IACvB,MAAM,GAAG,GAAG,GAAG,CAAC;IAChB,OAAO,CAAC,CAAC,MAAM,IAAI,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC,GAAG,GAAG,CAAC;AACvD,CAAC;AAED,MAAa,gBAAiB,SAAQ,wBAAgC;IAClE,YAAY,MAA0B,IAAI,KAAK,CAAC,MAAM,EAAE,gBAAgB,CAAC,CAAC,CAAC,CAAC;IAEnE,WAAW,GAAG,wFAAwF,CAAC;IAEhH,iGAAiG;IACzF,aAAa,GAAG,kBAAkB,CAAC;IAE3C,2FAA2F;IAC3F,8FAA8F;IACtF,QAAQ,GAAa,SAAS,CAAC;IAC/B,YAAY,GAAG,gBAAgB,CAAC;IAEvB,QAAQ,GAAG,IAAI,4BAAY,EAAE,CAAC;IAE/C,6FAA6F;IAC7F,iEAAiE;IACjE,EAAE;IACF,8FAA8F;IAC9F,gGAAgG;IAChG,+FAA+F;IAC/F,yEAAyE;IACzE,IAAI,OAAO;QACP,OAAO,IAAI,kBAAO,CACd,oFAAoF,EACpF,CAAC,mCAAmC,EAAE,EAAE,CAAC;aACpC,MAAM,CAAC,IAAI,CAAC,QAAQ,CAAC,YAAY,CAAC,IAAI,CAAC,QAAQ,EAAE,IAAI,CAAC,aAAa,EAAE,IAAI,CAAC,YAAY,CAAC,CAAC;aACxF,MAAM,CAAC,CAAC,EAAE,EAAE,yEAAyE,CAAC,CAAC;aACvF,IAAI,CAAC,IAAI,CAAC,CAClB,CAAC;IACN,CAAC;IAED;;;;OAIG;IACH,KAAK,CAAC,GAAgB;QAClB,IAAI,CAAC,iBAAiB,CAAC,IAAI,CAAC,GAAG,CAAC,OAAO,CAAC;YAAE,OAAO,EAAE,CAAC;QAEpD,2FAA2F;QAC3F,yFAAyF;QACzF,2EAA2E;QAC3E,MAAM,SAAS,GAAG,iCAAiC,CAAC,IAAI,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC;QACtE,MAAM,WAAW,GAAG,gCAAgC,CAAC,IAAI,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC;QACvE,MAAM,iBAAiB,GAAG,2BAA2B,CAAC,IAAI,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC;QAExE,IAAI,CAAC,QAAQ,GAAG,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC,GAAG,CAAC,aAAa,CAAC,CAAC;QACxD,IAAI,IAAI,CAAC,QAAQ,KAAK,UAAU,EAAE,CAAC;YAC/B,IAAI,iBAAiB,IAAI,SAAS;gBAAE,OAAO,EAAE,CAAC;QAClD,CAAC;aAAM,IAAI,WAAW,IAAI,SAAS,EAAE,CAAC;YAClC,OAAO,EAAE,CAAC;QACd,CAAC;QAED,IAAI,CAAC,aAAa,GAAG,IAAA,wBAAQ,EAAC,iCAAiC,EAAE;YAC7D,GAAG,EAAE,GAAG,CAAC,aAAa;YACtB,QAAQ,EAAE,MAAM;SACnB,CAAC,CAAC,IAAI,EAAE,CAAC;QACV,IAAI,CAAC,YAAY,GAAG,GAAG,CAAC,aAAa,CAAC;QAEtC,OAAO,CAAC,IAAI,iBAAC,CAAC,CAAC,EAAE,QAAQ,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC;IAC7C,CAAC;CACJ;AA9DD,4CA8DC","sourcesContent":["import { execSync } from 'child_process';\n\nimport { PrMergeGuardConfig } from '@webpieces/rules-config';\n\nimport type { BashContext, Violation } from '../types';\nimport { Violation as V } from '../types';\nimport { BashRuleBase } from '../rule-base';\nimport { FixHint } from '../fix-hint';\nimport { TreeRecovery, TreeKind } from './tree-recovery';\n\nfunction truncate(s: string): string {\n const MAX = 120;\n return s.length <= MAX ? s : s.slice(0, MAX) + '…';\n}\n\nexport class PrMergeGuardRule extends BashRuleBase<PrMergeGuardConfig> {\n constructor(config: PrMergeGuardConfig) { super(config, 'pr-merge-guard'); }\n\n readonly description = 'After merging a PR, require switching to main, pulling, and deleting the local branch.';\n\n // Substituted with the real branch name in check(); the getter reads it. Placeholder until then.\n private currentBranch = '<current-branch>';\n\n // The tree we are standing in, resolved in check() so fixHint renders the ONE cleanup that\n // actually works here. 'unknown' until check() runs (fixHint is also read before/without it).\n private treeKind: TreeKind = 'unknown';\n private worktreePath = '<worktree-dir>';\n\n private readonly recovery = new TreeRecovery();\n\n // Single fix, no distinct options — the whole guidance lives in mainMessage so it renders as\n // one coherent block (never split into fake \"Fix Option 1/2/3\").\n //\n // `pnpm wp-cleanup` rather than `git branch -d <branch>`: a raw `-d` reads as destructive, so\n // agents ask permission and stop, and the branch survives. wp-cleanup is one named command that\n // deletes ONLY provably-dead branches — and it reaps every OTHER dead branch at the same time,\n // which is the moment that actually keeps the local branch list bounded.\n get fixHint(): FixHint {\n return new FixHint(\n 'After merging a PR you must clean up the branch (and the worktree, if it has one).',\n ['Run `gh pr merge --squash`, then:', '']\n .concat(this.recovery.cleanupSteps(this.treeKind, this.currentBranch, this.worktreePath))\n .concat(['', \"Add this to your memory so you don't forget next time and waste tokens.\"])\n .join('\\n'),\n );\n }\n\n /**\n * The accepted cleanups differ by tree, so the \"already cleaning up\" detection has to as well:\n * in a linked worktree `git checkout main` FATALS, so demanding it there would be demanding an\n * impossible command. A worktree is cleaned up by `git worktree remove` + `git branch -D`.\n */\n check(ctx: BashContext): readonly Violation[] {\n if (!/gh\\s+pr\\s+merge/.test(ctx.command)) return [];\n\n // `wp-cleanup` counts as a delete: it is the command the branch-flavoured cleanupSteps now\n // hand out, and it deletes the just-merged branch (plus every other dead one). A literal\n // `git branch -d` still satisfies the guard too narrower, but not wrong.\n const hasDelete = /git\\s+branch\\s+-[dD]|wp-cleanup/.test(ctx.command);\n const hasCheckout = /git\\s+(checkout|switch)\\s+main/.test(ctx.command);\n const hasWorktreeRemove = /git\\s+worktree\\s+remove\\b/.test(ctx.command);\n\n this.treeKind = this.recovery.kindOf(ctx.workspaceRoot);\n if (this.treeKind === 'worktree') {\n if (hasWorktreeRemove && hasDelete) return [];\n } else if (hasCheckout && hasDelete) {\n return [];\n }\n\n this.currentBranch = execSync('git rev-parse --abbrev-ref HEAD', {\n cwd: ctx.workspaceRoot,\n encoding: 'utf8',\n }).trim();\n this.worktreePath = ctx.workspaceRoot;\n\n return [new V(1, truncate(ctx.command))];\n }\n}\n"]}
1
+ {"version":3,"file":"pr-merge-guard.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/ai-hook-rules/src/core/rules/pr-merge-guard.ts"],"names":[],"mappings":";;;AAAA,iDAAyC;AAKzC,oCAA0C;AAC1C,4CAA4C;AAC5C,0CAAsC;AACtC,mDAAyD;AAEzD,SAAS,QAAQ,CAAC,CAAS;IACvB,MAAM,GAAG,GAAG,GAAG,CAAC;IAChB,OAAO,CAAC,CAAC,MAAM,IAAI,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC,GAAG,GAAG,CAAC;AACvD,CAAC;AAED,MAAa,gBAAiB,SAAQ,wBAAgC;IAClE,YAAY,MAA0B,IAAI,KAAK,CAAC,MAAM,EAAE,gBAAgB,CAAC,CAAC,CAAC,CAAC;IAEnE,WAAW,GAAG,yFAAyF,CAAC;IAEjH,iGAAiG;IACzF,aAAa,GAAG,kBAAkB,CAAC;IAE3C,2FAA2F;IAC3F,8FAA8F;IACtF,QAAQ,GAAa,SAAS,CAAC;IAC/B,YAAY,GAAG,gBAAgB,CAAC;IAEvB,QAAQ,GAAG,IAAI,4BAAY,EAAE,CAAC;IAE/C,6FAA6F;IAC7F,iEAAiE;IACjE,EAAE;IACF,8FAA8F;IAC9F,gGAAgG;IAChG,+FAA+F;IAC/F,yEAAyE;IACzE,IAAI,OAAO;QACP,OAAO,IAAI,kBAAO,CACd,wEAAwE,EACxE;YACI,2FAA2F;YAC3F,2FAA2F;YAC3F,wFAAwF;YACxF,yFAAyF;YACzF,uFAAuF;YACvF,EAAE;YACF,mBAAmB;YACnB,EAAE;YACF,iFAAiF;YACjF,gEAAgE;YAChE,EAAE;SACL;aACI,MAAM,CAAC,IAAI,CAAC,QAAQ,CAAC,YAAY,CAAC,IAAI,CAAC,QAAQ,EAAE,IAAI,CAAC,aAAa,EAAE,IAAI,CAAC,YAAY,CAAC,CAAC;aACxF,MAAM,CAAC,CAAC,EAAE,EAAE,yEAAyE,CAAC,CAAC;aACvF,IAAI,CAAC,IAAI,CAAC,CAClB,CAAC;IACN,CAAC;IAED;;;;;;;;OAQG;IACH,KAAK,CAAC,GAAgB;QAClB,IAAI,CAAC,iBAAiB,CAAC,IAAI,CAAC,GAAG,CAAC,OAAO,CAAC;YAAE,OAAO,EAAE,CAAC;QAEpD,IAAI,CAAC,QAAQ,GAAG,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC,GAAG,CAAC,aAAa,CAAC,CAAC;QACxD,IAAI,CAAC,aAAa,GAAG,IAAA,wBAAQ,EAAC,iCAAiC,EAAE;YAC7D,GAAG,EAAE,GAAG,CAAC,aAAa;YACtB,QAAQ,EAAE,MAAM;SACnB,CAAC,CAAC,IAAI,EAAE,CAAC;QACV,IAAI,CAAC,YAAY,GAAG,GAAG,CAAC,aAAa,CAAC;QAEtC,OAAO,CAAC,IAAI,iBAAC,CAAC,CAAC,EAAE,QAAQ,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC;IAC7C,CAAC;CACJ;AAjED,4CAiEC","sourcesContent":["import { execSync } from 'child_process';\n\nimport { PrMergeGuardConfig } from '@webpieces/rules-config';\n\nimport type { BashContext, Violation } from '../types';\nimport { Violation as V } from '../types';\nimport { BashRuleBase } from '../rule-base';\nimport { FixHint } from '../fix-hint';\nimport { TreeRecovery, TreeKind } from './tree-recovery';\n\nfunction truncate(s: string): string {\n const MAX = 120;\n return s.length <= MAX ? s : s.slice(0, MAX) + '…';\n}\n\nexport class PrMergeGuardRule extends BashRuleBase<PrMergeGuardConfig> {\n constructor(config: PrMergeGuardConfig) { super(config, 'pr-merge-guard'); }\n\n readonly description = 'Redirect a hand-rolled `gh pr merge` to `pnpm wp-land-pr`, then require branch cleanup.';\n\n // Substituted with the real branch name in check(); the getter reads it. Placeholder until then.\n private currentBranch = '<current-branch>';\n\n // The tree we are standing in, resolved in check() so fixHint renders the ONE cleanup that\n // actually works here. 'unknown' until check() runs (fixHint is also read before/without it).\n private treeKind: TreeKind = 'unknown';\n private worktreePath = '<worktree-dir>';\n\n private readonly recovery = new TreeRecovery();\n\n // Single fix, no distinct options — the whole guidance lives in mainMessage so it renders as\n // one coherent block (never split into fake \"Fix Option 1/2/3\").\n //\n // `pnpm wp-cleanup` rather than `git branch -d <branch>`: a raw `-d` reads as destructive, so\n // agents ask permission and stop, and the branch survives. wp-cleanup is one named command that\n // deletes ONLY provably-dead branches — and it reaps every OTHER dead branch at the same time,\n // which is the moment that actually keeps the local branch list bounded.\n get fixHint(): FixHint {\n return new FixHint(\n 'Land the PR with `pnpm wp-land-pr`, never a hand-rolled `gh pr merge`.',\n [\n 'A bare `gh pr merge` writes the WRONG commit message into main. GitHub then falls back to',\n 'the repo\\'s squash_merge_commit_title/message settings, which commonly means the internal',\n '\"Squash merge of <branch>\" subject — and NEVER the compact risk/flags body, because no',\n 'value of those settings can produce it. Only an explicit `--subject`/`--body-file` can,',\n 'and `wp-land-pr` passes exactly the pair that `wp-finish-upsert-pr` already rendered:',\n '',\n ' pnpm wp-land-pr',\n '',\n 'It squash-merges when the PR is mergeable, and enables auto-merge with the same',\n 'subject/body when the checks are still running. Then clean up:',\n '',\n ]\n .concat(this.recovery.cleanupSteps(this.treeKind, this.currentBranch, this.worktreePath))\n .concat(['', \"Add this to your memory so you don't forget next time and waste tokens.\"])\n .join('\\n'),\n );\n }\n\n /**\n * Any hand-rolled `gh pr merge` is blocked outright there is no \"but I cleaned up afterwards\"\n * escape, because the damage is the commit message that merge writes into main, which cleanup\n * cannot undo. `pnpm wp-land-pr` is the way through; its own `gh pr merge` runs as a child\n * process this hook never sees, exactly like the other gated commands.\n *\n * The tree kind still decides which cleanup steps the hint prints: in a linked worktree\n * `git checkout main` FATALS, so demanding it there would be demanding an impossible command.\n */\n check(ctx: BashContext): readonly Violation[] {\n if (!/gh\\s+pr\\s+merge/.test(ctx.command)) return [];\n\n this.treeKind = this.recovery.kindOf(ctx.workspaceRoot);\n this.currentBranch = execSync('git rev-parse --abbrev-ref HEAD', {\n cwd: ctx.workspaceRoot,\n encoding: 'utf8',\n }).trim();\n this.worktreePath = ctx.workspaceRoot;\n\n return [new V(1, truncate(ctx.command))];\n }\n}\n"]}