import { BranchStateGuardConfig } from '@webpieces/rules-config'; import type { FileContext, Violation } from '../types'; import { FileRuleBase } from '../rule-base'; import { FixHint } from '../fix-hint'; /** * Comprehensive "are you on a proper feature branch?" guard — the single rule that blocks edits when * the branch isn't a healthy place to work. Four states, in priority order: * 1. On main (checked SYNCHRONOUSLY here) → block: create a feature branch. * 2. Branch already merged into main (merged PR) → block: your work is in main, branch off fresh. * 3. No fork point with origin/main → block: squash onto a new branch. * 4. origin/main moved & touches your files → block: merge main first. * States 2–4 are PRECOMPUTED into `.webpieces/main-sync-status.json` by the detached refresher, so * this check does NO network git (only a fast local `git rev-parse` for state 1). On every call it * fire-and-forget spawns the refresher so the NEXT call is fresh. Runs in the GUARDS hook (it's a * hookGuard); file-scoped, so only Write/Edit/MultiEdit are guarded — Bash passes through so the AI * can still run `pnpm wp-start-upsert-pr` and the rest of the recovery flow. * * ── STATE 1 IS UNCONDITIONAL, AND `B` NO LONGER TRACKS `E` HERE ────────────────────────────────── * * Earlier docblocks in this family argued that the Bash half of "on `main`" should match this one * exactly — one `git rev-parse`, no cache, so it fires on the first call of a session. That argument * has been split, deliberately, and the two halves now differ: * * `E` (here) blocks on ANY `main`, current or stale. * `B` (stale-main-bash-guard) blocks only once local `main` is KNOWN BEHIND `origin/main`. * * The hazards are not the same hazard. A WRITE on `main` puts work where it cannot be reviewed, cannot * be reverted as a unit, and is one `git checkout` from being lost — none of which depends on how * current `main` is, so nothing about freshness could make this block right or wrong. A READ or a * BUILD on a CURRENT `main` harms nothing at all, and denying it strands the agent immediately after * `pnpm wp-checkout-clean-main` — the very command this repo prescribes — put it there. * * So do NOT "restore the symmetry" by gating this on the cache. That would make writes on `main` * permitted for the whole first call of every session (the cache is populated for the NEXT call), and * permanently in a multi-worktree repo where another tree can hold the refresh lock. That ordering is * the most load-bearing thing in the L2 table, which is why row 5 sits ABOVE the cache divider. */ export declare class FeatureBranchGuardRule extends FileRuleBase { constructor(config: BranchStateGuardConfig); readonly description = "Block edits unless you are on a proper feature branch (not main, not already-merged, forked, in sync with main)."; readonly files: string[]; readonly defaultOptions: { branchNamingConvention: string; hangTimeoutMinutes: number; }; readonly fixHint: FixHint; check(ctx: FileContext): readonly Violation[]; private cacheSummary; /** * The guard could not ESTABLISH the state it judges on, so it judged nothing. * * A sibling of allow() rather than a reason string passed to it, because the difference has to * reach the LOG as a value: `ALLOW_FAIL_OPEN` vs `ALLOW`. It was previously a `' (fail-open)'` * suffix on the free-text reason, which meant an abstention and a real approval were the same * verdict and the abstentions could not be counted — so nobody could tell whether these guards * were protecting anything or quietly standing down. Never block on data you could not * establish; but say out loud, in a field, that you did not establish it. */ private failOpen; private allow; private block; private logDecision; private currentBranch; /** * The Write/Edit half of row 5. Same three points as stale-main-bash-guard's deny, told for THIS * surface: reading main while you plan is legitimate and is not what got blocked; the feature * branch is the unit of work; and a `main` that is behind makes the reads wrong too, so getting * current is not an extra step, it is what makes the plan you are about to write against correct. * * Per-surface, deliberately: this guard sees only Write/Edit, so it says the WRITE was blocked. * (read-stale-guard is the one that can close Read, and only once `main` falls behind.) * * ONE CURE, and it is the dirty-safe one. This half used to print `git pull origin main` and then * "create a feature branch" — but this guard fires on a WRITE, so uncommitted work is the LIKELY * state, and a pull is exactly the form that is not a clean fast-forward there. Its two siblings * (stale-main-bash-guard's rows-6/7 deny, StaleMainMessage.forReads) both prescribe * `git fetch origin main && git checkout -b origin/main`, which fetches AND carries the * uncommitted work onto the new branch. Three halves of one row printing one cure is the point: * a fragile cure is a defect here even when it often happens to work. */ private onMainMessage; private alreadyMergedMessage; private conflictMessage; private noForkPointMessage; }