@webpieces/ai-hook-rules 0.4.503 → 0.4.505
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.
|
|
3
|
+
"version": "0.4.505",
|
|
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.
|
|
35
|
+
"@webpieces/rules-config": "0.4.505"
|
|
36
36
|
},
|
|
37
37
|
"publishConfig": {
|
|
38
38
|
"access": "public"
|
|
@@ -10,13 +10,32 @@ const rule_base_1 = require("../rule-base");
|
|
|
10
10
|
const fix_hint_1 = require("../fix-hint");
|
|
11
11
|
const command_scan_1 = require("../command-scan");
|
|
12
12
|
const DEFAULT_MERGE_COMPLETE_COMMAND = 'pnpm wp-finish-upsert-pr';
|
|
13
|
+
// The ENTIRE enforcement surface of this guard, declared up here because the fixHint RENDERS ITSELF
|
|
14
|
+
// from these two lists (see `blockedCommandList`). The hint used to hand-write "do not run other
|
|
15
|
+
// commands" — an unbounded claim that forbade the reads, the `git add`, the build and the tests that
|
|
16
|
+
// finishing a merge actually requires, and that survived every edit to these lists. It was also wrapped
|
|
17
|
+
// in an "Add to memory" directive, so it outlived the session that read it. Generating the sentence
|
|
18
|
+
// from the code is what stops the two drifting apart again.
|
|
19
|
+
const BLOCKED_GIT_SUBCOMMANDS = ['commit', 'push', 'merge', 'rebase'];
|
|
20
|
+
const BLOCKED_GH_PR_SUBCOMMANDS = ['create', 'edit', 'merge'];
|
|
21
|
+
// webpieces-disable no-function-outside-class -- module-level guard helper, matches the rest of this file
|
|
22
|
+
function blockedCommandList() {
|
|
23
|
+
return BLOCKED_GIT_SUBCOMMANDS.map((sub) => `\`git ${sub}\``).join(', ')
|
|
24
|
+
+ `, and \`gh pr ${BLOCKED_GH_PR_SUBCOMMANDS.join('|')}\``;
|
|
25
|
+
}
|
|
26
|
+
// webpieces-disable no-function-outside-class -- module-level guard helper, matches the rest of this file
|
|
13
27
|
function fixHintFor(mergeCompleteCommand) {
|
|
14
28
|
return new fix_hint_1.FixHint('A merge is in progress and not yet validated — this command is blocked.', 'You started a merge but never called the finish-merge command, so a 3-point merge is still in progress.\n'
|
|
15
|
-
+
|
|
29
|
+
+ `Blocked until the merge is validated: ${blockedCommandList()}. That is the whole list;\n`
|
|
30
|
+
+ 'the finish command below does the commit for you.\n'
|
|
31
|
+
+ 'EXPECTED of you right now, and NOT blocked: read the conflicted files, edit them until every\n'
|
|
32
|
+
+ 'conflict marker is gone, `git add` each file you resolve, write the merge explanation for each\n'
|
|
33
|
+
+ 'one, and run the build and the tests. Finishing a merge requires all of those.\n'
|
|
34
|
+
+ 'Then run:\n'
|
|
16
35
|
+ ` ${mergeCompleteCommand}\n`
|
|
17
36
|
+ 'That scans for leftover conflict markers and runs the build; only when green does it commit,\n'
|
|
18
37
|
+ 'unblock commit/push/PR, render the dashboard, and create/update the PR.\n'
|
|
19
|
-
+ 'Add to memory:
|
|
38
|
+
+ 'Add to memory: finish a started merge before beginning other work.');
|
|
20
39
|
}
|
|
21
40
|
// Returns the path of the first UNVALIDATED merge marker found, or null. We detect validation
|
|
22
41
|
// by a raw substring (no JSON.parse) so a malformed marker can never crash the guard.
|
|
@@ -54,16 +73,19 @@ function findMarkerUnder(dir, depth) {
|
|
|
54
73
|
function findUnvalidatedMerge(workspaceRoot) {
|
|
55
74
|
return findMarkerUnder(path.join(workspaceRoot, rules_config_1.WEBPIECES_TMP_DIR, rules_config_1.MERGE_INFO_DIR), MARKER_SCAN_MAX_DEPTH);
|
|
56
75
|
}
|
|
57
|
-
const BLOCKED_GIT_SUBCOMMANDS = ['commit', 'push', 'merge', 'rebase'];
|
|
58
76
|
const SCANNER = new command_scan_1.CommandScanner();
|
|
77
|
+
// Built from BLOCKED_GH_PR_SUBCOMMANDS so the enforcement and the sentence the fixHint prints can
|
|
78
|
+
// never name different commands.
|
|
79
|
+
const BLOCKED_GH_PR_PATTERN = new RegExp(`\\bgh\\s+pr\\s+(${BLOCKED_GH_PR_SUBCOMMANDS.join('|')})\\b`);
|
|
59
80
|
// Operations that would let an agent route around the merge gate.
|
|
60
81
|
//
|
|
61
82
|
// Routed through CommandScanner rather than `/\bgit\s+merge\b/`: that pattern matches the read-only
|
|
62
83
|
// `git merge-base origin/main HEAD` (`\b` sits between `e` and `-`), which appears in this repo's own
|
|
63
84
|
// documented build command — so an in-progress merge used to block a harmless diff-scope lookup.
|
|
85
|
+
// webpieces-disable no-function-outside-class -- module-level guard helper, matches the rest of this file
|
|
64
86
|
function isBlockedDuringMerge(cmd) {
|
|
65
87
|
return SCANNER.commandInvokesAnyGit(cmd, BLOCKED_GIT_SUBCOMMANDS)
|
|
66
|
-
||
|
|
88
|
+
|| BLOCKED_GH_PR_PATTERN.test(cmd);
|
|
67
89
|
}
|
|
68
90
|
function truncate(s) {
|
|
69
91
|
const MAX = 120;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"merge-in-progress-guard.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/ai-hook-rules/src/core/rules/merge-in-progress-guard.ts"],"names":[],"mappings":";;;;AAAA,+CAAyB;AACzB,mDAA6B;AAE7B,0DAAgI;AAGhI,oCAA0C;AAC1C,4CAA4C;AAC5C,0CAAsC;AACtC,kDAAiD;AAEjD,MAAM,8BAA8B,GAAG,0BAA0B,CAAC;AAElE,SAAS,UAAU,CAAC,oBAA4B;IAC5C,OAAO,IAAI,kBAAO,CACd,yEAAyE,EACzE,2GAA2G;UACzG,
|
|
1
|
+
{"version":3,"file":"merge-in-progress-guard.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/ai-hook-rules/src/core/rules/merge-in-progress-guard.ts"],"names":[],"mappings":"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","sourcesContent":["import * as fs from 'fs';\nimport * as path from 'path';\n\nimport { WEBPIECES_TMP_DIR, MERGE_INFO_DIR, MERGE_IN_PROGRESS_FILE, MergeInProgressGuardConfig } 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';\n\nconst DEFAULT_MERGE_COMPLETE_COMMAND = 'pnpm wp-finish-upsert-pr';\n\n// The ENTIRE enforcement surface of this guard, declared up here because the fixHint RENDERS ITSELF\n// from these two lists (see `blockedCommandList`). The hint used to hand-write \"do not run other\n// commands\" — an unbounded claim that forbade the reads, the `git add`, the build and the tests that\n// finishing a merge actually requires, and that survived every edit to these lists. It was also wrapped\n// in an \"Add to memory\" directive, so it outlived the session that read it. Generating the sentence\n// from the code is what stops the two drifting apart again.\nconst BLOCKED_GIT_SUBCOMMANDS: readonly string[] = ['commit', 'push', 'merge', 'rebase'];\nconst BLOCKED_GH_PR_SUBCOMMANDS: readonly string[] = ['create', 'edit', 'merge'];\n\n// webpieces-disable no-function-outside-class -- module-level guard helper, matches the rest of this file\nfunction blockedCommandList(): string {\n return BLOCKED_GIT_SUBCOMMANDS.map((sub: string) => `\\`git ${sub}\\``).join(', ')\n + `, and \\`gh pr ${BLOCKED_GH_PR_SUBCOMMANDS.join('|')}\\``;\n}\n\n// webpieces-disable no-function-outside-class -- module-level guard helper, matches the rest of this file\nfunction fixHintFor(mergeCompleteCommand: string): FixHint {\n return new FixHint(\n 'A merge is in progress and not yet validated — this command is blocked.',\n 'You started a merge but never called the finish-merge command, so a 3-point merge is still in progress.\\n'\n + `Blocked until the merge is validated: ${blockedCommandList()}. That is the whole list;\\n`\n + 'the finish command below does the commit for you.\\n'\n + 'EXPECTED of you right now, and NOT blocked: read the conflicted files, edit them until every\\n'\n + 'conflict marker is gone, `git add` each file you resolve, write the merge explanation for each\\n'\n + 'one, and run the build and the tests. Finishing a merge requires all of those.\\n'\n + 'Then run:\\n'\n + ` ${mergeCompleteCommand}\\n`\n + 'That scans for leftover conflict markers and runs the build; only when green does it commit,\\n'\n + 'unblock commit/push/PR, render the dashboard, and create/update the PR.\\n'\n + 'Add to memory: finish a started merge before beginning other work.',\n );\n}\n\n// Returns the path of the first UNVALIDATED merge marker found, or null. We detect validation\n// by a raw substring (no JSON.parse) so a malformed marker can never crash the guard.\n/**\n * DEPTH-BOUNDED SCAN, not a fixed level. The marker's home has moved twice: it was\n * `merge-info/<feature>/`, then `merge-info/<feature>/merge-<n>/` once each sync got its own run dir,\n * and is now `merge-info/staged/<feature>/merge-<n>/` since merge-info split into staged/merged. This\n * guard was still looking only at the FIRST layout's depth, so it silently found nothing and stopped\n * blocking — a guard failing open without saying so. A bounded walk covers every layout, legacy\n * included, and stops long before it could descend into anything large.\n */\nconst MARKER_SCAN_MAX_DEPTH = 3;\n\n// webpieces-disable no-function-outside-class -- module-level guard helper, matches the rest of this file\nfunction findMarkerUnder(dir: string, depth: number): string | null {\n if (depth < 0 || !fs.existsSync(dir)) return null;\n const marker = path.join(dir, MERGE_IN_PROGRESS_FILE);\n if (fs.existsSync(marker)) {\n const raw = fs.readFileSync(marker, 'utf8');\n if (!/\"validated\"\\s*:\\s*true/.test(raw)) return marker;\n }\n if (depth === 0) return null;\n for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {\n if (!entry.isDirectory()) continue;\n const found = findMarkerUnder(path.join(dir, entry.name), depth - 1);\n if (found !== null) return found;\n }\n return null;\n}\n\n// webpieces-disable no-function-outside-class -- module-level guard helper, matches the rest of this file\nfunction findUnvalidatedMerge(workspaceRoot: string): string | null {\n return findMarkerUnder(path.join(workspaceRoot, WEBPIECES_TMP_DIR, MERGE_INFO_DIR), MARKER_SCAN_MAX_DEPTH);\n}\n\nconst SCANNER = new CommandScanner();\n// Built from BLOCKED_GH_PR_SUBCOMMANDS so the enforcement and the sentence the fixHint prints can\n// never name different commands.\nconst BLOCKED_GH_PR_PATTERN = new RegExp(`\\\\bgh\\\\s+pr\\\\s+(${BLOCKED_GH_PR_SUBCOMMANDS.join('|')})\\\\b`);\n\n// Operations that would let an agent route around the merge gate.\n//\n// Routed through CommandScanner rather than `/\\bgit\\s+merge\\b/`: that pattern matches the read-only\n// `git merge-base origin/main HEAD` (`\\b` sits between `e` and `-`), which appears in this repo's own\n// documented build command — so an in-progress merge used to block a harmless diff-scope lookup.\n// webpieces-disable no-function-outside-class -- module-level guard helper, matches the rest of this file\nfunction isBlockedDuringMerge(cmd: string): boolean {\n return SCANNER.commandInvokesAnyGit(cmd, BLOCKED_GIT_SUBCOMMANDS)\n || BLOCKED_GH_PR_PATTERN.test(cmd);\n}\n\nfunction truncate(s: string): string {\n const MAX = 120;\n return s.length <= MAX ? s : s.slice(0, MAX) + '…';\n}\n\nexport class MergeInProgressGuardRule extends BashRuleBase<MergeInProgressGuardConfig> {\n private readonly mergeCompleteCommand: string;\n\n constructor(config: MergeInProgressGuardConfig) {\n super(config, 'merge-in-progress-guard');\n this.mergeCompleteCommand = config.mergeCompleteCommand ?? DEFAULT_MERGE_COMPLETE_COMMAND;\n }\n\n readonly description = 'Block commit/push/merge/PR while a 3-point merge marker is unvalidated, forcing the merge-complete command.';\n get fixHint(): FixHint { return fixHintFor(this.mergeCompleteCommand); }\n\n check(ctx: BashContext): readonly Violation[] {\n if (!isBlockedDuringMerge(ctx.commandCode)) return [];\n const marker = findUnvalidatedMerge(ctx.workspaceRoot);\n if (!marker) return [];\n return [new V(\n 1,\n truncate(ctx.command),\n 'A merge is in progress and not yet validated — this command is blocked.\\n'\n + `Marker: ${marker}`,\n )];\n }\n}\n"]}
|
|
@@ -33,7 +33,12 @@ const FIX_HINT = new fix_hint_1.FixHint('`git merge` / `git rebase` are never ru
|
|
|
33
33
|
+ '\n'
|
|
34
34
|
+ 'READ the instruct-ai git-workflow doc at the absolute path on the violation line above for the\n'
|
|
35
35
|
+ 'full flow (incl. worktrees).\n'
|
|
36
|
-
|
|
36
|
+
// NOT "add that info to memory": "that info" was the whole git-workflow doc, which is REGENERATED
|
|
37
|
+
// per repo and per version — memorizing it guarantees a stale recall in the next session. Only the
|
|
38
|
+
// invariant is stable enough to persist; the doc must be re-read, never remembered.
|
|
39
|
+
+ 'Add to memory: never run a raw `git merge`/`git rebase` — use this repo\'s gated sync flow, and\n'
|
|
40
|
+
+ 're-read its git-workflow doc each time rather than recalling it (it is regenerated per repo and\n'
|
|
41
|
+
+ 'per version, so a remembered copy will be wrong).');
|
|
37
42
|
// `git merge --abort` / `git rebase --abort|--quit` UNDO an in-progress operation — they cannot create
|
|
38
43
|
// a merge commit or rewrite history, so they cannot violate the fork-point invariant this rule
|
|
39
44
|
// protects. They stay allowed so a repo left mid-operation (e.g. by a human-run rebase) can still be
|
|
@@ -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';\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 + 'Either flow does a 3-point merge (fork-point=A, feature-HEAD=B, main-HEAD=C), which is what\\n'\n + 'keeps PR diffs clean. A raw `git merge`/`git rebase` destroys the fork-point system. The gated\\n'\n + 'commands merge internally as child processes this hook never sees, so they are unaffected by\\n'\n + 'this guard.\\n'\n + '\\n'\n + GUIDANCE.readOnlyChecks().join('\\n') + '\\n'\n + '\\n'\n + 'If you believe a raw merge/rebase is genuinely required, do NOT run it and do NOT work around\\n'\n + 'this guard. STOP and ask the HUMAN to run that exact command themselves — and when you ask,\\n'\n + 'warn them, in these words:\\n'\n + '\\n'\n + ' \"I am asking you to run a raw git merge/rebase. This is almost always the WRONG call —\\n'\n + ' the gated pair for my situation (`wp-start-update` → `wp-finish-update` with no PR, or\\n'\n + ' `wp-start-upsert-pr` → `wp-finish-upsert-pr` when a PR is open) does a 3-point merge and is\\n'\n + ' the correct flow. Please push back and tell me to use the 3-point merge instead, unless you\\n'\n + ' are certain this is a genuine exception.\"\\n'\n + '\\n'\n + 'READ the instruct-ai git-workflow doc at the absolute path on the violation line above for the\\n'\n + 'full flow (incl. worktrees).\\n'\n + 'Add that info to memory so you remember next time.',\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\n// Switches to a branch OTHER than main. `git branch -D <x>` is not a checkout so it does not trip\n// this; `checkout main` and flag-only forms like `checkout -` do not count as a feature switch.\nconst SWITCHES_TO_NON_MAIN = /git\\s+(?:checkout|switch)\\s+(?!main\\b|-\\s|-$)\\S+/;\n\nexport class RedirectHowToMergeMainRule extends BashRuleBase<RedirectHowToMergeMainConfig> {\n private readonly scanner = new CommandScanner();\n private readonly recovery = new TreeRecovery();\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 if (SWITCHES_TO_NON_MAIN.test(ctx.command)) {\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 (/git\\s+(?:checkout|switch)\\s+main\\b/.test(ctx.command)) {\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":"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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';\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 + 'Either flow does a 3-point merge (fork-point=A, feature-HEAD=B, main-HEAD=C), which is what\\n'\n + 'keeps PR diffs clean. A raw `git merge`/`git rebase` destroys the fork-point system. The gated\\n'\n + 'commands merge internally as child processes this hook never sees, so they are unaffected by\\n'\n + 'this guard.\\n'\n + '\\n'\n + GUIDANCE.readOnlyChecks().join('\\n') + '\\n'\n + '\\n'\n + 'If you believe a raw merge/rebase is genuinely required, do NOT run it and do NOT work around\\n'\n + 'this guard. STOP and ask the HUMAN to run that exact command themselves — and when you ask,\\n'\n + 'warn them, in these words:\\n'\n + '\\n'\n + ' \"I am asking you to run a raw git merge/rebase. This is almost always the WRONG call —\\n'\n + ' the gated pair for my situation (`wp-start-update` → `wp-finish-update` with no PR, or\\n'\n + ' `wp-start-upsert-pr` → `wp-finish-upsert-pr` when a PR is open) does a 3-point merge and is\\n'\n + ' the correct flow. Please push back and tell me to use the 3-point merge instead, unless you\\n'\n + ' are certain this is a genuine exception.\"\\n'\n + '\\n'\n + 'READ the instruct-ai git-workflow doc at the absolute path on the violation line above for the\\n'\n + 'full flow (incl. worktrees).\\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 (it is regenerated per repo and\\n'\n + 'per version, so a remembered copy will be wrong).',\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\n// Switches to a branch OTHER than main. `git branch -D <x>` is not a checkout so it does not trip\n// this; `checkout main` and flag-only forms like `checkout -` do not count as a feature switch.\nconst SWITCHES_TO_NON_MAIN = /git\\s+(?:checkout|switch)\\s+(?!main\\b|-\\s|-$)\\S+/;\n\nexport class RedirectHowToMergeMainRule extends BashRuleBase<RedirectHowToMergeMainConfig> {\n private readonly scanner = new CommandScanner();\n private readonly recovery = new TreeRecovery();\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 if (SWITCHES_TO_NON_MAIN.test(ctx.command)) {\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 (/git\\s+(?:checkout|switch)\\s+main\\b/.test(ctx.command)) {\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"]}
|