import type { ProjectSummary } from './dashboard/projects.js';
import type { GitRunner } from './project.js';
/**
 * Committing the conversations the daemon records (#912) into the project checkout.
 *
 * #908 made `.the-framework/conversations/<runId>.md` tracked files, and the paths that already
 * commit pick them up: a run's worktree sweeps its own on teardown (`store/worktree.ts`). The main
 * checkout has no such path — `install.ts` commits once at activation and nothing after — so a
 * conversation held there sat as a working-tree change until a human happened to commit it. That
 * is the one gap between "the chat is in Git" (#857) and "the chat reaches Git by itself".
 *
 * Two rules shape the whole module, both about writing into a repo somebody else is using.
 *
 * Path-scoped, never `git add -A`. The pathspec names the conversations directory and nothing
 * else, the way `queue-promote.ts` names the queue file, so whatever the user has in progress
 * cannot ride along in our commit. A pathspec commit also leaves their index alone: what they had
 * staged is still staged afterwards. Scoped down further at commit time to the pathspecs that
 * actually have something pending — see {@link pathspecsFor}.
 *
 * Debounced on an idle window rather than committed per turn. A chat turn is seconds apart, and a
 * commit each would bury the project's real history under transcript noise. A poll that sees the
 * same pending set twice running treats the conversation as settled and commits the batch; a burst
 * keeps resetting it. {@link ConversationCommitterOptions.maxWaitMs} caps that, so a conversation
 * that never goes idle still lands instead of being starved forever.
 *
 * Tolerates not being alone (the #605 question this waited on). One daemon per machine is the rule
 * today (#393), but the committer never assumes it: a locked index or a rebase/merge in progress
 * means somebody else is mid-operation, so it skips rather than commits into their work, and a
 * failed commit is swallowed and retried on the next window. That way #605's eventual answer about
 * who owns the chat bot does not invalidate any of this.
 */
/** The pathspec the conversations live under. Posix separators: it is a git pathspec, not a path. */
export declare const CONVERSATIONS_PATHSPEC = ".the-framework/conversations";
/**
 * The committed session archives (#1179), under every user's own directory.
 *
 * `:(glob)` magic so the `*` stops at a path separator — a plain pathspec wildcard matches `/` too,
 * and would reach further down `.the-framework/` than this means to.
 *
 * The trailing `/**` is load-bearing, and its absence is silent: glob magic matches the pattern
 * against each file's whole path rather than treating a directory as a prefix, so
 * `.the-framework/*​/sessions` matches no *file* and `git add` fails with "did not match any files"
 * — a committer that commits nothing, every time. Only a real repo shows this.
 */
export declare const SESSIONS_PATHSPEC = ":(glob).the-framework/*/sessions/**";
/**
 * Everything this committer is scoped to. Both are records the daemon writes into the repo and
 * nobody would think to commit by hand: the chat of a run (#908) and the run's own archived history
 * (#1179). They share one debounce because they are written by the same events and a commit each
 * would double the noise in the project's log.
 */
export declare const COMMITTED_PATHSPECS: readonly string[];
/**
 * The subset of {@link COMMITTED_PATHSPECS} that `files` actually has something under.
 *
 * `git add` and `git commit` are all-or-nothing about their pathspecs: one that matches no file
 * aborts the whole command with "pathspec ... did not match any files", so passing both patterns
 * unconditionally means a project that has sessions but has never recorded a conversation — every
 * project, until its first chat — fails to commit and puts that failure in the daemon log on every
 * poll. Nothing to commit under a pattern is the ordinary state, not an error, so the pattern is
 * dropped instead.
 *
 * This covers the empty directory as well as the missing one, and the two fail in different places:
 * `git add` tolerates an existing-but-empty directory, and then `git commit` rejects it, because by
 * then the pathspec has to match a file git knows about.
 */
export declare function pathspecsFor(files: readonly string[]): string[];
/** How often the committer looks for settled conversations. */
export declare const COMMIT_POLL_MS = 30000;
/** How long a conversation may keep changing before it is committed anyway. */
export declare const COMMIT_MAX_WAIT_MS: number;
/** What one attempt did, or why it did nothing. */
export type CommitOutcome = {
    committed: true;
    files: string[];
} | {
    committed: false;
    reason: string;
};
/** Whether a path exists. Injectable so the busy check is testable without real lock files. */
export type PathProbe = (path: string) => Promise<boolean>;
/** A {@link PathProbe} over `fs.access`. */
export declare function nodePathProbe(): PathProbe;
/**
 * The commit message a batch writes. Names what moved, so the log line stands alone.
 *
 * Sessions are counted by run, not by file: one archived run is a `<id>.json` and a `<id>.jsonl`,
 * and "2 sessions" for a single session would be a lie told by the batch's own commit message.
 */
export declare function commitMessage(files: string[]): string;
/**
 * The conversation files with uncommitted changes, as repo-relative paths, sorted so the result is
 * a stable fingerprint the debounce can compare across polls.
 *
 * `--porcelain` v1 is parsed rather than `--short` because its two status columns are fixed-width
 * and its paths are quoted consistently. A rename (`R  old -> new`) reports the destination, which
 * is the path we would commit. Anything unreadable — not a repo, no git — reads as no changes.
 *
 * `-uall` is load-bearing, not a detail. By default git collapses a wholly-untracked directory into
 * one entry (`?? .the-framework/conversations/`) instead of naming the files under it, which makes
 * the fingerprint identical whether one conversation is being written or ten. The debounce compares
 * fingerprints, so without this the idle window could never see a burst and would commit straight
 * through the middle of one. Only a real repo shows this; a per-file fake does not.
 */
export declare function pendingConversations(cwd: string, git?: GitRunner): Promise<string[]>;
/**
 * Why the repo is in no state to be committed into, or `undefined` when it is fine.
 *
 * The git dir is resolved through git rather than assumed to be `<cwd>/.git`, so this is right in a
 * linked worktree, where `.git` is a file pointing elsewhere and the markers live in the real dir.
 */
export declare function gitBusy(cwd: string, git?: GitRunner, exists?: PathProbe): Promise<string | undefined>;
/**
 * Stage and commit the pending conversations under `cwd`, scoped to {@link COMMITTED_PATHSPECS}.
 *
 * `add` before `commit` because a brand-new conversation is untracked, and `git commit -- <path>`
 * only knows paths git already knows. Both are pathspec-scoped, so the staging is as narrow as the
 * commit and the user's own staged work is neither swept in nor disturbed, and both are narrowed to
 * the pathspecs that have something pending ({@link pathspecsFor}) — a pathspec matching nothing is
 * a hard error to git, and "no conversation has been recorded here yet" is not an error at all.
 *
 * Never throws: this runs on a background tick with nothing to catch it.
 */
export declare function commitConversations(cwd: string, git?: GitRunner, exists?: PathProbe): Promise<CommitOutcome>;
/** A running committer; call {@link ConversationCommitter.stop} to end it. */
export interface ConversationCommitter {
    stop: () => void;
    /** Run one poll now. Exposed so the daemon and tests can drive it deterministically. */
    poll: () => Promise<void>;
    /**
     * Commit every project's pending conversations now, skipping the idle window. For shutdown: the
     * daemon is going away, so waiting for quiet would just defer the work to the next boot. Returns
     * how many projects committed.
     */
    flush: () => Promise<number>;
}
/** Options for {@link startConversationCommitter}. */
export interface ConversationCommitterOptions {
    /** The projects to sweep each poll (the daemon passes the registry, mapped to summaries). */
    projects: () => Promise<ProjectSummary[]>;
    /** Poll cadence and idle window, ms. Default {@link COMMIT_POLL_MS}. */
    intervalMs?: number;
    /** Commit anyway once a project has been pending this long, ms. Default {@link COMMIT_MAX_WAIT_MS}. */
    maxWaitMs?: number;
    /** Injectable git (tests). */
    git?: GitRunner;
    /** Injectable existence probe for the busy check (tests). */
    exists?: PathProbe;
    /** Clock, injectable for the max-wait cap (tests). */
    now?: () => number;
    /** Where a committed batch is announced. */
    log?: (message: string) => void;
}
/**
 * Start committing settled conversations, and return the handle that stops it.
 *
 * The idle window is the poll itself: a project whose pending set is byte-identical to the previous
 * poll's has stopped being written to, so its batch is committed. Anything still moving is recorded
 * and reconsidered next time, unless it has been dirty past `maxWaitMs`, which forces it through.
 *
 * Forgiving throughout — a failed project scan, a busy repo or a rejected commit costs one window
 * and is retried, never a throw. Runs immediately, then every `intervalMs`; the timer is unref'd so
 * it never keeps the daemon alive past shutdown.
 */
export declare function startConversationCommitter(opts: ConversationCommitterOptions): ConversationCommitter;
//# sourceMappingURL=conversation-commit.d.ts.map