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.
282 lines • 16.3 kB
TypeScript
import { type ClaudeCodeDriverOptions } from './driver/index.js';
import { type DriverName } from './driver-cli.js';
import type { ParsedPullRequest } from './turn-gate.js';
import { type FrameworkEvent } from './events.js';
import { type AgentKind } from './agent.js';
import { type AgentLocation } from './agent-location.js';
import { type HandoffLevel } from './handoff-level.js';
import { type AgentSpec } from './agent-spec.js';
import { type FrameworkFileConfig } from './config.js';
import { type ResolvedAgentConfig } from './config-layers.js';
import { type VersionFetcher } from './update-check.js';
import { AgentStore, type StoreFs } from './store/index.js';
import { type OnBeforeMergeableContext } from './on-before-mergeable-prompt.js';
/**
* The default link shown for a live agent: the generic Claude Code entry point,
* surfaced as "Open Claude Code" (not a per-agent live session). We drive Claude
* Code headless, which is not Remote-Controlled, so there is no per-session deep
* link to construct (#214); a cloud run reports its real link through its own
* driver events instead.
*/
export declare const CLAUDE_CODE_SESSION_LIST = "https://claude.ai/code";
/**
* The session link to show for an agent: the generic Claude Code entry point for
* a live agent, nothing for a fake one (which has no real session). Pure, so the
* default is unit-testable without a live agent.
*
* The default is Claude Code's *own* entry point, so it is only honest on a
* Claude run: pointing a Codex session at claude.ai/code offers the user a link
* to somewhere their run isn't. Codex keeps its sessions locally with nothing
* equivalent to open, so another agent gets no default link at all (#542).
*/
export declare function chooseSessionLink(opts: Pick<AgentOptions, 'driver'>, fake: boolean): string | undefined;
/** Where the CLI writes. Injectable so tests capture output. */
export interface CliIO {
out: (line: string) => void;
err: (line: string) => void;
}
export declare function frameworkVersion(): string;
/**
* One agent's resolved configuration, as it reads it itself (D4).
*
* These used to be command-line flags, which made every one of them a human surface as well as
* the dashboard's process API. They arrive as a {@link AgentSpec} now — one JSON blob on a temp
* file — so what is left here is a plain options object with no argv semantics: no tri-state
* `--no-*` spellings, no mutual validation, no help text.
*/
export interface AgentOptions {
/** The `FakeDriver` (`FRAMEWORK_FAKE=1`): an offline stand-in for the agent, for tests and e2e. */
fake: boolean;
/** What the session was asked to do. */
intent: string;
/** Which coding-agent CLI does the work (#542). Default `claude`. */
driver: DriverName;
/** `--run-on <local|actions|web>` (#1050/#610): where the agent executes. `actions` drives it on a
* GitHub Actions runner via ActionsDriver (#934); `web` hands it to a Claude Code cloud session
* via CloudDriver (#610); absent / `local` runs on this device as before. */
target?: AgentLocation | undefined;
cwd?: string | undefined;
/**
* `--run-id <id>` (#736): the id the daemon allocated for this agent before spawning it. Its
* presence says the framework owns this agent's checkout — `--cwd` is a worktree the daemon
* created on a `tf-agent-<id>` branch, which the agent renames once the agent names
* the session. Absent for a plain `framework "..."`, which runs in the user's own checkout.
*/
agentId?: string | undefined;
/**
* `--continue-run` (#762): this agent continues the agent `--run-id` names rather than starting a new
* one. The store reopens that agent's log instead of truncating it, so messaging a stopped agent
* stays one row in the history.
*/
continueAgent?: boolean | undefined;
model?: string | undefined;
/** Continue a finished agent's agent session (#720) — the prompt resumes that conversation (full prior context). Set by the dashboard when you message an agent that has ended. */
resumeSession?: string | undefined;
/** The `tickets/<file>.md` this agent is implementing (#1117). Set by the daemon when it starts a drain agent, from the ticket its queue entry links to; recorded on the agent's meta. */
ticket?: string | undefined;
/** The {@link ticket} is being planned, not implemented (#1327), so the PR title must not inherit its issue as `(fix #42)` (#1334) — the plan's merge would close the issue with the work still undone. */
planAgent?: boolean;
/** No human is watching (#846), so choice gates take the recommended option. */
unattended?: boolean;
scope: 'prototype' | 'full';
/**
* The mode toggles are tri-state (#841): `undefined` is "this agent said nothing", so the repo's
* the-framework.yml decides, while an explicit `--no-*` turns the mode off over the file.
*/
/** Remove the built-in #326 system prompt entirely, keeping the session controls (#314). */
vanilla?: boolean | undefined;
/** Transparent mode (#625): run the wrapped agent fully raw — no framework system prompt, emit
* protocols, consumption guard, dashboard, or TODO loop, so an agent is identical to `claude -p`. */
transparent?: boolean | undefined;
/** In-context directories, added as one `Context:` line (#439). */
context: string[];
/** Fire the built-in on-before-mergeable (#326) prompt when the agent signals setReadyForMerge(), queueing the quality follow-ups as TODO entries. */
onBeforeMergeable: boolean;
/** Give the agent a real browser via chrome-devtools-mcp (navigate, console, network, DOM, screenshot) during the agent (#452). */
browser: boolean;
/**
* How far this session publishes itself when it finishes (#1102/#1216/B5). `undefined` is "this
* session said nothing", so the repo's the-framework.yml decides, and nobody setting it resolves
* to `pr` — which is what makes the handoff zero-config.
*/
handoff?: HandoffLevel | undefined;
todoLoop: boolean;
/** Whether the session records itself to `.the-framework/`. Always true outside tests. */
persist: boolean;
/** {@link AgentSpec.kind} `research`: run the Research preset as a direct prompt (#331). */
research: boolean;
/** {@link AgentSpec.kind} `prompt`: run one prompt verbatim through the direct path (#353). */
directPrompt: boolean;
}
/** What the CLI itself accepts: four options, no verbs (D4). */
export interface CliArgs {
help: boolean;
version: boolean;
/** `--port <n>`: the port the dashboard binds. Default {@link DEFAULT_DAEMON_PORT}; `0` is ephemeral. */
port?: number;
/**
* `--host <addr>` (#1051): the dashboard's bind address. Default loopback; a non-loopback
* address exposes it to the network and gates every route behind the generated shared token.
*/
host?: string;
/**
* `--agent <path>`: run the session described by the JSON spec at `path` (D4). The dashboard's
* process API, not a human option — it is how one session is spawned, and the file is consumed.
*/
session?: string;
error?: string;
}
/**
* Parse argv (without the node/script prefix). Pure and testable.
*
* Four options and nothing else. Everything a session needs used to be a flag here — sixty-seven
* of them, twenty-seven with no human user at all, because the dashboard serialized
* `StartAgentOptions` onto a command line. Those travel as a {@link AgentSpec} now. `--host` and
* `--port` survive because they are the two things a browser cannot be asked and a dashboard
* cannot serve about itself; `--help` and `--version` because a command with options owes the
* user both.
*/
export declare function parseArgs(argv: string[]): CliArgs;
/**
* Read a session's options off the spec the dashboard wrote (D4).
*
* The handoff pair and the mode toggles are left unset when the spec says nothing about them, on
* purpose: that is what lets the repo's `the-framework.yml` decide, with nobody setting them
* resolving to on (#1102/#841). JSON distinguishes "absent" from `false` without needing a
* `--no-*` spelling for each, which is the whole reason this stopped being an argv.
*/
export declare function agentOptions(spec: AgentSpec, env?: NodeJS.ProcessEnv): AgentOptions;
/**
* Resolve the Claude Code driver options for a live session. A session is a headless autonomous
* builder: every turn is `claude -p`, which cannot answer an interactive approval. The driver's
* library default (`acceptEdits`) silently denies installs/builds/tests, so the production-grade
* checklist can never verify the app actually builds/runs (#225). `bypassPermissions`, so the full
* loop runs unattended.
*
* There used to be a `--permission-mode` and a `--dangerously-skip-permissions` to override it.
* Neither had a dashboard control, so with the flags gone (D4) nothing sets them and the mode is
* simply what it always resolved to.
*/
export declare function claudeDriverOptions(): ClaudeCodeDriverOptions;
/**
* The settings the picked driver cannot honor (#542), as lines to print at startup.
*
* A setting that silently does nothing is worse than one that errors. So the session says which
* settings are not in force, rather than letting them imply they are.
*/
export declare function unguardedNotices(opts: Pick<AgentOptions, 'driver' | 'browser'>): string[];
/** Which flow an agent starts under (#1467), recorded on its meta: the direct paths are prompts, a
* build agent is a build. Transparent (#625) routes a build-kind agent through the raw prompt path too,
* so it records as a prompt. */
export declare function agentLogKind(opts: Pick<AgentOptions, 'directPrompt' | 'research'>, transparent?: boolean): AgentKind;
type AgentConfigFlags = Pick<AgentOptions, 'vanilla' | 'transparent' | 'handoff'>;
/**
* Resolve an agent's config over its layers, nearest wins (#841): the agent's flags, then the repo's
* `the-framework.yml`. #800 slots the project-user and global tiers in between and at the end.
*/
export declare function mergeAgentConfig(opts: AgentConfigFlags, file: FrameworkFileConfig): ResolvedAgentConfig;
/**
* The `framework` command. Wires the parsed options into {@link runFramework}
* over a live dashboard + terminal narration, and resolves with an exit code.
* Returns 0 on success, 1 on an agent error, 2 on a usage error.
*/
/**
* Whether this agent can be steered over `.the-framework/control.jsonl` (#344): Stop, a choice pick,
* a live message. True when its own dashboard is up (#427), or when whoever spawned it handed it a
* agent id — the dashboard spawns each session with one in its spec, and steers it
* from its own process.
*
* This used to have a third clause, "a daemon is alive somewhere on this machine", read from a
* global state file. That file is gone with the background daemon (D4b), and it was never a fact
* about *this* run in the first place: it went missing while the daemon was very much alive
* (#922), and every Stop press then landed in control.jsonl and was read by nobody, in silence
* (#905). An agent id holds when a file about another process does not.
*/
export declare function isSteerable(opts: {
persist: boolean;
agentId?: string | undefined;
}): boolean;
/**
* Whether this session should stay open for the user's own messages once it settles (#714).
*
* The other half of #905. Being steerable only means someone *could* reach it; staying open means
* a human is expected to keep talking to it. A headless agent is neither, but it used to inherit the
* chat queue purely because a daemon happened to be alive elsewhere on the machine, and then
* parked forever on a message nothing could send. #714 said as much: "headless / CI runs end when
* done, exactly as today."
*
* So: the dashboard started it (an agent id) and therefore has a UI to carry on the conversation in.
* Stop and gate picks keep working either way.
*/
export declare function isInteractive(opts: {
agentId?: string | undefined;
}): boolean;
/** What the agent journal exposes to the epilogue. See {@link createAgentJournal}. */
export interface AgentJournal {
onEvent: (event: FrameworkEvent) => void;
/** The session name the agent chose via setSessionName() (#326), once it has. */
sessionName: () => string | undefined;
/** The agent signalled setReadyForMerge() this agent (#326). */
sawReadyForMerge: () => boolean;
/** The pull request the agent asked for via an `open-pr` block (#1567/#1618), if any. */
pullRequest: () => ParsedPullRequest | undefined;
/** The agent stopped cleanly (user interrupt / budget cap #322) rather than failed. */
stoppedCleanly: () => boolean;
/** Hold the browser preview's port until the session opens (#829/#813). */
announceBrowserPort: (port: number) => void;
/** The page the browser preview is on (#1455 item 6b): emitted as a `browser` event once a
* session is open, held until then, and re-said after every later `session` so the row
* survives the dashboard's last-session slice. */
announceBrowserUrl: (url: string) => void;
}
/**
* The agent's event sink and the state its epilogue reads. One event arrives and this prints it,
* persists it, tracks the settle flags (#322/#326), renames the framework-owned branch once the
* agent names its session (#736), and re-emits a held browser-stream port right after `session` so
* it lands in the slice the dashboard renders (#829). These jobs sat inline in runCli across six
* mutable locals; the journal is their one owner, and runCli reads the getters. Exported for the
* rename-records-the-branch test (#1277).
*/
export declare function createAgentJournal(deps: {
io: CliIO;
cwd: string;
store: AgentStore | undefined;
agentId: string | undefined;
}): AgentJournal;
export declare function runCli(argv: string[], io?: CliIO): Promise<number>;
/**
* The startup footer every dashboard path prints (#312): where prompts come from, the version,
* and then — once npm answers — whether that version is the latest.
*
* The update line is deliberately not awaited before the static lines. #312 asks for the static
* info first, and the foreground path (bare `framework`) blocks on the server forever, so a line
* printed after the await would never appear there at all. `checkForUpdate` is already forgiving:
* offline or slow (2.5s cap) resolves to 'unknown', which prints nothing.
*/
export declare function printStartupFooter(io: CliIO, opts?: {
fetchLatest?: VersionFetcher;
}): Promise<void>;
/**
* The session spec a spawned on-before-mergeable child runs with (D4). Pure so a test can assert
* it: note it carries **no** `onBeforeMergeable`, which is the recursion guard — a quality pass
* must not trigger its own suite.
*/
export declare function promptAgentSpec(prompt: string, cwd: string, vanilla?: boolean): AgentSpec;
/** How the on-before-mergeable prompt is spawned; injectable so tests observe it without spawning. */
export type PromptRunner = (prompt: string, cwd: string, binPath: string) => Promise<boolean>;
/**
* Fire the built-in on-before-mergeable (#326) prompt after an agent signalled setReadyForMerge(): one
* `framework prompt` child on the same workspace that appends the quality follow-ups to the
* session's TODO file, for the backlog loop (#323/#538) to pick up.
*
* It used to run maintainability, readability and security-audit inline instead, as three
* child runs back to back (#556). Queueing is both what the doc says and the cheaper thing:
* one short turn that writes a few TODO lines, rather than three full preset passes serialized
* on the same git index. Best-effort, like the suite was: a failure is logged, never thrown.
*
* Returns how it went so the caller can emit it as an event (#835); the `io` lines stay for
* a terminal agent, which is the one surface that can still read them.
*/
export declare function runOnBeforeMergeable(cwd: string, binPath: string, io: CliIO, tf: OnBeforeMergeableContext, agent?: PromptRunner, fs?: StoreFs): Promise<'queued' | 'incomplete'>;
export {};
//# sourceMappingURL=cli.d.ts.map