@webpieces/ai-hook-rules 0.4.593 → 0.4.594

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.593",
3
+ "version": "0.4.594",
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.593"
28
+ "@webpieces/rules-config": "0.4.594"
29
29
  },
30
30
  "publishConfig": {
31
31
  "access": "public"
@@ -29,16 +29,18 @@ class PrMergeGuardRule extends rule_base_1.BashRuleBase {
29
29
  // which is the moment that actually keeps the local branch list bounded.
30
30
  get fixHint() {
31
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>" subjectand 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 `pnpm wp-land-pr` passes exactly the pair that `wp-finish-upsert-pr` already rendered:',
32
+ 'A bare `gh pr merge` leaves the commit message to the repo\'s squash_merge_commit_title/',
33
+ 'message settings. On a repo set to PR_TITLE + PR_BODY that now happens to be RIGHT — the',
34
+ 'PR description IS the gated commit body but on any other combination it is wrong, and',
35
+ 'commonly means the internal "Squash merge of <branch>" subject. `pnpm wp-land-pr` passes',
36
+ '`--subject`/`--body-file` explicitly, so it is right on EVERY repo regardless of settings:',
37
37
  '',
38
38
  ' pnpm wp-land-pr',
39
39
  '',
40
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:',
41
+ 'subject/body when the checks are still running. It also does the two things a raw merge',
42
+ 'never does: archive the pre-squash tip as archive/<date>/<branch>, and reap the landed',
43
+ 'worktree. Then clean up:',
42
44
  '',
43
45
  ]
44
46
  .concat(this.recovery.cleanupSteps(this.treeKind, this.currentBranch, this.worktreePath))
@@ -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,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,4FAA4F;YAC5F,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;;;;;;;;;;;;;;;;;;;;;;;OAuBG;IACH,KAAK,CAAC,GAAgB;QAClB,IAAI,CAAC,iBAAiB,CAAC,IAAI,CAAC,GAAG,CAAC,WAAW,CAAC;YAAE,OAAO,EAAE,CAAC;QAExD,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;AAhFD,4CAgFC","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>\" subjectand 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 `pnpm 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 * ─── Why a CORRECT-LOOKING `gh pr merge --auto --subject --body-file` is still blocked ──────────\n * Full reasoning, and what was rejected: `decisions/0004-pr-artifacts-are-machine-global.md` § 8.\n * It was re-examined when the gated body moved to its machine-global home, and the block stands. A\n * `--body-file` proves a file was passed, never that it holds the GATED bytes: the guard sees a\n * command string, cannot know the PR number, and so cannot check the path against\n * `~/.webpieces/prs/<host>/<owner>/<repo>/<n>/merge-commit-body.md`. Allowing the shape would let any\n * file at all — including a hand-written one, or the PR description — land as the reviewed body,\n * with nothing to distinguish it afterwards.\n *\n * Two things now depend on that. `wp-land-pr --fallback-title-only` is a HUMAN opt-in that stamps\n * \"this is a fallback\" into the commit; a permitted hand-rolled merge would be the same degraded\n * outcome with no stamp and no opt-in. And landing now DECIDES whether the archive/merge-info/reap\n * bookkeeping belongs to this tree, using the provenance filed beside the body — a hand-rolled merge\n * skips that decision silently, which is how a landed worktree becomes a corpse.\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.commandCode)) 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"]}
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,0FAA0F;YAC1F,0FAA0F;YAC1F,yFAAyF;YACzF,0FAA0F;YAC1F,4FAA4F;YAC5F,EAAE;YACF,mBAAmB;YACnB,EAAE;YACF,iFAAiF;YACjF,yFAAyF;YACzF,wFAAwF;YACxF,0BAA0B;YAC1B,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;;;;;;;;;;;;;;;;;;;;;;;OAuBG;IACH,KAAK,CAAC,GAAgB;QAClB,IAAI,CAAC,iBAAiB,CAAC,IAAI,CAAC,GAAG,CAAC,WAAW,CAAC;YAAE,OAAO,EAAE,CAAC;QAExD,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;AAlFD,4CAkFC","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` leaves the commit message to the repo\\'s squash_merge_commit_title/',\n 'message settings. On a repo set to PR_TITLE + PR_BODY that now happens to be RIGHT — the',\n 'PR description IS the gated commit body but on any other combination it is wrong, and',\n 'commonly means the internal \"Squash merge of <branch>\" subject. `pnpm wp-land-pr` passes',\n '`--subject`/`--body-file` explicitly, so it is right on EVERY repo regardless of settings:',\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. It also does the two things a raw merge',\n 'never does: archive the pre-squash tip as archive/<date>/<branch>, and reap the landed',\n 'worktree. 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 * ─── Why a CORRECT-LOOKING `gh pr merge --auto --subject --body-file` is still blocked ──────────\n * Full reasoning, and what was rejected: `decisions/0004-pr-artifacts-are-machine-global.md` § 8.\n * It was re-examined when the gated body moved to its machine-global home, and the block stands. A\n * `--body-file` proves a file was passed, never that it holds the GATED bytes: the guard sees a\n * command string, cannot know the PR number, and so cannot check the path against\n * `~/.webpieces/prs/<host>/<owner>/<repo>/<n>/merge-commit-body.md`. Allowing the shape would let any\n * file at all — including a hand-written one, or the PR description — land as the reviewed body,\n * with nothing to distinguish it afterwards.\n *\n * Two things now depend on that. `wp-land-pr --fallback-title-only` is a HUMAN opt-in that stamps\n * \"this is a fallback\" into the commit; a permitted hand-rolled merge would be the same degraded\n * outcome with no stamp and no opt-in. And landing now DECIDES whether the archive/merge-info/reap\n * bookkeeping belongs to this tree, using the provenance filed beside the body — a hand-rolled merge\n * skips that decision silently, which is how a landed worktree becomes a corpse.\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.commandCode)) 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"]}