import * as readline from 'node:readline/promises'; export declare const TOTEM_HOOK_MARKER = "[totem] post-merge hook"; export declare const TOTEM_HOOK_END = "[totem] end post-merge"; export declare const TOTEM_CHECKOUT_MARKER = "[totem] post-checkout hook"; export declare const TOTEM_CHECKOUT_END = "[totem] end post-checkout"; export declare const TOTEM_PRECOMMIT_MARKER = "[totem] pre-commit hook"; export declare const TOTEM_PRECOMMIT_END = "[totem] end pre-commit"; export declare const TOTEM_PREPUSH_MARKER = "[totem] pre-push hook"; export declare const TOTEM_PREPUSH_END = "[totem] end pre-push"; /** * Resolve the git hooks directory for `gitRoot`. In a plain checkout this is * `.git/hooks`, but in a linked worktree (or submodule) `.git` is a FILE * (`gitdir: ` pointer) and hooks live under the resolved git dir — shared * across worktrees via `commondir` — so a blind `.git/hooks` join makes * `mkdirSync` crash with ENOTDIR (mmnto-ai/totem#2418; the owner-repo * `tools/install-hooks.js` variant already discriminates). Resolution is * delegated to `git rev-parse --git-path hooks` — git's own worktree/commondir * walk, which also honors `core.hooksPath` — with a filesystem probe as the * offline fallback for the plain-directory layout. Returns null when no hooks * directory can be resolved: the #2410 declared-skip class — callers skip * loudly instead of guessing a path. */ export declare function resolveHooksDir(gitRoot: string): string | null; /** * `resolveGitRoot` for hook-install paths: maps the malformed `.git` pointer * FILE to `{ gitRoot: null, unparseablePointer: true }` instead of letting the * throw reach `handleError` → exit 1 — EVERY hook-install entry point owes the * #2410 declared skip on it, including the hidden legacy `totem install-hooks` * command and the direct `installHooksNonInteractive` API (#2422 review round: * only `hooksCommand` was guarded). All other failures stay fail-loud. * * Exported so the hook-REMOVAL path (`eject`) resolves the git root the SAME way * every install entry point does, instead of hand-rolling a second resolver * (mmnto-ai/totem#2426). */ export declare function resolveGitRootForHookPath(cwd: string): { gitRoot: string | null; unparseablePointer: boolean; }; /** * Determine the package-manager fallback command for invoking totem. * Used inside the runtime resolve block when `totem` is not on PATH. * * Priority: pnpm > yarn > bun > npx (with package.json) > bare totem. */ export declare function getFallbackCommand(cwd: string): string; /** @deprecated Use {@link getFallbackCommand} instead. Kept for backwards compatibility. */ export declare function detectTotemPrefix(cwd: string): string; /** * Build a POSIX shell block that resolves the totem command at runtime. * * Prefers the lockfile-pinned / in-tree build over a volatile ambient global * (mmnto-ai/totem#2053; Tenet 14 — never tie governance to volatile state). Order: * workspace-HEAD > pinned `node_modules/@mmnto/cli` > `pnpm exec` > PATH global > dlx fallback. * Each pinned tier is identity-guarded on the `@mmnto/cli` package (not a bare `totem` bin name, * which a colliding package could shadow). * A stale global shadowing a newer workspace build is the `lesson-1ef06d16` foot-gun this * order prevents. Sets TOTEM_CMD="" when unavailable — callers must guard with * `[ -n "$TOTEM_CMD" ]`. Never exits early, to avoid killing chained user hooks. */ export declare function buildResolveBlock(fallbackCmd: string): string; export declare function buildHookContent(fallbackCmd: string): string; export declare function buildPostCheckoutHookContent(fallbackCmd: string): string; /** * Generate helper shell scripts under `.totem/hooks/` for hook manager integration. * These scripts contain the full guard logic (diff checks, null-SHA guards) that * bare inline commands would skip. */ export declare function generateHookHelpers(gitRoot: string, fallbackCmd: string, options?: { tier?: 'strict' | 'standard'; }): void; export declare function installPostMergeHook(cwd: string, rl: readline.Interface, options?: { tier?: 'strict' | 'standard'; /** Threaded from init's single non-interactive predicate (mmnto-ai/totem#2601); * falls back to the bare TTY probe for the standalone `install-hooks` caller. * A prompt raised without a TTY never settles — init dies mid-run with its * mutations already landed. */ interactive?: boolean; }): Promise; export declare function buildPreCommitHook(tier?: 'strict' | 'standard'): string; export declare function buildPrePushHook(fallbackCmd: string, tier?: 'strict' | 'standard'): string; /** * Install a single git hook with idempotency and chain preservation. * Returns the action taken. * * When the hook already carries the totem marker and `force` is not set, a * totem-OWNED whole file whose content has drifted from the regenerated canonical * is repaired in place (`overwritten`) — this makes bare `totem hook install` * actually fix a stale hook, so the doctor's drift remediation is truthful * (mmnto-ai/totem#2138). A user hook with an appended totem block is left untouched * (`exists`); overwriting it still requires `--force`. `endMarker` bounds the totem * region so appended user content downstream of it is never clobbered; all four hook * templates now emit one. Drift-repair fires only when the caller threads the end * marker AND the on-disk hook carries it — a legacy pre-end-marker hook declines to * `exists` and takes one `totem hook install --force`. */ export declare function installGitHook(hooksDir: string, hookName: string, hookContent: string, marker: string, force?: boolean, endMarker?: string): 'installed' | 'exists' | 'appended' | 'skipped-non-shell' | 'overwritten'; export interface EnforcementHookResult { preCommit: 'installed' | 'exists' | 'appended' | 'skipped' | 'skipped-non-shell' | 'overwritten'; prePush: 'installed' | 'exists' | 'appended' | 'skipped' | 'skipped-non-shell' | 'overwritten'; } /** * Install pre-commit (block main) and pre-push (totem lint) hooks. * Respects hook managers by printing guidance instead of writing raw hooks. * Returns actions taken for reporting in init summary. */ export declare function installEnforcementHooks(cwd: string, rl: readline.Interface, options?: { tier?: 'strict' | 'standard'; /** See installPostMergeHook — the same non-interactive predicate, threaded * (mmnto-ai/totem#2601). */ interactive?: boolean; }): Promise; export declare function installHooksCommand(): Promise; export interface HooksCommandResult { preCommit: 'installed' | 'exists' | 'appended' | 'skipped-non-shell' | 'overwritten'; prePush: 'installed' | 'exists' | 'appended' | 'skipped-non-shell' | 'overwritten'; postMerge: 'installed' | 'exists' | 'appended' | 'skipped-non-shell' | 'overwritten'; postCheckout: 'installed' | 'exists' | 'appended' | 'skipped-non-shell' | 'overwritten'; } /** * Non-interactive hook installer for `totem hooks` and `prepare` scripts. * Installs pre-commit, pre-push, and post-merge hooks without prompting. */ export declare function installHooksNonInteractive(cwd: string, force?: boolean, options?: { tier?: 'strict' | 'standard'; }): HooksCommandResult | null; /** * Check that all Totem hooks are installed. Returns true if all present. */ export declare function checkHooksInstalled(cwd: string): boolean; /** * CLI entrypoint for `totem hooks [--check]`. */ export declare function hooksCommand(opts: { check?: boolean; force?: boolean; strict?: boolean; standard?: boolean; }): Promise; /** * The action taken on one managed session-hook artifact by * {@link regenerateManagedSessionHooks}: * - `exists` — present, marker-headed, already byte-identical to canonical (no write). * - `overwritten` — regenerated: a bare bounded drift-repair OR a `--force` overwrite. * - `declined` — marker present but the region is NOT bounded-owned (legacy file * with no end marker, or user content after the end marker) and no * `--force`: left untouched, takes one `totem hook install --force`. * - `skipped` — a user-owned file carrying NO totem marker at all: never touched, * not even under `--force`. */ export type ManagedSessionHookAction = 'exists' | 'overwritten' | 'declined' | 'skipped'; export interface ManagedSessionHookResult { /** Repo-relative path of the artifact. */ file: string; action: ManagedSessionHookAction; } /** * Walk the {@link MANAGED_SESSION_HOOKS} roster and regenerate the whole-file * session-hook artifacts (`.claude/hooks/*.cjs`, `.gemini/hooks/*.js`) that EXIST * under `cwd`, applying the #2406 bounded-ownership semantics generalized to the * JS/CJS hook family: * * - Missing file → not created (creation is `totem init`'s job); omitted * from the results entirely. * - Marker + identical → `exists` (already current). * - Marker + drifted, bounded totem-owned whole file → bare drift-repair * (`overwritten`), or `--force` → `overwritten`. * - Marker + drifted, unbounded (legacy no-end-marker / trailing user content): * bare → `declined`; `--force` → `overwritten`. * - Marker does not OPEN the file (no marker, or a merely-quoted marker) → * `skipped` even under `--force` (never clobber a * user-owned file). * * Regenerate-only-if-present, single-writer per invocation. A write failure * (perms/FS) PROPAGATES (Tenet 4 — a hook the tool cannot write must fail loud, * never silently report success), mirroring `installGitHook`. */ export declare function regenerateManagedSessionHooks(cwd: string, force?: boolean): Promise; export interface GeminiHookMigrationResult { /** Repo-relative legacy path acted on. */ file: string; /** `migrated` — successor materialized + legacy removed; `declined` — drifted * unbounded, awaits `--force`; `skipped` — user-owned file, never touched. */ action: 'migrated' | 'declined' | 'skipped'; /** Why a non-`migrated` action was taken, when the bare action is not enough. * `user-owned-successor` / `drifted-successor` name a block caused by the file at * the SUCCESSOR path (mmnto-ai/totem#2488): no Totem marker there — the migration * never overwrites a user's successor (even under `--force`); or marker-headed but * unbounded (user content past the end marker) — awaits `--force`. Both files stay * either way. `unreadable-legacy` / `unreadable-successor` name a candidate at the * respective path that could not be READ (a directory sharing the name, a * permissions failure): unprovable ownership is the skip arm, never a crash * (mmnto-ai/totem#2601), and BOTH summary consumers must render it as unreadable — * not as "user-owned", an ownership claim no read ever established. The `totem * init` path surfaces `migrated` and the unreadable reasons; the rest are disclosed * on the `totem hook install` summary path (what `prepare` runs). */ reason?: 'user-owned-successor' | 'drifted-successor' | 'unreadable-legacy' | 'unreadable-successor'; } /** * Rename-migrate the whole-file artifacts in {@link LEGACY_MANAGED_SESSION_HOOKS}: * materialize each `successorRel` from canonical content and remove the bounded * totem-owned `legacyRel`. Ownership gate mirrors {@link regenerateManagedSessionHooks}: * * - Legacy missing → nothing to migrate (omitted from results). * - Legacy present but UNREADABLE → `skipped` + `reason: 'unreadable-legacy'` * (mmnto-ai/totem#2601): a directory sharing * the name reads EISDIR, and an ownership gate * that crashes init mid-run is worse than one * that declines and says so. * - Marker does not OPEN the file → `skipped` even under `--force` (a user file * that merely shares the name is never touched). * - Marker + bounded totem-owned whole file, OR `--force` → materialize successor + * remove legacy → `migrated`. * - Marker + drifted/unbounded (trailing user content), no `--force` → `declined`. * - USER-OWNED file at `successorRel` → `skipped` + `reason: 'user-owned-successor'`, * even under `--force`: the ownership gate * protects the successor path too, so a * hand-authored successor (e.g. an ESM rewrite * of the briefing) is never clobbered by the * canonical write; both files stay in place. * - Marker-headed but DRIFTED-UNBOUNDED file at `successorRel`, no `--force` * → `declined` + `reason: 'drifted-successor'` * (same bar as the legacy arm and as * regenerateManagedSessionHooks: a bare run * repairs only bounded totem-owned files; * `--force` overwrites any marker-headed one). * * A write/remove failure PROPAGATES (Tenet 4 — a migration the tool cannot complete * fails loud, never silently reports success), matching the drift-repair path. */ export declare function migrateLegacyGeminiHooks(cwd: string, force?: boolean): Promise; /** * Idempotently rewrite an existing `.gemini/settings.json` hook registration whose * command references the legacy `BeforeTool.js` basename to the `.cjs` successor. * * Rewrite-if-present ONLY: never CREATES a registration (arming the hook is the * deferred adoption slice — #2478 OPTION 1), and fail-soft on unreadable/malformed * JSON (preserve user content, mirroring `scaffoldMcpConfig`). Basename matching is * separator- and prefix-agnostic, so `$GEMINI_PROJECT_DIR/.gemini/hooks/BeforeTool.js`, * `node .gemini/hooks/BeforeTool.js`, and an absolute path all migrate. The rewrite is * scoped to the `hooks` subtree, and the file is only rewritten when a change occurred * (no churn / reformat on a no-op). */ export declare function migrateGeminiHookRegistration(cwd: string): { changed: boolean; err?: string; }; /** * Silently upgrade the pre-push hook if it was installed by Totem but uses * an old format (flag-checking or command-executing) instead of the new * stateless format that runs verify-manifest + lint directly. * * Returns true if the hook was upgraded, false otherwise. */ export declare function upgradePrePushHookIfNeeded(cwd: string): boolean; //# sourceMappingURL=install-hooks.d.ts.map