/**
 * Git operations for the agents-cli system repo and package repositories.
 *
 * Handles cloning, pulling, syncing, and inspecting git repos used by
 * the agents version management and plugin/package system. Includes
 * source parsing for GitHub shorthand, SSH, HTTPS, and local paths.
 */
import { SimpleGit } from 'simple-git';
/**
 * Validate that a clone/pull source uses a safe git transport before it is
 * handed to `git`.
 *
 * Git's remote-helper transports (`ext::`, `fd::`, …) execute arbitrary
 * commands at clone time, `file://`/`git://` are unauthenticated, and a source
 * beginning with `-` is parsed by `git` as a command-line flag (option
 * injection). We therefore allow only:
 *   - `https://`                         (encrypted + authenticated)
 *   - `ssh://` and SCP-style `git@host:path` / `host:path`
 *   - local filesystem paths (callers handle these before reaching `git clone`)
 *
 * Pure string inspection — no filesystem or platform calls — so it behaves
 * identically on Linux, macOS, and Windows.
 *
 * @throws Error if the source uses a disallowed transport.
 */
export declare function assertSafeGitTransport(source: string): void;
/**
 * Validate a branch name before it is passed to `git push` / `git pull`.
 *
 * A name beginning with `-` is parsed by git as a command-line option
 * (e.g. `--mirror`, `--receive-pack=…`), not a ref. Pure string check —
 * identical on every OS, no spawn.
 *
 * @throws Error if the name is empty or would be interpreted as a git option.
 */
export declare function assertValidBranchName(branch: string): void;
/**
 * `git push origin <branch>` with option-injection hardening:
 *   1. {@link assertValidBranchName} rejects leading `-`
 *   2. `--` ends option parsing so a hostile ref cannot be read as a flag
 *
 * Prefer this over `git.push(remote, branch)` whenever the branch comes from
 * repo state rather than a hard-coded literal.
 *
 * Pass `targetBranch` to push the local `branch` to a differently-named remote
 * branch (`git push origin <branch>:<targetBranch>`) — used when publishing the
 * working tree to a branch other than the checked-out one.
 */
export declare function pushOrigin(git: SimpleGit, branch: string, targetBranch?: string): Promise<void>;
/** Parsed representation of a git source string (GitHub, generic URL, or local path). */
export interface GitSource {
    type: 'github' | 'url' | 'local';
    url: string;
    ref?: string;
}
/**
 * Parse a source string into a GitSource object.
 *
 * Supported formats:
 *   gh:owner/repo                    -> https://github.com/owner/repo.git
 *   gh:owner/repo@branch             -> https://github.com/owner/repo.git (ref: branch)
 *   owner/repo                       -> https://github.com/owner/repo.git
 *   owner/repo@branch                -> https://github.com/owner/repo.git (ref: branch)
 *   github.com/owner/repo            -> https://github.com/owner/repo.git
 *   github.com:owner/repo            -> https://github.com/owner/repo.git
 *   github.com:owner/repo.git        -> https://github.com/owner/repo.git
 *   git@github.com:owner/repo.git    -> https://github.com/owner/repo.git
 *   https://github.com/owner/repo    -> https://github.com/owner/repo.git
 *   https://github.com/owner/repo.git -> https://github.com/owner/repo.git
 *   /path/to/local                   -> local path
 *   ./relative/path                  -> local path
 */
export declare function parseSource(source: string): GitSource;
/** Clone a remote repo or pull updates if it already exists locally. */
export declare function cloneOrPull(source: GitSource, targetDir: string): Promise<{
    isNew: boolean;
    commit: string;
}>;
/** Clone a repository from a source string, returning the local path and commit hash. */
export declare function cloneRepo(source: string): Promise<{
    localPath: string;
    commit: string;
    isNew: boolean;
}>;
/** Clone a package from a source string into the packages directory. */
export declare function clonePackage(source: string): Promise<{
    localPath: string;
    commit: string;
    isNew: boolean;
}>;
/** Get the short commit hash (8 chars) of the latest commit in a repo. */
export declare function getRepoCommit(repoPath: string): Promise<string>;
/** Compact, self-contained state of a git repo — branch, short HEAD, and a
 *  dirty flag — for cross-device comparison (RUSH-2027). Synchronous and
 *  best-effort: a non-repo or unreadable path yields `null` fields, never a
 *  throw, so a device's `doctor --json` payload always serializes. */
export interface RepoStateSnapshot {
    branch: string | null;
    head: string | null;
    dirty: boolean;
}
/** Read {@link RepoStateSnapshot} for `repoPath` using plumbing commands so the
 *  result is stable across git versions and never mutates the tree. Returns null
 *  when the path is not a git worktree. */
export declare function readRepoState(repoPath: string): RepoStateSnapshot | null;
/**
 * The short HEAD sha of the git repo at `repoRoot` (`git -C <repoRoot>
 * rev-parse --short HEAD`), for provenance — "which commit of this DotAgents
 * repo was this resource/plugin resolved from". `undefined` when `repoRoot`
 * isn't a git repo (or has no commits yet), never a throw.
 *
 * Deliberately synchronous + resolved once and cached: callers (resources.ts,
 * plugins.ts) attach this as a lazy getter on the resolved object, so a
 * consumer that never inspects provenance never pays for the git shell-out —
 * see {@link ResolvedResource.snapshotSha} / {@link DiscoveredPlugin.snapshotSha}.
 */
export declare function resolveSnapshotSha(repoRoot: string): string | undefined;
/** Test seam: clear the memoized snapshot-sha cache between test cases. */
export declare function _resetSnapshotShaCacheForTest(): void;
/**
 * Get the current GitHub username using gh CLI.
 * Returns null if gh is not installed or user is not authenticated.
 */
export declare function getGitHubUsername(): Promise<string | null>;
/**
 * Resolve the GitHub username synchronously for `agents artifacts share`. Order:
 *   1. `AGENTS_SHARE_GITHUB_USER` env override
 *   2. `git config --global github.user`
 * Falls back to null so callers can decide whether to require auth or proceed
 * without a namespace.
 */
export declare function resolveGitHubUsernameSync(): string | null;
/**
 * Resolve the GitHub username asynchronously. Order:
 *   1. `AGENTS_SHARE_GITHUB_USER` env override
 *   2. `gh api user --jq ".login"`
 *   3. `git config --global github.user`
 * Returns null if none succeed.
 */
export declare function resolveGitHubUsername(): Promise<string | null>;
/**
 * Get the remote URL for origin in a git repo.
 */
export declare function getRemoteUrl(repoPath: string): Promise<string | null>;
/** The repo's checked-out branch, or 'main' on a detached HEAD / read failure. */
export declare function getCurrentBranch(repoPath: string): Promise<string>;
/**
 * Canonical `host/owner/repo` form of a git remote, transport-agnostic, so the
 * same repo cloned over SSH vs HTTPS compares equal. Strips protocol, any
 * `user@`, a trailing `.git`, and folds the scp-style `host:owner/repo` colon to
 * a slash. Lower-cased. Used to decide whether an existing checkout is "the same
 * repo" as a requested source before adopting it.
 */
export declare function canonicalGitRemote(url: string): string;
/**
 * True when a git remote URL (any transport form: ssh, https, scp-style) points
 * at the system DotAgents repo — {@link DEFAULT_SYSTEM_REPO}'s current slug OR
 * its `phnx-labs/.agents` rename target (PHNX-3394), which
 * {@link canonicalGitRemote} folds onto it via {@link RENAMED_REMOTE_ALIASES}.
 * Pure string check with no git spawn, so it is unit-testable off a live
 * checkout; {@link isSystemRepoOrigin} reads a dir's origin and delegates here.
 */
export declare function isSystemRepoRemote(remote: string | null | undefined): boolean;
/**
 * True when `remote` is the origin the system repo is EXPECTED to track on this
 * machine, honouring an operator's `AGENTS_SYSTEM_REPO` override.
 *
 * The system repo ships hooks that register as shell `command` strings run on
 * every tool event, and its checkout auto-fast-forwards from origin — so a
 * fast-forward from an origin the operator never chose is remote code execution
 * on the next command that loads a system resource (PHNX-2957). This is the
 * pinning predicate every auto-pull of the system repo gates on: pull only when
 * origin is the canonical {@link isSystemRepoRemote} repo, or the exact
 * `AGENTS_SYSTEM_REPO` the operator pointed at instead. Anything else — a
 * repointed origin, a fork, an unset-then-swapped remote — is refused, not
 * pulled. Pure string check; no git spawn.
 */
export declare function isExpectedSystemRepoRemote(remote: string | null | undefined): boolean;
/** True when two git remote URLs point at the same repo across transport forms. */
export declare function sameGitRemote(a: string | null | undefined, b: string | null | undefined): boolean;
/**
 * Set the remote URL for origin in a git repo.
 */
export declare function setRemoteUrl(repoPath: string, url: string): Promise<void>;
/**
 * Check if a GitHub repo exists.
 */
export declare function checkGitHubRepoExists(owner: string, repo: string): Promise<boolean>;
/** Result of {@link commitAndPush}. */
export type CommitAndPushResult = {
    success: boolean;
    error?: string;
    /** Human detail for success: "already up to date", "pushed abc..def", "committed and pushed …". */
    detail?: string;
    branch?: string;
    committed?: boolean;
    pushed?: boolean;
};
/**
 * Commit (if dirty) and push a repo.
 *
 * Clean tree + local ahead of origin still pushes — "nothing to commit" is not
 * "nothing to push". Reports "already up to date" only when `ahead === 0` and
 * there is nothing to commit.
 *
 * `targetBranch` pushes the working tree to a differently-named remote branch
 * (`<current>:<targetBranch>`) and is reported back as the result `branch`, so
 * callers that print a branch-scoped URL reference where the commit actually
 * landed — not the checked-out branch.
 */
export declare function commitAndPush(repoPath: string, message: string, targetBranch?: string): Promise<CommitAndPushResult>;
/**
 * Check if repo has uncommitted changes.
 */
export declare function hasUncommittedChanges(repoPath: string): Promise<boolean>;
/**
 * Check if a directory is a git repository (**synchronous, root-only**).
 *
 * Tests for a `.git` entry directly under `dir`, so it recognizes only a
 * repository *root* — it returns false inside a subdirectory and for linked
 * worktrees (whose `.git` is a file pointing elsewhere is caught, but a nested
 * cwd is not). This is deliberate: the system-repo sync callers here always
 * pass a known root. For the async, worktree-correct predicate used by teams,
 * see `isGitRepo` in `lib/teams/worktree.ts` (which shells out to
 * `git rev-parse --git-dir`). The two are intentionally **not** merged.
 */
export declare function isGitRepo(dir: string): boolean;
/**
 * Return the absolute path to the git working-tree root containing `dir`.
 *
 * Shells out to `git rev-parse --show-toplevel`, so it resolves correctly from
 * any subdirectory and for linked worktrees (unlike the root-only, synchronous
 * {@link isGitRepo} above). Throws if `dir` is not inside a git repository.
 *
 * Git prints POSIX separators even on Windows (`C:/Users/...`); we fold them to
 * the native separator so the result is a real filesystem path that compares
 * equal to one built with `path.*`. On POSIX this is a no-op.
 */
export declare function getGitRoot(dir: string): Promise<string>;
/**
 * Return the absolute path to the **main** working-tree root for `dir`.
 *
 * Unlike {@link getGitRoot}, this stays correct when `dir` is inside a *linked*
 * worktree: `--show-toplevel` there returns the worktree's own path, but the
 * common git dir (`--git-common-dir`) always points at the primary repo's
 * `.git`, whose parent is the main checkout. Throws if `dir` is not in a repo.
 *
 * Git prints POSIX separators even on Windows (`C:/Users/...`); we fold them to
 * the native separator so the result is a real filesystem path that compares
 * equal to one built with `path.*`. On POSIX this is a no-op.
 */
export declare function getMainRepoRoot(dir: string): Promise<string>;
/**
 * Initialize a git repo in an existing directory.
 */
export declare function initRepo(dir: string): Promise<void>;
/**
 * Clone a repo into an existing directory (for initializing ~/.agents/).
 * This clones into a temp dir, moves .git, then checks out tracked files.
 */
export declare function cloneIntoExisting(source: string, targetDir: string): Promise<{
    success: boolean;
    commit: string;
    error?: string;
}>;
/**
 * Git-back an EXISTING, populated directory from a remote — clone it in place
 * without deleting the local files. Turns a plain `~/.agents` folder (which setup
 * creates as a bare `mkdirSync` and never git-clones — see state.ts ensureAgentsDir)
 * into a real clone of the user's config remote, so `agents repo pull/push` and
 * `agents sync` work on a fresh or Windows machine that never got the manual clone.
 *
 * Unlike cloneIntoExisting (which blindly `checkout .`s over local files), this
 * BACKS UP every tracked file whose local copy differs from the remote — into a
 * sibling `<dir>.pre-adopt-backup/` OUTSIDE the repo so it can't be re-committed —
 * before overwriting it. So a box with local edits to agents.yaml/hooks/rules
 * doesn't silently lose them. Untracked runtime state (.cache/.history/.system,
 * all gitignored) is never touched because `checkout .` only restores tracked paths.
 */
export declare function adoptRepo(source: string, targetDir: string): Promise<{
    success: boolean;
    commit: string;
    backupDir?: string;
    backedUp: string[];
    error?: string;
}>;
/** Read an origin remote URL from a git dir, or null when there is none. */
export declare function readOriginUrl(dir: string): string | null;
/**
 * Persist the user repo's remote URL to device-local runtime state so a future
 * adopt-in-place can recover it after a `.git` loss. Best-effort — a write
 * failure never blocks a sync.
 */
export declare function recordUserRepoRemote(dir: string, url: string): void;
/**
 * Resolve the user config repo's remote URL WITHOUT hardcoding it, for the
 * adopt-in-place self-heal. In priority order:
 *   1. an existing `origin` remote on the dir (a partial repo that kept its
 *      `.git` but drifted) — the same source `agents repo sync` already reads;
 *   2. the `AGENTS_USER_REPO_URL` env override (a fresh/never-cloned box);
 *   3. the device-local record written by a prior healthy sync (a box that lost
 *      its `.git` but kept `.history/` runtime state).
 * Returns null when none is known — the caller then guides the operator to
 * `agents repo pull user <git-url>` instead of crashing.
 */
export declare function resolveUserRepoRemoteUrl(dir: string): string | null;
/**
 * Decide whether a local top-level `agents.yaml` is a stale install stub that
 * should be restored from the committed copy, vs. a legitimately customized file
 * that must be preserved.
 *
 * The stub a partial install leaves behind (createDefaultMeta + a few config
 * writes) is strictly SHORTER than the committed config AND missing whole
 * top-level blocks the committed one carries (`config:` / `hooks:` — the fleet
 * browser hub and hook registrations). Device-specific settings live in
 * `devices/<host>/agents.yaml`, never here, so restoring the top-level file is
 * safe. A file that already carries those blocks (or is longer) is treated as a
 * real local edit and left alone — it surfaces as a modified path instead.
 */
export declare function isStaleAgentsYamlStub(local: string, committed: string): boolean;
export interface AdoptInPlaceResult {
    success: boolean;
    commit: string;
    /** Tracked files that were absent locally and materialized from origin/main. */
    materialized: number;
    /** True when the stale-stub top-level agents.yaml was restored from origin. */
    reconciledAgentsYaml: boolean;
    /**
     * When agents.yaml was reconciled, the path the PRE-reconcile local copy was
     * saved to first — so even a false-positive stub match (e.g. a user who
     * deliberately removed a whole `hooks:`/`config:` block) is recoverable, never
     * silently lost.
     */
    agentsYamlBackup?: string;
    /**
     * Tracked paths whose local copy differs from origin/main and was NOT touched
     * — un-gitignored local edits surfaced rather than silently overwritten.
     */
    localEdits: string[];
    error?: string;
}
/**
 * Adopt an EXISTING, non-git (or origin-less) `~/.agents` directory in place —
 * git-back it against its remote WITHOUT re-cloning and WITHOUT destroying the
 * runtime state it carries (`.cache` / `.history` / `scratch` / `.system`, all
 * gitignored). The self-heal for a partial install (PHNX-3301): the current code
 * hard-fails with "Not a git repo", and the only manual fix is a destructive
 * re-clone that wipes that runtime state.
 *
 * Plumbing-only, so it never trips the fleet git-guard (no `reset` / `checkout
 * <branch>` / `stash` / `git config`):
 *   1. `git init` + point HEAD at `main`.
 *   2. `git remote add origin <url>`.
 *   3. `git fetch origin main`.
 *   4. `git update-ref refs/heads/main origin/main`; set upstream to origin/main.
 *   5. `git read-tree origin/main` — index = origin/main, working tree untouched.
 *   6. Materialize only the tracked files MISSING from the working tree
 *      (`checkout-index` on that set) — existing local files are never overwritten.
 *   7. Reconcile the top-level `agents.yaml`: restore it from origin/main only
 *      when the local copy is a stale stub ({@link isStaleAgentsYamlStub}).
 *
 * Idempotent: a second run finds the remote/refs already present and simply
 * re-materializes nothing. Any tracked path with real local edits is returned in
 * `localEdits` (surfaced, never clobbered).
 */
export declare function adoptRepoInPlace(dir: string, remoteUrl: string): Promise<AdoptInPlaceResult>;
/**
 * Self-heal entry point for the USER config repo: when `dir` is not a git repo
 * (or is a repo with no `origin`), resolve its remote URL and adopt it in place;
 * otherwise return null (nothing to adopt — the normal sync path runs). Returns a
 * failed result carrying `needsUrl` when the URL cannot be resolved, so the
 * caller can print the `agents repo pull user <git-url>` remediation instead of
 * the old "Not a git repo" crash.
 */
export declare function adoptUserRepoIfNeeded(dir: string, opts?: {
    explicitUrl?: string;
}): Promise<(AdoptInPlaceResult & {
    needsUrl?: boolean;
}) | null>;
/**
 * Check if the repo's origin points to the system repo — `phnx-labs/.agents-system`
 * or its GitHub rename target `phnx-labs/.agents` (PHNX-3394), across any
 * transport form. Reads the dir's origin and delegates the match to the pure
 * {@link isSystemRepoRemote}.
 */
export declare function isSystemRepoOrigin(dir: string): Promise<boolean>;
/**
 * Check if repo has uncommitted changes (including untracked files).
 */
export declare function hasLocalChanges(dir: string): Promise<boolean>;
/**
 * Render an absolute path in ~-relative form with forward slashes, matching the
 * way the rest of the CLI prints home-anchored paths (e.g. `~/.agents/.system`).
 */
export declare function displayHomePath(dir: string): string;
/**
 * Pull changes in an existing repo.
 * A dirty working tree no longer refuses outright: a fast-forward that touches
 * no uncommitted path still runs (see `dirtyTreeRefusal`). It refuses when the
 * branch has local commits to rebase, or when an incoming path is also dirty.
 *
 * Strategy (RUSH-2282):
 *   1. Fetch, then compare HEAD to the resolved tracking ref.
 *   2. Clean behind-only (HEAD is an ancestor of tracking) → `merge --ff-only`
 *      against the tracking ref. Never re-enter `git pull` after a bare fetch —
 *      multi-entry FETCH_HEAD (concurrent fetch, multi-branch remote) makes
 *      `git pull --rebase` die with "Cannot rebase onto multiple branches" even
 *      when the checkout is a pure fast-forward.
 *   3. Genuinely diverged (local commits not on tracking) → `rebase` onto the
 *      tracking ref (same outcome as {@link syncRepoGit}, without a second pull).
 */
export interface PullRepoOptions {
    /**
     * `'default-branch-fast-forward'` — strict mode for `projects pull`:
     *   - Blocks immediately if the tree is dirty (no remote read needed).
     *   - Fetches origin, resolves the remote default branch, and refuses if the
     *     current branch is not the remote default.
     *   - Refuses if HEAD is ahead of the upstream (local commits not on remote).
     *   - Fast-forwards only (`merge --ff-only`). NEVER rebases.
     *   - NEVER installs git hook symlinks (read-only model path).
     *
     * `'preserve-local'` (default) — the existing behavior: tolerates dirty trees
     * when the incoming diff does not collide, and rebases a diverged branch.
     */
    mode?: 'preserve-local' | 'default-branch-fast-forward';
}
export declare function pullRepo(dir: string, options?: PullRepoOptions): Promise<{
    success: boolean;
    commit: string;
    error?: string;
    branch?: string;
}>;
/**
 * Rebase a repo onto its remote, optionally pushing local commits back up.
 *
 * The one-repo counterpart to `pullRepo` used by `agents sync <repo>`:
 *   1. `git fetch origin`.
 *   2. Clean tree → `git pull --rebase origin <branch>`: rebase, not merge, so a
 *      local commit lands cleanly on top of upstream with no merge bubble.
 *   3. Dirty tree → fast-forward instead, but only when that is provably safe:
 *      no local commits ahead of upstream, and no incoming path collides with a
 *      dirty one. Otherwise refuse, naming the paths that collided.
 *   4. When `push` is set, `git push origin <branch>` to send local commits up.
 *
 * The branch is read from the repo's current HEAD (falls back to `main`) rather
 * than hardcoded. System repos pass `push: false` — they are pull-only mirrors
 * of the npm-shipped upstream.
 *
 * Why step 3 exists: refusing on *any* dirt strands merged changes indefinitely.
 * A DotAgents repo accumulates unrelated local state — a modified `agents.yaml`,
 * a session's scratch file, a machine-local dotfile — and under the old rule one
 * such file froze that box's layer forever, silently. Measured 2026-08-10: a
 * merged fix could not reach three of three boxes, each blocked by files the
 * incoming commits never touched. A `--ff-only` merge is the safe primitive
 * here: it cannot rewrite local commits, and git itself aborts rather than
 * overwrite a modified file, so the path check is belt-and-braces, not the only
 * guard.
 */
export declare function syncRepoGit(dir: string, opts: {
    push: boolean;
}): Promise<{
    success: boolean;
    commit: string;
    pushed: boolean;
    error?: string;
}>;
/**
 * Get git status for sync display.
 * Returns files categorized by their status relative to HEAD.
 */
export interface GitSyncStatus {
    /** Tracked and unchanged files. */
    synced: string[];
    /** Modified but not staged files. */
    modified: string[];
    /** Untracked files. */
    new: string[];
    /** Staged for commit. */
    staged: string[];
    /** Deleted files. */
    deleted: string[];
}
/** Compute the sync status of a git repo, optionally scoped to a subdirectory. */
export declare function getGitSyncStatus(dir: string, subdir?: string): Promise<GitSyncStatus | null>;
/**
 * Get list of files tracked by git in a directory.
 */
export declare function getTrackedFiles(dir: string, subdir?: string): Promise<string[]>;
/**
 * Check if upstream remote is configured.
 */
export declare function hasUpstreamRemote(dir: string): Promise<boolean>;
/**
 * Get the upstream remote fetch URL, or null if none configured.
 */
export declare function getUpstreamUrl(dir: string): Promise<string | null>;
/**
 * Add or update the upstream remote.
 */
export declare function setUpstreamRemote(dir: string, url: string): Promise<void>;
/**
 * Pull from upstream remote (merge updates from system repo).
 */
export declare function pullFromUpstream(dir: string): Promise<{
    success: boolean;
    commit: string;
    error?: string;
}>;
/**
 * Try to auto-pull a git repo if it's clean and has a remote.
 * Uses --ff-only for safety (fails if diverged instead of creating merge commits).
 * Returns silently on success, returns error message on failure.
 */
export declare function tryAutoPull(dir: string): Promise<{
    pulled: boolean;
    error?: string;
}>;
/** Result of {@link tryAutoPullSystemRepo}. `refused` is set only when the pull
 *  was blocked because origin is not the expected system remote. */
export interface SystemRepoPullResult {
    pulled: boolean;
    error?: string;
    /** True when origin is present but is NOT the expected system remote; no
     *  fast-forward was attempted. `actualRemote` names what was found. */
    refused?: boolean;
    /** The origin fetch URL that was examined (present when a remote exists). */
    actualRemote?: string;
}
/**
 * Auto-pull the system repo ONLY after verifying its origin is the expected
 * system remote (PHNX-2957). The system repo ships hooks that run as shell on
 * tool events, so fast-forwarding it from an unexpected/repointed origin is
 * remote code execution. An origin that fails {@link isExpectedSystemRepoRemote}
 * is REFUSED loud (`refused: true`), never pulled — the canonical system repo
 * (or an operator's `AGENTS_SYSTEM_REPO`) still fast-forwards exactly as before.
 *
 * A system dir with no origin at all is a plain no-op (`pulled: false`), not a
 * refusal — there is nothing to pull from and nothing to distrust.
 */
export declare function tryAutoPullSystemRepo(dir: string): Promise<SystemRepoPullResult>;
/**
 * How many commits `dir`'s checked-out branch is behind its upstream, read from
 * the LAST-FETCHED remote-tracking ref — no network call. Returns null when the
 * dir is not a git repo, has no upstream configured, or git errors.
 *
 * Used by `agents doctor` to flag a source layer (`~/.agents`, `~/.agents/.system`)
 * that is reconciled against stale truth. Staleness relative to origin is a
 * background auto-pull concern; this surfaces the same fact synchronously in the
 * per-version verdict.
 */
export declare function commitsBehindUpstream(dir: string): {
    behind: number;
    branch: string;
} | null;
