/** * The shim's PreToolUse deny payload, as it prints it on stdout. Named (not an inline literal on the * JSON.parse cast) so the wire shape this testkit depends on is stated once, in one place. */ export declare class HookSpecificOutput { readonly permissionDecisionReason: string; constructor(permissionDecisionReason: string); } /** The decision envelope wrapping {@link HookSpecificOutput}. */ export declare class PreToolUseDecision { readonly hookSpecificOutput: HookSpecificOutput; constructor(hookSpecificOutput: HookSpecificOutput); } /** The outcome of one shim invocation. Data-only → a class, per CLAUDE.md. */ export declare class ShimRun { readonly status: number | null; readonly stdout: string; readonly stderr: string; constructor(status: number | null, stdout: string, stderr: string); /** True when the shim emitted a PreToolUse deny. */ isDenied(): boolean; /** * The deny REASON, parsed out of the PreToolUse JSON. * @throws if this run was not a deny (there is no reason to read). */ denyReason(): string; } /** * ShimTestkit — the shared harness for driving the rendered shim through a REAL /bin/sh. * * Extracted so setup.spec.ts and shim-drift.spec.ts drive the shim the SAME way instead of each * keeping its own copy: the shim's entire contract is "what /bin/sh actually does with it", so two * drifting harnesses would silently become two different contracts. * * An instance class (not module-scope functions) because this is normal source to the linter — only * *.spec.ts is exempt from no-function-outside-class, and a testkit should not need a disable comment * to exist. */ export declare class ShimTestkit { /** A throwaway repo root under the OS temp dir. */ mktmp(): string; /** * Run the rendered shim exactly as Claude Code would: `sh ...`, from a repo cwd, * piping tool-payload JSON on stdin. spawnSync never throws on non-zero exit. */ runShim(root: string, bin: string, stdin: string): ShimRun; /** * A repo root staged so the shim's VERSION-DRIFT check (fault D) fires: an installed guard bin, a * declared @webpieces/pr-gate pin in package.json, and a different installed version in * node_modules. The fake bin prints EXECED, so "the guards actually ran" is observable in stdout — * pass matching versions to stage the no-drift case instead. * * Lives here rather than in one spec because two spec files now need the identical staging, and a * second copy is a second definition of what "drift" means. */ stageDriftRoot(declared: string, installed: string): string; /** * Make `root` a real git repo whose CURRENT BRANCH is `branch`, and return it. * * The fault-D deny asks `git branch --show-current` to decide whether "move forward to what origin * pins" is a legal command here (it is on main, and it destroys the fork point on a feature branch), * so the branch is now an INPUT to the message and has to be stageable. `symbolic-ref` rather than a * commit: `--show-current` answers on an UNBORN branch, so this needs no user identity, no index and * no object write — which is also why a staged root with NO git dir at all keeps answering '' and * lands on the conservative, non-main half. */ stageBranch(root: string, branch: string): string; /** * A repo root that DECLARES @webpieces/ai-hook-rules but has nothing installed — fault X, the * ordinary fresh-clone / new-worktree case whose cure really is `pnpm install`. * * A bare mktmp() root is NOT this case, and that distinction is the whole point of fault U: with no * package.json at all, nothing asks for the package and `pnpm install` is a no-op. Every X spec must * stage the declaration explicitly, or it silently asserts against the U message instead. */ stageDeclaredRoot(declared?: string): string; /** * A throwaway repo root that OWNS a committed shim at shimPath(root) with the given contents * (null = no shim at all, i.e. a fresh clone / global install). Shared by the two spec files that * exercise the committed-shim self-guard, so "a root with a shim in it" has one definition. */ stageCommittedShim(content: string | null): string; /** A Bash tool payload, as Claude Code sends it on stdin. */ bashPayload(command: string): string; /** A Read payload — the L0 allowlist entry that keeps a broken tree inspectable. */ readPayload(filePath: string): string; /** A file-tool payload, for the always-allowed webpieces.config.json recovery target. */ filePayload(toolName: string, filePath: string): string; /** True when `cmd` matches a POSIX ERE, judged by the SAME `grep -E` the shim itself runs. */ ereMatches(ere: string, cmd: string): boolean; /** * Which of `cmds` that same `grep -E` matches — answered in ONE grep process for the whole batch. * * grep is a line matcher, so feeding N commands as N lines asks exactly the question `-Eq` answers * per command; the engine, the ERE and the anchors are unchanged. What changes is cost: a process * spawn is ~5ms on an idle machine but ~100ms when the suite runs projects in parallel, so a * 16-command twin check used to be 16 spawns (~2s of pure spawn latency) for one grep pass. That * is what made these files miss the per-test timeout under load. */ ereMatchSet(ere: string, cmds: readonly string[]): EreMatchSet; } /** The lines `grep -E` matched in one batched run — ask it per command with {@link matched}. */ export declare class EreMatchSet { private readonly hits; constructor(hits: ReadonlySet); matched(cmd: string): boolean; }