@webpieces/ai-hook-rules 0.4.590 → 0.4.592

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.590",
3
+ "version": "0.4.592",
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.590"
28
+ "@webpieces/rules-config": "0.4.592"
29
29
  },
30
30
  "publishConfig": {
31
31
  "access": "public"
@@ -16,6 +16,21 @@ export declare class PrMergeGuardRule extends BashRuleBase<PrMergeGuardConfig> {
16
16
  * cannot undo. `pnpm wp-land-pr` is the way through; its own `gh pr merge` runs as a child
17
17
  * process this hook never sees, exactly like the other gated commands.
18
18
  *
19
+ * ─── Why a CORRECT-LOOKING `gh pr merge --auto --subject --body-file` is still blocked ──────────
20
+ * Full reasoning, and what was rejected: `decisions/0004-pr-artifacts-are-machine-global.md` § 8.
21
+ * It was re-examined when the gated body moved to its machine-global home, and the block stands. A
22
+ * `--body-file` proves a file was passed, never that it holds the GATED bytes: the guard sees a
23
+ * command string, cannot know the PR number, and so cannot check the path against
24
+ * `~/.webpieces/prs/<host>/<owner>/<repo>/<n>/merge-commit-body.md`. Allowing the shape would let any
25
+ * file at all — including a hand-written one, or the PR description — land as the reviewed body,
26
+ * with nothing to distinguish it afterwards.
27
+ *
28
+ * Two things now depend on that. `wp-land-pr --fallback-title-only` is a HUMAN opt-in that stamps
29
+ * "this is a fallback" into the commit; a permitted hand-rolled merge would be the same degraded
30
+ * outcome with no stamp and no opt-in. And landing now DECIDES whether the archive/merge-info/reap
31
+ * bookkeeping belongs to this tree, using the provenance filed beside the body — a hand-rolled merge
32
+ * skips that decision silently, which is how a landed worktree becomes a corpse.
33
+ *
19
34
  * The tree kind still decides which cleanup steps the hint prints: in a linked worktree
20
35
  * `git checkout main` FATALS, so demanding it there would be demanding an impossible command.
21
36
  */
@@ -51,6 +51,21 @@ class PrMergeGuardRule extends rule_base_1.BashRuleBase {
51
51
  * cannot undo. `pnpm wp-land-pr` is the way through; its own `gh pr merge` runs as a child
52
52
  * process this hook never sees, exactly like the other gated commands.
53
53
  *
54
+ * ─── Why a CORRECT-LOOKING `gh pr merge --auto --subject --body-file` is still blocked ──────────
55
+ * Full reasoning, and what was rejected: `decisions/0004-pr-artifacts-are-machine-global.md` § 8.
56
+ * It was re-examined when the gated body moved to its machine-global home, and the block stands. A
57
+ * `--body-file` proves a file was passed, never that it holds the GATED bytes: the guard sees a
58
+ * command string, cannot know the PR number, and so cannot check the path against
59
+ * `~/.webpieces/prs/<host>/<owner>/<repo>/<n>/merge-commit-body.md`. Allowing the shape would let any
60
+ * file at all — including a hand-written one, or the PR description — land as the reviewed body,
61
+ * with nothing to distinguish it afterwards.
62
+ *
63
+ * Two things now depend on that. `wp-land-pr --fallback-title-only` is a HUMAN opt-in that stamps
64
+ * "this is a fallback" into the commit; a permitted hand-rolled merge would be the same degraded
65
+ * outcome with no stamp and no opt-in. And landing now DECIDES whether the archive/merge-info/reap
66
+ * bookkeeping belongs to this tree, using the provenance filed beside the body — a hand-rolled merge
67
+ * skips that decision silently, which is how a landed worktree becomes a corpse.
68
+ *
54
69
  * The tree kind still decides which cleanup steps the hint prints: in a linked worktree
55
70
  * `git checkout main` FATALS, so demanding it there would be demanding an impossible command.
56
71
  */
@@ -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;;;;;;;;OAQG;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;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 `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 * 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,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>\" 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 `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"]}