import { BranchCreationGuardConfig } from '@webpieces/rules-config'; import type { BashContext, Violation } from '../types'; import { BashRuleBase } from '../rule-base'; import { FixHint } from '../fix-hint'; export declare class BranchCreationGuardRule extends BashRuleBase { constructor(config: BranchCreationGuardConfig); readonly description: string; readonly defaultOptions: { subBranchNaming: string; branchFormat: string; maxLocalBranches: number; maxWorktrees: number; }; private readonly worktrees; private readonly mergedBranches; private capCache; private worktreeCapCache; private worktreeAdd; private get branchFormat(); private get subBranchNaming(); private get maxLocalBranches(); private get maxWorktrees(); private freshMainCommand; get fixHint(): FixHint; /** * Strip the parts of a shell command that are DATA rather than executable commands, so the guard * stops reading prose as instructions. * * This guard regex-scans the raw command string and has no notion of quoting, so * `git commit -m "... git checkout -b foo ..."` — or any heredoc commit message that mentions a * branch command — was parsed as an actual branch creation and blocked. That bit three separate * times while building the branch cap, including on the cap's own commit. It matters far more now * that the cap check runs BEFORE the origin/main allow: at the cap, a merely-MENTIONED branch * command would block your commit. * * A quoted span whose content has no whitespace is kept verbatim (it is a single token — the name * in `git checkout -b "dean/foo"`), so quoting a branch name cannot smuggle a creation past the * guard. Anything with whitespace inside quotes is prose, and collapses to a space. */ check(ctx: BashContext): readonly Violation[]; /** * `git worktree add ../dir ` onto a branch whose PR is ALREADY MERGED. * * The count caps never catch this: the command creates no branch, and if you are under the * worktree cap it sails straight through — materialising a fresh directory full of PRE-MERGE * code that the AI will then read, plan from and edit. read-stale-guard blocks the reads and * feature-branch-guard blocks the edits once you are in there, but that is a turn wasted per * tool call. Refuse at the moment of creation instead, using the SAME merged-PR proof the caps * already have precomputed on disk. * * Fails OPEN exactly like both caps: no cache (fresh clone, no `gh`, refresher hasn't run) or an * unparseable command → no opinion. */ private checkWorktreeOntoDeadBranch; private checkReservedSuffix; /** * Both budgets, in the order that produces the most useful complaint. * * Called BEFORE the origin/main allow in check() — `... -b origin/main` is the normal, * always-permitted path, so a cap checked after it would never once fire. * * Worktree cap first: a `git worktree add -b` spends BOTH budgets, and when both are full the * worktree is the thing the command was actually trying to make, so it is the thing to talk about. */ private checkCaps; /** * The cap. Blocks branch #N+1 until already-merged branches are reaped, which is the ONLY thing * keeping the local branch list bounded. * * Fails OPEN when the cache is absent (fresh clone, `gh` unavailable, refresher hasn't run yet): * never block on data we don't have. The detached refresher regenerates it within one hook call, * so the cap starts enforcing on its own. */ private checkBranchCap; /** * The cache, with entries for branches/worktrees that no longer exist dropped. * * Reconciling is the fix for the phantom count: the file is written by a detached refresher and is * DELIBERATELY allowed to go stale, so it keeps naming branches deleted minutes ago and worktrees * already removed. Quoting it verbatim is how the guard announced "8 parked local branches … none of * them are dead" over a repo that had ONE, and then blocked a legitimate `git worktree add` on that * figure. `reconcile` re-checks existence with instant local git reads; it does NOT re-derive any * verdict (that needs the network and belongs in the refresher). * * Its AGE is no longer read here. The cap messages stopped quoting verdict counts entirely, so * there is nothing left for a freshness caveat to qualify — `wp-cleanup` recomputes from scratch. */ private loadReconciledCache; /** * The branch cap, yielding by ONE when the agent is standing on an already-merged branch. * * Two individually-correct guards were composing into a trap: merged-branch-bash-guard blocks * almost all Bash until you get off a merged branch, and the ONLY way off it that guard advertises * is creating a fresh branch — which this cap then refused. Every printed exit led to editing * webpieces.config.json, and that is what an unsupervised agent did. * * One over cap, and only while a merged branch is what is pushing you: the branch about to be * created replaces a branch that is already dead, so the steady-state count does not grow. The cap * still fires on the NEXT creation, so this defers the cleanup by exactly one branch, never skips it. * * Fails toward the strict cap: no cache, a cache for another branch, or a clean branch → no yield. */ private effectiveBranchCap; private currentBranchOrNull; /** * The worktree cap — the second budget. Same gate, same fail-open rule as the branch cap: a * worktree list we cannot classify (no cache on disk) blocks nothing. * * Counts LINKED worktrees only. The primary clone is not a thing anyone can remove, so charging the * budget for it would just silently cost you one worktree. */ private checkWorktreeCap; }