@webpieces/ai-hook-rules 0.4.634 → 0.4.635

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.634",
3
+ "version": "0.4.635",
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.634"
28
+ "@webpieces/rules-config": "0.4.635"
29
29
  },
30
30
  "publishConfig": {
31
31
  "access": "public"
@@ -28,9 +28,14 @@ const FIX_HINT = new fix_hint_1.FixHint('`git merge` / `git rebase` are never ru
28
28
  + '\n'
29
29
  // The "do not run it, ask the human, tell them to push back" instruction is NOT repeated here: the
30
30
  // violation line the AI reads first already says it. This paragraph's job is only the pointer.
31
+ // The liveness phrase is kept on ONE line deliberately: it is asserted in full, and a wrap would
32
+ // split the assertion's substring across two chunks. The retired wording ("a branch you do not own")
33
+ // was pinned only by a `never sync a branch` PREFIX, which passed under both framings — which is how
34
+ // this hint kept teaching the removed rule after the doc it links had replaced it.
31
35
  + 'READ the instruct-ai git-workflow doc at the absolute path on the violation line above — the full\n'
32
- + 'flow (incl. worktrees), the fork-point invariant, why you must never sync a branch another\n'
33
- + 'session owns, and the exact words to warn a human with when you ask them to run a merge.\n'
36
+ + 'flow (incl. worktrees), the fork-point invariant, why you must\n'
37
+ + 'never sync a branch something is ACTIVELY holding (liveness, not authorship a finished agent\n'
38
+ + 'holds nothing), and the exact words to warn a human with when you ask them to run a merge.\n'
34
39
  // NOT "add that info to memory": "that info" was the whole git-workflow doc, which is REGENERATED
35
40
  // per repo and per version — memorizing it guarantees a stale recall in the next session. Only the
36
41
  // invariant is stable enough to persist; the doc must be re-read, never remembered.
@@ -1 +1 @@
1
- {"version":3,"file":"redirect-how-to-merge-main.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/ai-hook-rules/src/core/rules/redirect-how-to-merge-main.ts"],"names":[],"mappings":"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","sourcesContent":["import { execSync } from 'child_process';\n\nimport { RedirectHowToMergeMainConfig, RepoRootFinder, SyncFlowGuidance, writeTemplate } 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 { CommandScanner } from '../command-scan';\nimport { TreeRecovery } from './tree-recovery';\nimport { BranchSwitch, BranchSwitchScan } from './branch-switch-scan';\n\nconst INSTRUCT_FILE = 'webpieces.git-workflow.md';\nconst GUIDANCE = new SyncFlowGuidance();\n\nconst FIX_HINT = new FixHint(\n '`git merge` / `git rebase` are never run by AI — on any branch, in any form.',\n 'To bring main\\'s changes into your feature branch:\\n'\n + '\\n'\n + GUIDANCE.flows().join('\\n') + '\\n'\n + '\\n'\n // The fork-point DERIVATION (why the invariant has two halves, and the three consumers a polluted\n // fork point corrupts) and the WORD-FOR-WORD warning to hand the human are NOT here on purpose.\n // Both are stated in full in the git-workflow doc this hint links by absolute path, and a message\n // that re-derives what the linked doc already says is a second copy to keep in step. What survives\n // here is only what changes what the AI types next.\n + 'Either flow does a 3-point merge, which keeps the fork point a pure main commit — what the build\\n'\n + 'gate\\'s --base and the PR review diff are computed from; a raw merge/rebase destroys it. The\\n'\n + 'gated commands merge internally as child processes this hook never sees.\\n'\n + '\\n'\n + GUIDANCE.readOnlyChecks().join('\\n') + '\\n'\n + '\\n'\n // The \"do not run it, ask the human, tell them to push back\" instruction is NOT repeated here: the\n // violation line the AI reads first already says it. This paragraph's job is only the pointer.\n + 'READ the instruct-ai git-workflow doc at the absolute path on the violation line above — the full\\n'\n + 'flow (incl. worktrees), the fork-point invariant, why you must never sync a branch another\\n'\n + 'session owns, and the exact words to warn a human with when you ask them to run a merge.\\n'\n // NOT \"add that info to memory\": \"that info\" was the whole git-workflow doc, which is REGENERATED\n // per repo and per version — memorizing it guarantees a stale recall in the next session. Only the\n // invariant is stable enough to persist; the doc must be re-read, never remembered.\n + 'Add to memory: never run a raw `git merge`/`git rebase` — use this repo\\'s gated sync flow and\\n'\n + 're-read its git-workflow doc each time rather than recalling it (regenerated per repo/version).',\n);\n\n// `git merge --abort` / `git rebase --abort|--quit` UNDO an in-progress operation — they cannot create\n// a merge commit or rewrite history, so they cannot violate the fork-point invariant this rule\n// protects. They stay allowed so a repo left mid-operation (e.g. by a human-run rebase) can still be\n// cleaned up. `--continue` is deliberately NOT here: it COMPLETES the operation.\nconst UNDO_FLAG = /--(?:abort|quit)\\b/;\n\n// Typed as a query (\"would this fast-forward?\"), but a successful --ff-only IS the merge.\nconst FF_ONLY = /--ff-only\\b/;\n\nfunction truncate(s: string): string {\n const MAX = 120;\n return s.length <= MAX ? s : s.slice(0, MAX) + '…';\n}\n\nexport class RedirectHowToMergeMainRule extends BashRuleBase<RedirectHowToMergeMainConfig> {\n private readonly scanner = new CommandScanner();\n private readonly recovery = new TreeRecovery();\n // \"Which branch does this land on?\" — see branch-switch-scan.ts. It replaced a pair of regexes\n // here (`/git\\s+(?:checkout|switch)\\s+main\\b/` and its negative-lookahead twin) that assumed the\n // branch name is the token immediately after the subcommand. One flag broke both halves at once:\n // `git checkout -q main && git pull -q origin main` missed the main EXEMPTION and then tripped the\n // feature-switch block, so this guard rejected the very command stale-main-bash-guard prescribes,\n // with a reason (\"switches to a feature branch\") that was the opposite of the truth.\n private readonly switches = new BranchSwitchScan(this.scanner);\n\n constructor(config: RedirectHowToMergeMainConfig) { super(config, 'redirect-how-to-merge-main'); }\n\n readonly description = 'Block ALL `git merge`/`git rebase` (any branch, any form) and `git pull origin main` on a feature branch. Use the squash-update process instead.';\n readonly fixHint = FIX_HINT;\n\n check(ctx: BashContext): readonly Violation[] {\n for (const segment of this.scanner.commandSegments(ctx.command)) {\n const violation = this.checkSegment(ctx, segment);\n if (violation !== null) return [violation];\n }\n return [];\n }\n\n private checkSegment(ctx: BashContext, segment: string): Violation | null {\n // 1. merge/rebase: unconditional block. Deliberately NO branch lookup.\n //\n // This rule used to read hook-time HEAD and bail out when it was `main`. But a PreToolUse hook\n // runs BEFORE the command, so HEAD-at-hook-time is a value the command itself is about to\n // change: `git checkout feat && git rebase main`, issued while HEAD was still `main` from a\n // prior cleanup, read as \"we're on main, this is fine\" and was waved through. That is the\n // incident this rule exists to prevent. Since merge/rebase have no legitimate AI-run form on\n // ANY branch, there is no branch to consult — and so no HEAD to spoof.\n if (this.scanner.invokesGit(segment, 'merge') || this.scanner.invokesGit(segment, 'rebase')) {\n if (UNDO_FLAG.test(segment)) return null;\n // `--ff-only` reads like a probe (\"can I fast-forward?\") but it MUTATES whenever the answer\n // is yes, so say that here — the AI that typed it was usually only trying to look.\n const probe = FF_ONLY.test(segment)\n ? ' `--ff-only` is NOT a read-only check — it moves your branch whenever it succeeds; see the read-only checks below.'\n : '';\n return this.block(ctx, segment, 'Direct `git merge`/`git rebase` is blocked — AI never runs it, on any branch.' + probe);\n }\n\n // 2. pull: unlike merge/rebase this DOES retain a legitimate on-main form\n // (`git checkout main && git pull origin main`), so it must consult the branch — which is\n // exactly why it also needs the branch-switch check that (1) no longer requires.\n if (this.scanner.invokesGit(segment, 'pull') && /\\borigin\\s+main\\b/.test(segment)) {\n return this.checkPull(ctx, segment);\n }\n\n return null;\n }\n\n private checkPull(ctx: BashContext, segment: string): Violation | null {\n // The two branch questions are COMPLEMENTS of one parse, so they are asked of one parse —\n // that is what keeps them from disagreeing. `git branch -D <x>` is not a checkout so it does\n // not trip this, and `git checkout -` (previous branch, unknowable at hook time) lands\n // nowhere we can name, so it counts as neither.\n const switches = this.switches.switchesIn(ctx.command);\n if (switches.some((s: BranchSwitch): boolean => !this.switches.isExistingMain(s))) {\n return this.block(ctx, segment, 'Blocked: this command switches to a feature branch and then pulls main into it.');\n }\n // The recommended `git checkout main && git pull origin main` — but ONLY in the primary\n // clone. Inside a linked worktree that checkout FATALS (\"'main' is already checked out at\n // <primary>\"), so waving it through here hands the AI a command that cannot work and costs\n // it a turn to discover. Steer to the fetch, which is all a worktree needs.\n if (switches.length > 0) {\n if (this.recovery.kindOf(ctx.workspaceRoot) !== 'worktree') return null;\n // block() appends updateMainSteps for the tree we are in — don't say it twice here.\n return this.block(ctx, segment, 'Blocked.');\n }\n\n const currentBranch = execSync('git rev-parse --abbrev-ref HEAD', {\n cwd: ctx.workspaceRoot,\n encoding: 'utf8',\n }).trim();\n if (currentBranch === 'main') return null;\n\n return this.block(ctx, segment, `Pulling main into feature branch '${currentBranch}' is blocked.`);\n }\n\n private block(ctx: BashContext, segment: string, what: string): Violation {\n // Materialize the doc we are about to send the AI to. This guard fires long before any `wp-*`\n // command runs (those are what normally write it), so the linked path could easily not exist —\n // and a STALE copy from an older @webpieces is just as misleading, hence overwrite.\n writeTemplate(ctx.workspaceRoot, INSTRUCT_FILE);\n const docPath = new RepoRootFinder().instructAiDocPath(ctx.workspaceRoot, INSTRUCT_FILE);\n // \"How do I get MAIN itself current?\" is a different question from \"sync my feature branch\",\n // and it used to have no answer anywhere on this path — the flows cover feature branches and\n // the read-only checks cover looking. An AI on main with no third option improvises, and what\n // it improvises is `git reset --hard origin/main`. Answer the question instead, shaped to the\n // tree we are actually in (in a worktree `git checkout main` fatals).\n const updateMain = [\n '',\n 'On main and just wanted to bring MAIN itself up to date? That is a different question from',\n 'syncing a feature branch, and merge/reset is not the answer to it:',\n ...this.recovery.updateMainSteps(this.recovery.kindOf(ctx.workspaceRoot)),\n ].join('\\n');\n return new V(\n 1,\n truncate(segment),\n `${what} Use the gated 3-point flow instead: 'pnpm wp-start-update' → 'pnpm wp-finish-update' when NO PR is open, or 'pnpm wp-start-upsert-pr' → 'pnpm wp-finish-upsert-pr' when a PR IS open (required then — the merge rewrites the branch and the PR must be re-pointed in the same run). If you truly need a raw merge/rebase, ask the HUMAN to run it — and warn them to push back. Full flow: READ ${docPath}.${updateMain}`,\n );\n }\n}\n"]}
1
+ {"version":3,"file":"redirect-how-to-merge-main.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/ai-hook-rules/src/core/rules/redirect-how-to-merge-main.ts"],"names":[],"mappings":";;;AAAA,iDAAyC;AAEzC,0DAAwH;AAGxH,oCAA0C;AAC1C,4CAA4C;AAC5C,0CAAsC;AACtC,kDAAiD;AACjD,mDAA+C;AAC/C,6DAAsE;AAEtE,MAAM,aAAa,GAAG,2BAA2B,CAAC;AAClD,MAAM,QAAQ,GAAG,IAAI,+BAAgB,EAAE,CAAC;AAExC,MAAM,QAAQ,GAAG,IAAI,kBAAO,CACxB,8EAA8E,EAC9E,sDAAsD;MACpD,IAAI;MACJ,QAAQ,CAAC,KAAK,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,IAAI;MAClC,IAAI;IACN,kGAAkG;IAClG,gGAAgG;IAChG,kGAAkG;IAClG,mGAAmG;IACnG,oDAAoD;MAClD,oGAAoG;MACpG,gGAAgG;MAChG,4EAA4E;MAC5E,IAAI;MACJ,QAAQ,CAAC,cAAc,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,IAAI;MAC3C,IAAI;IACN,mGAAmG;IACnG,+FAA+F;IAC/F,iGAAiG;IACjG,qGAAqG;IACrG,qGAAqG;IACrG,mFAAmF;MACjF,qGAAqG;MACrG,kEAAkE;MAClE,kGAAkG;MAClG,8FAA8F;IAChG,kGAAkG;IAClG,mGAAmG;IACnG,oFAAoF;MAClF,kGAAkG;MAClG,iGAAiG,CACtG,CAAC;AAEF,uGAAuG;AACvG,+FAA+F;AAC/F,qGAAqG;AACrG,iFAAiF;AACjF,MAAM,SAAS,GAAG,oBAAoB,CAAC;AAEvC,0FAA0F;AAC1F,MAAM,OAAO,GAAG,aAAa,CAAC;AAE9B,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,0BAA2B,SAAQ,wBAA0C;IACrE,OAAO,GAAG,IAAI,6BAAc,EAAE,CAAC;IAC/B,QAAQ,GAAG,IAAI,4BAAY,EAAE,CAAC;IAC/C,+FAA+F;IAC/F,iGAAiG;IACjG,iGAAiG;IACjG,mGAAmG;IACnG,kGAAkG;IAClG,qFAAqF;IACpE,QAAQ,GAAG,IAAI,qCAAgB,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;IAE/D,YAAY,MAAoC,IAAI,KAAK,CAAC,MAAM,EAAE,4BAA4B,CAAC,CAAC,CAAC,CAAC;IAEzF,WAAW,GAAG,kJAAkJ,CAAC;IACjK,OAAO,GAAG,QAAQ,CAAC;IAE5B,KAAK,CAAC,GAAgB;QAClB,KAAK,MAAM,OAAO,IAAI,IAAI,CAAC,OAAO,CAAC,eAAe,CAAC,GAAG,CAAC,OAAO,CAAC,EAAE,CAAC;YAC9D,MAAM,SAAS,GAAG,IAAI,CAAC,YAAY,CAAC,GAAG,EAAE,OAAO,CAAC,CAAC;YAClD,IAAI,SAAS,KAAK,IAAI;gBAAE,OAAO,CAAC,SAAS,CAAC,CAAC;QAC/C,CAAC;QACD,OAAO,EAAE,CAAC;IACd,CAAC;IAEO,YAAY,CAAC,GAAgB,EAAE,OAAe;QAClD,uEAAuE;QACvE,EAAE;QACF,+FAA+F;QAC/F,0FAA0F;QAC1F,4FAA4F;QAC5F,0FAA0F;QAC1F,6FAA6F;QAC7F,uEAAuE;QACvE,IAAI,IAAI,CAAC,OAAO,CAAC,UAAU,CAAC,OAAO,EAAE,OAAO,CAAC,IAAI,IAAI,CAAC,OAAO,CAAC,UAAU,CAAC,OAAO,EAAE,QAAQ,CAAC,EAAE,CAAC;YAC1F,IAAI,SAAS,CAAC,IAAI,CAAC,OAAO,CAAC;gBAAE,OAAO,IAAI,CAAC;YACzC,4FAA4F;YAC5F,mFAAmF;YACnF,MAAM,KAAK,GAAG,OAAO,CAAC,IAAI,CAAC,OAAO,CAAC;gBAC/B,CAAC,CAAC,oHAAoH;gBACtH,CAAC,CAAC,EAAE,CAAC;YACT,OAAO,IAAI,CAAC,KAAK,CAAC,GAAG,EAAE,OAAO,EAAE,+EAA+E,GAAG,KAAK,CAAC,CAAC;QAC7H,CAAC;QAED,0EAA0E;QAC1E,0FAA0F;QAC1F,iFAAiF;QACjF,IAAI,IAAI,CAAC,OAAO,CAAC,UAAU,CAAC,OAAO,EAAE,MAAM,CAAC,IAAI,mBAAmB,CAAC,IAAI,CAAC,OAAO,CAAC,EAAE,CAAC;YAChF,OAAO,IAAI,CAAC,SAAS,CAAC,GAAG,EAAE,OAAO,CAAC,CAAC;QACxC,CAAC;QAED,OAAO,IAAI,CAAC;IAChB,CAAC;IAEO,SAAS,CAAC,GAAgB,EAAE,OAAe;QAC/C,0FAA0F;QAC1F,6FAA6F;QAC7F,uFAAuF;QACvF,gDAAgD;QAChD,MAAM,QAAQ,GAAG,IAAI,CAAC,QAAQ,CAAC,UAAU,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC;QACvD,IAAI,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAe,EAAW,EAAE,CAAC,CAAC,IAAI,CAAC,QAAQ,CAAC,cAAc,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;YAChF,OAAO,IAAI,CAAC,KAAK,CAAC,GAAG,EAAE,OAAO,EAAE,iFAAiF,CAAC,CAAC;QACvH,CAAC;QACD,wFAAwF;QACxF,0FAA0F;QAC1F,2FAA2F;QAC3F,4EAA4E;QAC5E,IAAI,QAAQ,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YACtB,IAAI,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC,GAAG,CAAC,aAAa,CAAC,KAAK,UAAU;gBAAE,OAAO,IAAI,CAAC;YACxE,oFAAoF;YACpF,OAAO,IAAI,CAAC,KAAK,CAAC,GAAG,EAAE,OAAO,EAAE,UAAU,CAAC,CAAC;QAChD,CAAC;QAED,MAAM,aAAa,GAAG,IAAA,wBAAQ,EAAC,iCAAiC,EAAE;YAC9D,GAAG,EAAE,GAAG,CAAC,aAAa;YACtB,QAAQ,EAAE,MAAM;SACnB,CAAC,CAAC,IAAI,EAAE,CAAC;QACV,IAAI,aAAa,KAAK,MAAM;YAAE,OAAO,IAAI,CAAC;QAE1C,OAAO,IAAI,CAAC,KAAK,CAAC,GAAG,EAAE,OAAO,EAAE,qCAAqC,aAAa,eAAe,CAAC,CAAC;IACvG,CAAC;IAEO,KAAK,CAAC,GAAgB,EAAE,OAAe,EAAE,IAAY;QACzD,8FAA8F;QAC9F,+FAA+F;QAC/F,oFAAoF;QACpF,IAAA,4BAAa,EAAC,GAAG,CAAC,aAAa,EAAE,aAAa,CAAC,CAAC;QAChD,MAAM,OAAO,GAAG,IAAI,6BAAc,EAAE,CAAC,iBAAiB,CAAC,GAAG,CAAC,aAAa,EAAE,aAAa,CAAC,CAAC;QACzF,6FAA6F;QAC7F,6FAA6F;QAC7F,8FAA8F;QAC9F,8FAA8F;QAC9F,sEAAsE;QACtE,MAAM,UAAU,GAAG;YACf,EAAE;YACF,4FAA4F;YAC5F,oEAAoE;YACpE,GAAG,IAAI,CAAC,QAAQ,CAAC,eAAe,CAAC,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC,GAAG,CAAC,aAAa,CAAC,CAAC;SAC5E,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QACb,OAAO,IAAI,iBAAC,CACR,CAAC,EACD,QAAQ,CAAC,OAAO,CAAC,EACjB,GAAG,IAAI,oYAAoY,OAAO,IAAI,UAAU,EAAE,CACra,CAAC;IACN,CAAC;CACJ;AAxGD,gEAwGC","sourcesContent":["import { execSync } from 'child_process';\n\nimport { RedirectHowToMergeMainConfig, RepoRootFinder, SyncFlowGuidance, writeTemplate } 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 { CommandScanner } from '../command-scan';\nimport { TreeRecovery } from './tree-recovery';\nimport { BranchSwitch, BranchSwitchScan } from './branch-switch-scan';\n\nconst INSTRUCT_FILE = 'webpieces.git-workflow.md';\nconst GUIDANCE = new SyncFlowGuidance();\n\nconst FIX_HINT = new FixHint(\n '`git merge` / `git rebase` are never run by AI — on any branch, in any form.',\n 'To bring main\\'s changes into your feature branch:\\n'\n + '\\n'\n + GUIDANCE.flows().join('\\n') + '\\n'\n + '\\n'\n // The fork-point DERIVATION (why the invariant has two halves, and the three consumers a polluted\n // fork point corrupts) and the WORD-FOR-WORD warning to hand the human are NOT here on purpose.\n // Both are stated in full in the git-workflow doc this hint links by absolute path, and a message\n // that re-derives what the linked doc already says is a second copy to keep in step. What survives\n // here is only what changes what the AI types next.\n + 'Either flow does a 3-point merge, which keeps the fork point a pure main commit — what the build\\n'\n + 'gate\\'s --base and the PR review diff are computed from; a raw merge/rebase destroys it. The\\n'\n + 'gated commands merge internally as child processes this hook never sees.\\n'\n + '\\n'\n + GUIDANCE.readOnlyChecks().join('\\n') + '\\n'\n + '\\n'\n // The \"do not run it, ask the human, tell them to push back\" instruction is NOT repeated here: the\n // violation line the AI reads first already says it. This paragraph's job is only the pointer.\n // The liveness phrase is kept on ONE line deliberately: it is asserted in full, and a wrap would\n // split the assertion's substring across two chunks. The retired wording (\"a branch you do not own\")\n // was pinned only by a `never sync a branch` PREFIX, which passed under both framings — which is how\n // this hint kept teaching the removed rule after the doc it links had replaced it.\n + 'READ the instruct-ai git-workflow doc at the absolute path on the violation line above — the full\\n'\n + 'flow (incl. worktrees), the fork-point invariant, why you must\\n'\n + 'never sync a branch something is ACTIVELY holding (liveness, not authorship — a finished agent\\n'\n + 'holds nothing), and the exact words to warn a human with when you ask them to run a merge.\\n'\n // NOT \"add that info to memory\": \"that info\" was the whole git-workflow doc, which is REGENERATED\n // per repo and per version — memorizing it guarantees a stale recall in the next session. Only the\n // invariant is stable enough to persist; the doc must be re-read, never remembered.\n + 'Add to memory: never run a raw `git merge`/`git rebase` — use this repo\\'s gated sync flow and\\n'\n + 're-read its git-workflow doc each time rather than recalling it (regenerated per repo/version).',\n);\n\n// `git merge --abort` / `git rebase --abort|--quit` UNDO an in-progress operation — they cannot create\n// a merge commit or rewrite history, so they cannot violate the fork-point invariant this rule\n// protects. They stay allowed so a repo left mid-operation (e.g. by a human-run rebase) can still be\n// cleaned up. `--continue` is deliberately NOT here: it COMPLETES the operation.\nconst UNDO_FLAG = /--(?:abort|quit)\\b/;\n\n// Typed as a query (\"would this fast-forward?\"), but a successful --ff-only IS the merge.\nconst FF_ONLY = /--ff-only\\b/;\n\nfunction truncate(s: string): string {\n const MAX = 120;\n return s.length <= MAX ? s : s.slice(0, MAX) + '…';\n}\n\nexport class RedirectHowToMergeMainRule extends BashRuleBase<RedirectHowToMergeMainConfig> {\n private readonly scanner = new CommandScanner();\n private readonly recovery = new TreeRecovery();\n // \"Which branch does this land on?\" — see branch-switch-scan.ts. It replaced a pair of regexes\n // here (`/git\\s+(?:checkout|switch)\\s+main\\b/` and its negative-lookahead twin) that assumed the\n // branch name is the token immediately after the subcommand. One flag broke both halves at once:\n // `git checkout -q main && git pull -q origin main` missed the main EXEMPTION and then tripped the\n // feature-switch block, so this guard rejected the very command stale-main-bash-guard prescribes,\n // with a reason (\"switches to a feature branch\") that was the opposite of the truth.\n private readonly switches = new BranchSwitchScan(this.scanner);\n\n constructor(config: RedirectHowToMergeMainConfig) { super(config, 'redirect-how-to-merge-main'); }\n\n readonly description = 'Block ALL `git merge`/`git rebase` (any branch, any form) and `git pull origin main` on a feature branch. Use the squash-update process instead.';\n readonly fixHint = FIX_HINT;\n\n check(ctx: BashContext): readonly Violation[] {\n for (const segment of this.scanner.commandSegments(ctx.command)) {\n const violation = this.checkSegment(ctx, segment);\n if (violation !== null) return [violation];\n }\n return [];\n }\n\n private checkSegment(ctx: BashContext, segment: string): Violation | null {\n // 1. merge/rebase: unconditional block. Deliberately NO branch lookup.\n //\n // This rule used to read hook-time HEAD and bail out when it was `main`. But a PreToolUse hook\n // runs BEFORE the command, so HEAD-at-hook-time is a value the command itself is about to\n // change: `git checkout feat && git rebase main`, issued while HEAD was still `main` from a\n // prior cleanup, read as \"we're on main, this is fine\" and was waved through. That is the\n // incident this rule exists to prevent. Since merge/rebase have no legitimate AI-run form on\n // ANY branch, there is no branch to consult — and so no HEAD to spoof.\n if (this.scanner.invokesGit(segment, 'merge') || this.scanner.invokesGit(segment, 'rebase')) {\n if (UNDO_FLAG.test(segment)) return null;\n // `--ff-only` reads like a probe (\"can I fast-forward?\") but it MUTATES whenever the answer\n // is yes, so say that here — the AI that typed it was usually only trying to look.\n const probe = FF_ONLY.test(segment)\n ? ' `--ff-only` is NOT a read-only check — it moves your branch whenever it succeeds; see the read-only checks below.'\n : '';\n return this.block(ctx, segment, 'Direct `git merge`/`git rebase` is blocked — AI never runs it, on any branch.' + probe);\n }\n\n // 2. pull: unlike merge/rebase this DOES retain a legitimate on-main form\n // (`git checkout main && git pull origin main`), so it must consult the branch — which is\n // exactly why it also needs the branch-switch check that (1) no longer requires.\n if (this.scanner.invokesGit(segment, 'pull') && /\\borigin\\s+main\\b/.test(segment)) {\n return this.checkPull(ctx, segment);\n }\n\n return null;\n }\n\n private checkPull(ctx: BashContext, segment: string): Violation | null {\n // The two branch questions are COMPLEMENTS of one parse, so they are asked of one parse —\n // that is what keeps them from disagreeing. `git branch -D <x>` is not a checkout so it does\n // not trip this, and `git checkout -` (previous branch, unknowable at hook time) lands\n // nowhere we can name, so it counts as neither.\n const switches = this.switches.switchesIn(ctx.command);\n if (switches.some((s: BranchSwitch): boolean => !this.switches.isExistingMain(s))) {\n return this.block(ctx, segment, 'Blocked: this command switches to a feature branch and then pulls main into it.');\n }\n // The recommended `git checkout main && git pull origin main` — but ONLY in the primary\n // clone. Inside a linked worktree that checkout FATALS (\"'main' is already checked out at\n // <primary>\"), so waving it through here hands the AI a command that cannot work and costs\n // it a turn to discover. Steer to the fetch, which is all a worktree needs.\n if (switches.length > 0) {\n if (this.recovery.kindOf(ctx.workspaceRoot) !== 'worktree') return null;\n // block() appends updateMainSteps for the tree we are in — don't say it twice here.\n return this.block(ctx, segment, 'Blocked.');\n }\n\n const currentBranch = execSync('git rev-parse --abbrev-ref HEAD', {\n cwd: ctx.workspaceRoot,\n encoding: 'utf8',\n }).trim();\n if (currentBranch === 'main') return null;\n\n return this.block(ctx, segment, `Pulling main into feature branch '${currentBranch}' is blocked.`);\n }\n\n private block(ctx: BashContext, segment: string, what: string): Violation {\n // Materialize the doc we are about to send the AI to. This guard fires long before any `wp-*`\n // command runs (those are what normally write it), so the linked path could easily not exist —\n // and a STALE copy from an older @webpieces is just as misleading, hence overwrite.\n writeTemplate(ctx.workspaceRoot, INSTRUCT_FILE);\n const docPath = new RepoRootFinder().instructAiDocPath(ctx.workspaceRoot, INSTRUCT_FILE);\n // \"How do I get MAIN itself current?\" is a different question from \"sync my feature branch\",\n // and it used to have no answer anywhere on this path — the flows cover feature branches and\n // the read-only checks cover looking. An AI on main with no third option improvises, and what\n // it improvises is `git reset --hard origin/main`. Answer the question instead, shaped to the\n // tree we are actually in (in a worktree `git checkout main` fatals).\n const updateMain = [\n '',\n 'On main and just wanted to bring MAIN itself up to date? That is a different question from',\n 'syncing a feature branch, and merge/reset is not the answer to it:',\n ...this.recovery.updateMainSteps(this.recovery.kindOf(ctx.workspaceRoot)),\n ].join('\\n');\n return new V(\n 1,\n truncate(segment),\n `${what} Use the gated 3-point flow instead: 'pnpm wp-start-update' → 'pnpm wp-finish-update' when NO PR is open, or 'pnpm wp-start-upsert-pr' → 'pnpm wp-finish-upsert-pr' when a PR IS open (required then — the merge rewrites the branch and the PR must be re-pointed in the same run). If you truly need a raw merge/rebase, ask the HUMAN to run it — and warn them to push back. Full flow: READ ${docPath}.${updateMain}`,\n );\n }\n}\n"]}