@webpieces/ai-hook-rules 0.4.599 → 0.4.601

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