import { ExcludePaths } from '@webpieces/rules-config'; import { CommandScanner } from './command-scan'; /** * One path the denied command NAMED that `excludePaths` already exempts, plus how to reach it. * * Data-only, so a class (per CLAUDE.md). * * `cdDirectory` is the whole point of the class rather than a bare string: it is a directory that * ITSELF satisfies `globMatches` against one of the configured globs, or null when no such directory * exists for this reference. Callers must never invent one — see ExcludedPathEscapeScan for why a * naive `dirname` is wrong exactly when it looks most obviously right. */ export declare class ExcludedPathReference { /** The referenced path, workspace-relative, as it was matched. */ readonly referencedPath: string; /** The `excludePaths` glob that matched it — quoted back so the agent need not re-derive it. */ readonly matchedGlob: string; /** A directory a `cd` may legally land in (matches a glob on its own), or null when none does. */ readonly cdDirectory: string | null; /** `referencedPath` re-expressed relative to `cdDirectory`; '' when there is no `cdDirectory`. */ readonly pathFromCdDirectory: string; constructor(referencedPath: string, matchedGlob: string, cdDirectory: string | null, pathFromCdDirectory: string); } /** * Finds the paths a BASH command names that the top-level `excludePaths` already exempts. * * WHY this exists: the bash guards judge the shell's effective CWD, not the paths in the command, so * `cat .webpieces/tasks.md` at the repo root is DENIED even though that exact file is exempt from * every guard and was reachable, that instant, through Read/Write. Every remedy the deny body printed * was a git state change — strictly more destructive than the one it omitted. This class supplies the * missing sentence; it changes no verdict. * * A token is treated as a candidate path with no filesystem check: whether it exists says nothing * about whether it is exempt, and a stat per token on the blocking hook path is exactly the cost these * guards avoid. Non-path tokens (a `grep` pattern, a `sed` script) simply match no glob. */ export declare class ExcludedPathEscapeScan { private readonly scanner; private readonly workspaceRoot; private readonly effectiveCwd; /** * `effectiveCwd` is the directory the command really runs in, after its own leading `cd` — the * same base ContentReadScan resolves relative operands against. RELATIVE tokens are resolved * there and ABSOLUTE ones normalised straight to workspace-relative, which is how * `cat /abs/path/to/repo/.webpieces/tasks.md` reaches the same verdict as `cat .webpieces/tasks.md`. */ constructor(scanner: CommandScanner, workspaceRoot: string, effectiveCwd: string); /** * Every excluded path this command references, in command order, de-duplicated. * * Scans EVERY segment, not just a leading `cd`: `cat x`, `grep -n foo x`, `sed -n '1,5p' x` and a * path inside an `&&` compound all count, because the agent's next move depends on the file it * wanted, wherever in the line it named it. */ references(command: string, ex: ExcludePaths): readonly ExcludedPathReference[]; private reference; /** * A directory the agent may `cd` into and have the guards actually stand down, or null. * * THE TRAP: the bash path matches the relative cwd with `globMatches`, which compiles * `.webpieces/**` to the anchored `/^\.webpieces\/.*$/` — the `/` is a LITERAL, so the bare * directory `.webpieces` does NOT match its own glob. `cd .webpieces && cat tasks.md` is still * denied. Emitting that would be worse than emitting nothing: it looks authoritative and costs a * turn to disprove. So the directory is only offered when it passes the very same matcher the * runner will use, and the renderer says so plainly when nothing does. */ private cdDirectory; private matchingGlob; /** * Workspace-relative form of a token, or null when it is not a path inside this workspace. * * `~`-rooted and out-of-tree paths are nothing to do with this repo's exclusions, and the * workspace root itself ('') matches no glob by construction. */ private workspaceRelative; /** * Every token that could name a file: each segment's words minus the command word itself and * minus flags. Deliberately generous — a token that is not a path matches no glob, while a token * dropped by a cleverer filter is a file the agent is never told it can read. */ private candidateTokens; } /** * Renders the leading stanza of a bash deny body: the Read/Write escape the agent already has, and — * when one genuinely exists — the `cd` that makes bash itself work. * * It goes at the TOP of the report, above the git remedies, because the agent acts on the first * actionable thing it reads. The live incident this fixes had the agent create a branch in a human's * working tree to record a row in a gitignored scratch file; the cheap, side-effect-free path was one * sentence away and never printed. Returns '' when the command names no excluded path, so a repo * without exemptions — and every command that names nothing exempt — sees no extra noise. */ export declare class ExcludedPathEscapeHint { private readonly scan; /** `effectiveCwd` is the directory the command really runs in — see ExcludedPathEscapeScan. */ constructor(workspaceRoot: string, effectiveCwd: string); render(command: string, ex: ExcludePaths): string; private bashLines; private cdLines; private noCdLines; }