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.
182 lines • 10.4 kB
TypeScript
import type { DriverSession } from './driver/index.js';
import type { ChoicePick, ChoiceRequest, FrameworkEvent } from './events.js';
export { FLAT_TODO_FILE, TICKETS_DIR, ticketFromQueueEntry, todoPriorityForTicket } from './tickets.js';
/**
* The backlog loop (#323): once the main work settles, consume the agent's own
* TODO backlog one entry per turn until it is empty. The agent writes the
* backlog itself (a very large scope, the Maintenance follow-ups, or the
* [Research] preset's deep-dive picks all append entries); the framework only
* drives: read the file, gate ("start the next item?") when someone can answer,
* prompt the agent to complete exactly one entry and check it off, repeat.
* Termination is Rom's call on the issue: stop when the backlog is empty. The
* dashboard's autopilot auto-accepts the per-item gate, so `[x] autopilot`
* consumes the whole backlog unattended; autopilot off pauses before each entry.
*/
/** A located backlog: its filename and the entries still open. */
export interface TodoBacklog {
/** The backlog filename (workspace-root relative, e.g. `TODO_AGENTS.md`). */
name: string;
/** The open (unchecked) entries, in file order. */
entries: string[];
}
/**
* The open entries of a backlog document: markdown list items (`-`, `*`, or
* `1.`), where a task checkbox counts only while unchecked (`- [ ]`); a checked
* `- [x]` entry is done. Headings, prose, and blank lines are not entries.
*/
export declare function parseTodoEntries(md: string): string[];
/**
* Append an open entry to the queue on the data branch (#1582), creating {@link FLAT_TODO_FILE}
* when the branch has none. Resolves with the file written, or `undefined` if it couldn't be.
*
* This is how a paused agent leaves word to pick itself up again (#529): the
* backlog is already the thing a later agent drains, so a resume note needs no
* machinery of its own. Never throws — it is called while an agent is already
* unwinding, and must not mask the reason it stopped.
*/
export declare function appendTodoEntry(cwd: string, entry: string): Promise<string | undefined>;
/**
* Append an open entry to the queue with a priority, creating {@link FLAT_TODO_FILE} when there
* is none.
*
* The difference from {@link appendTodoEntry} is the priority placement (#1164): a dashboard
* pick (#697) lands in its `## Priority N` section rather than at the end of the file.
*/
export declare function appendFlatTodoEntry(cwd: string, entry: string, priority?: number): Promise<string | undefined>;
/**
* Place an open entry in the backlog's priority section (`prompts/todo_format.md`), creating the
* section when the file has none, and return the new document.
*
* Pure, because the placement is the whole point and it is much easier to pin here than through
* the filesystem. The old behaviour was a plain append, which put a just-queued ticket at the
* *end* of the file — and since the drain preset works "the FIRST open entry" and
* {@link parseTodoEntries} reads in file order, queueing something meant it would be worked last,
* behind everything already there (#1164).
*
* Placement rules, in the order they are tried:
* - a section for this priority already exists: the entry joins the end of it, so a queue keeps
* its arrival order within a priority
* - otherwise the section is created before the first *lower*-priority section, since the format
* sorts high to low
* - with no priority sections at all, it goes above the first heading of any kind: the file's
* own sections are then unranked, and burying a deliberate pick under them is the bug
*/
export declare function insertTodoEntry(md: string, entry: string, priority: number): string;
/**
* Retire one open entry in a queue document: check its box (or give a bare bullet one, checked).
* The same "open" grammar {@link parseTodoEntries} reads (#1164/#1297), the retire half of what
* `queue-promote.ts` did before #1582 made the daemon the queue's one local writer.
*/
export declare function checkOffEntry(md: string, entry: string): string;
/**
* The backlog and its open entries, read off the data branch (#1582) — the queue's one location,
* readable from the project checkout and from any agent worktree alike. Returns `undefined` when
* no queue exists or it has no open entry. Session-scoped `TODO_<slug>.agent.md` files are
* retired (#1369).
*
* `fresh: true` re-fetches the branch first: for a long-lived agent process about to act on the
* queue, where the local ref may trail what other writers pushed meanwhile.
*/
export declare function findTodoBacklog(cwd: string, opts?: {
fresh?: boolean;
}): Promise<TodoBacklog | undefined>;
/**
* Does this session's own backlog still have open work (#1363)?
*
* Reads only `TODO_<SESSION_NAME>.agent.md` — the file the [Research] preset (and a very-large
* scope) has the agent keep for its own session. Never the global `TODO_AGENTS.md`: the queue is
* decoupled from sessions (#1390), and withholding a merge on it would mean auto-merge never
* fires while the project has any backlog at all. `false` on a missing or unreadable file, and on
* a session name that could not name a file — no pendingness known is not pendingness.
*
* TEMPORARY SAFETY BELT, built to be deleted (#1390): the agent's setReadyForMerge() is the
* authorization, and this only catches the agent declaring done while its own session file says
* otherwise. When the agent's word is deemed enough, delete this function and its single call
* site in `maybeAutoHandoff`.
*/
export declare function agentTodoPending(cwd: string, sessionName: string | undefined): Promise<boolean>;
/**
* The ticket the next drain agent will pick up, or `undefined` when there is none (#1117).
*
* "Next" is the first open entry of the flat backlog, because that is what the [Drain queue]
* preset says to work ("the FIRST open entry only") and {@link parseTodoEntries} returns entries
* in file order. Read from the project checkout, the same copy the sweep already consults when it
* decides whether there is anything to drain, so the entry this names is the entry that decision
* was made on.
*
* A best guess by construction: the agent reads its own worktree a moment later, and an entry
* checked off in between would move it on. Being wrong here costs a mislabelled lane on the
* Overview and nothing else — no run is started or steered by this.
*/
export declare function nextQueuedTicket(cwd: string): Promise<string | undefined>;
/**
* The ticket an agent started by hand is about to implement, when that agent is a drain (#1117).
*
* The daemon already does this for the sweep's own drain, off the `drains` flag on the job. An agent
* fired from the dashboard reaches the same start with none of that context, so a hand-fired drain
* showed up working on nothing: the agent implemented the ticket, and the lane it belonged in stayed
* empty. Same read as the sweep's, so both agree on which entry is next.
*
* Undefined for anything that is not a drain, and for a drain over an empty queue. The `read` seam
* is for tests; production always takes the default.
*/
export declare function ticketForPrompt(prompt: string, cwd: string, read?: (cwd: string) => Promise<string | undefined>): Promise<string | undefined>;
/** Why the loop ended. */
export type TodoLoopReason =
/** The backlog is empty (or was never written) — the success case. */
'empty'
/** The user picked "stop" at a per-item gate. */
| 'stopped'
/** Two check-offs in a row could not be written to the data branch. */
| 'stalled'
/** The item cap was reached with entries still open. */
| 'max-items';
/** What {@link runTodoLoop} resolves with. */
export interface TodoLoopResult {
/** Backlog entries worked (turns taken), regardless of outcome. */
completed: number;
/** Why the loop ended. */
reason: TodoLoopReason;
/** The backlog filename, when one was found. */
file?: string;
/**
* An answer marked `stop` (#358) inside an item's turn — a plan the user declined — ends the
* whole session, not just the loop. Distinct from the benign `reason: 'stopped'` per-item gate,
* which only stops draining the backlog. The caller aborts the session on this.
*/
sessionStopped?: boolean;
}
/** Options for {@link runTodoLoop}. */
export interface TodoLoopOptions {
/** The live driver session the agent already owns. */
session: DriverSession;
/** The workspace the backlog lives in. */
cwd: string;
/** Emit the loop's events onto the agent stream. */
emit: (event: FrameworkEvent) => void;
/**
* The interactive gate handler (#304). When wired, the loop pauses before each
* entry ("start the next item?") — the dashboard's autopilot auto-accepts, so
* autopilot off means a human gate per item (#323). Headless runs don't pause.
*/
requestChoice?: ((req: ChoiceRequest) => Promise<ChoicePick>) | undefined;
/** The agent signal; aborting (Stop button / budget cap #322) ends the loop. */
signal?: AbortSignal | undefined;
/** Hard cap on entries worked in one agent. Default {@link DEFAULT_MAX_TODO_ITEMS}. */
maxItems?: number | undefined;
}
/** The default per-agent cap on backlog entries — a backstop beside the budget cap (#322). */
export declare const DEFAULT_MAX_TODO_ITEMS = 25;
/**
* Drive the backlog to empty: read the next open entry (fresh off the data branch), gate, prompt
* the agent to complete exactly that entry, check it off on the data branch, and repeat. The
* check-off is the framework's, not the agent's (#1582): the queue lives on a branch the agent's
* checkout does not hold, and the one writer model keeps every edit going through the same
* funnel. Caps make it safe to leave unattended (#322's concern): the agent's budget/abort
* signal ends any turn, a hard item cap bounds the agent, and two check-offs in a row failing to
* land stop the loop instead of re-working the same entry. A backlog turn is a turn like any
* other: await gates (`showChoices()` / `showMultiSelect()`) and the signals (`showMarkdown()`,
* `setSessionName()`, `setReadyForMerge()`) are honored here too.
*/
export declare function runTodoLoop(opts: TodoLoopOptions): Promise<TodoLoopResult>;
//# sourceMappingURL=todo-loop.d.ts.map