framework
Version:
The (AI) Framework: turnkey, zero-config AI orchestration that wraps a coding-agent CLI (Claude Code) as a black box and takes you from an idea to a running app. Vite for AI.
140 lines • 7.9 kB
TypeScript
import type { ProjectSummary } from './dashboard/projects.js';
import type { GitRunner } from './project.js';
/**
* Committing the agent archives the daemon writes (#912/#1179) into the project checkout.
*
* An agent's own worktree sweeps its archive on teardown (`store/worktree.ts`). The main checkout has
* no such path — `install.ts` commits once at activation and nothing after — so an archive written
* there sat as a working-tree change until a human happened to commit it. That is the gap between
* "the history is in Git" and "the history reaches Git by itself".
*
* It used to carry a second pathspec, the per-agent conversation markdown, and the machinery for
* choosing between them: a pathspec matching no file aborts the whole `git add`, so every project
* that had sessions but had never recorded a chat needed the other pattern dropped. With one
* record (B3) there is one pathspec and nothing to choose.
*
* 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 archives 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.
*
* Debounced on an idle window rather than committed per write. Archives land seconds apart, and a
* commit each would bury the project's real history under noise. A poll that sees the same pending
* set twice running treats it as settled and commits the batch; a burst keeps resetting it.
* {@link AgentCommitterOptions.maxWaitMs} caps that, so a project that never goes idle still
* lands instead of being starved forever.
*
* Tolerates not being alone (the question (#605) 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.
*/
/**
* The committed agent 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 ARCHIVE_PATHSPEC = ":(glob).the-framework/*/agents/**";
/** How long writes may keep arriving before the batch 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.
*
* Counted by run, not by file: one archived agent 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 archive 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 instead of naming the files under it, which makes the fingerprint identical whether one
* archive 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 pendingAgents(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 agent archives under `cwd`, scoped to {@link ARCHIVE_PATHSPEC}.
*
* `add` before `commit` because a brand-new archive 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. Nothing pending returns
* early: a pathspec matching no file is a hard error to git, and "no session has been archived here
* yet" is not an error at all.
*
* Never throws: this runs on a background tick with nothing to catch it.
*/
export declare function commitAgents(cwd: string, git?: GitRunner, exists?: PathProbe): Promise<CommitOutcome>;
/** A running committer; call {@link AgentCommitter.stop} to end it. */
export interface AgentCommitter {
stop: () => void;
/** Run one poll now. Exposed so the daemon and tests can drive it deterministically. */
poll: () => Promise<void>;
/**
* Commit every project's pending agent archives 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 startAgentCommitter}. */
export interface AgentCommitterOptions {
/** The projects to sweep each poll (the daemon passes the registry, mapped to summaries). */
projects: () => Promise<ProjectSummary[]>;
/** 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 agent archives, 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. Owns no timer (E4): the daemon's one clock calls {@link
* AgentCommitter.poll}, and the window it debounces on is that cadence.
*/
export declare function startAgentCommitter(opts: AgentCommitterOptions): AgentCommitter;
//# sourceMappingURL=agent-commit.d.ts.map