Documentation
¶
Overview ¶
Package cli parses foundry's command-line arguments and drives an Act from production through the human trust boundary: it runs the Engine, seeks an Authority's approval, and — only once accepted — applies the Outcome and records the Act.
Index ¶
- Variables
- func BuildVersion() string
- func CloseReporter(r engine.Reporter) error
- func IsInteractiveTerminal(in io.Reader, out io.Writer) bool
- func NewReporter(out io.Writer) engine.Reporter
- func ParseArgs(args []string) (intent string, repoPath string, err error)
- func PromptForApproval(in io.Reader, out io.Writer, act *domain.Act) (authority string, approved bool, err error)
- func ReadInteractiveLine(prompt string, candidates []CommandCandidate, history *PromptHistory) (string, error)
- func TraceExecutor(e engine.Executor, r engine.Reporter)
- func TraceExecutorTo(e engine.Executor, sink func(string))
- func Usage() string
- type CLI
- func (c *CLI) Do(ctx context.Context, intent string, repoPath string) error
- func (c *CLI) Log(ctx context.Context, limit int) error
- func (c *CLI) Replay(ctx context.Context, actID string, verifier engine.Verifier, ...) error
- func (c *CLI) Resume(ctx context.Context, actID string, checkpoints checkpointLoader, ...) error
- func (c *CLI) Show(ctx context.Context, actID string) error
- type CommandCandidate
- type ExecutorTracer
- type InteractiveAuthority
- type InteractiveRenderer
- type ProgressReporter
- func (p *ProgressReporter) BudgetExceeded(reason string)
- func (p *ProgressReporter) Close() error
- func (p *ProgressReporter) ContextGathered(entries, bytes int)
- func (p *ProgressReporter) Executed(iteration int, actualCostUSD *float64)
- func (p *ProgressReporter) Executing(iteration int)
- func (p *ProgressReporter) ExecutorFinished(stepID, executor string, elapsed time.Duration, err error)
- func (p *ProgressReporter) ExecutorLine(s string)
- func (p *ProgressReporter) ExecutorStarting(stepID, executor string, timeout time.Duration)
- func (p *ProgressReporter) Gathering()
- func (p *ProgressReporter) IntentDeclared(text string)
- func (p *ProgressReporter) ModelFailover(stepID, from, to string, class model.FailureClass, cause error)
- func (p *ProgressReporter) RepairSkipped(reason string)
- func (p *ProgressReporter) Repairing(reason string)
- func (p *ProgressReporter) StepStarting(attempt, index, total int, stepID, kind string)
- func (p *ProgressReporter) Verified(iteration int, judgment *domain.Judgment)
- func (p *ProgressReporter) Verifying(iteration int)
- type PromptHistory
- type TraceRelay
Constants ¶
This section is empty.
Variables ¶
var ErrHelp = errors.New("cli: help requested")
ErrHelp indicates the caller asked for usage information rather than requesting work.
var ErrPromptEOF = errors.New("cli: interactive prompt ended")
ErrPromptEOF is ReadInteractiveLine's signal that the user ended input at the prompt (Ctrl-C or Ctrl-D) rather than submitting a line — the same "stop the session" meaning session/repl.go's Run already gives a plain io.EOF from its non-interactive fallback path.
Functions ¶
func BuildVersion ¶
func BuildVersion() string
BuildVersion reports the running binary's real build identity — its VCS revision, truncated the way `git rev-parse --short` would, with a "+" marker if the working tree had uncommitted changes at build time — rather than a fabricated semantic version. Foundry has no release process or version scheme yet (pre-1.0, ADR-0009): claiming a "v0.x.y" would assert a promise this project does not make. Returns "dev" when no VCS revision is embedded (e.g. `go run`, or a binary built outside a git checkout).
func CloseReporter ¶
CloseReporter releases whatever r holds open, if r holds anything open at all — both concrete Reporters NewReporter can return implement io.Closer, but a Reporter is not required to, so a non-Closer is simply left alone.
func IsInteractiveTerminal ¶
IsInteractiveTerminal reports whether both in and out are connected to a real terminal — the gate session.NewSession uses (ADR-0012) to decide between ReadInteractiveLine's rich, completion-aware editor and the plain line-at-a-time read every existing test exercises. Both sides must be real: a redirected/piped input with a real terminal output (or vice versa) is not an interactive session.
func NewReporter ¶
NewReporter returns the engine.Reporter a composition root should attach for one Engine run: always a human-facing ProgressReporter writing to out, and — only when FOUNDRY_LOG is set in the environment — also a structured engine.SlogReporter emitting JSON lines to stderr, fanned out via engine.MultiReporter. This is the one place that decision is made, so cmd/foundry/commands/do.go and session/run_pipeline_command.go both stay in sync without duplicating the environment check. The returned Reporter may hold a goroutine open for the duration of one Act (ProgressReporter's elapsed-time heartbeat). A caller whose process outlives the Act — the interactive session, which runs one Act per slash command — must pass it to CloseReporter when the Act is done; a one-shot `foundry do` process may simply exit.
func ParseArgs ¶
ParseArgs parses arguments for the `do` command: a required positional intent and a required --repo flag, in either order. The standard library flag package is not used here because it stops parsing flags at the first positional argument, which would reject the documented invocation order `foundry do "<intent>" --repo <path>`.
func PromptForApproval ¶
func PromptForApproval(in io.Reader, out io.Writer, act *domain.Act) (authority string, approved bool, err error)
PromptForApproval shows act's proposed patch and machine verdict on out, then reads a yes/no decision from in. On acceptance it returns the accountable Authority (the current user) and approved=true; otherwise it returns approved=false. This is the trust boundary: nothing is applied or recorded unless an Authority accepts.
A patch longer than pagerThresholdLines is piped through $PAGER (see maybePage) when out is an interactive terminal, so a large diff doesn't scroll past what a human can review before the y/n prompt appears — exactly the review surface this trust boundary depends on actually being readable.
If in is already a *bufio.Reader, PromptForApproval reads directly from it instead of wrapping it in a new bufio.Reader. This matters the moment a caller issues more than one approval prompt over the same underlying stream (an interactive session, one prompt per slash command): bufio.NewReader's first Read greedily drains everything currently available from the underlying reader into its own buffer, so wrapping anew on every call silently discards every byte after the first line — the caller's *next* read (whether the next approval prompt or the next REPL line) would see EOF even though more input was typed. Wrapping only once, by the one caller who owns the stream for its whole lifetime, avoids that. The one-shot CLI (which calls this exactly once per process) is unaffected either way.
func ReadInteractiveLine ¶
func ReadInteractiveLine(prompt string, candidates []CommandCandidate, history *PromptHistory) (string, error)
ReadInteractiveLine collects one line of input from a real terminal via bubbletea's raw-mode line editor: a full, arrow-navigable dropdown menu of every candidate whose name has the current input as a prefix — opening the moment the line starts with "/", filtering as the user keeps typing, closing once the line no longer looks like a bare command name (a space, or no longer a "/" prefix) — plus Up/Down arrow-key recall through history when the menu isn't showing. It owns the terminal only for the duration of one line — bubbletea restores cooked mode before Run returns — so a caller's own subsequent blocking reads (e.g. an approval prompt reading from the same *bufio.Reader over os.Stdin) behave exactly as they did before this existed.
Returns ErrPromptEOF on Ctrl-C or Ctrl-D instead of the submitted line.
func TraceExecutor ¶
TraceExecutor wires e's live output into r's narration, if FOUNDRY_VERBOSE is set, e can stream at all, and r can render a line. Called by a composition root once per Executor it builds — every condition failing is an ordinary no-op, never an error: this is additional visibility, and an Act must not fail because it could not be narrated.
func TraceExecutorTo ¶
TraceExecutorTo is TraceExecutor against an explicit sink, for a caller whose Executors outlive any single Reporter — see TraceRelay. A nil sink traces nothing.
Types ¶
type CLI ¶
type CLI struct {
// contains filtered or unexported fields
}
CLI drives the Act lifecycle across the trust boundary.
func NewCLI ¶
NewCLI wires an Engine, a Recorder, an input source (for approval), and an output writer into a CLI.
func (*CLI) Do ¶
Do produces an Act for intent, presents it to the Authority, and — only if accepted — applies the patch to the repository and records the Act. A declined Act is neither applied nor recorded (recording rejections is deferred to M0.2).
A Pipeline that declares its own approve/apply/record Steps (RFC-0002 §9 Phase 4) has already sought approval, applied the patch, and persisted the Act inside c.engine.Run, via whatever Authority/Applier/Checkpointer the caller gave the Engine (typically an InteractiveAuthority wrapping this same package's PromptForApproval, and a workspace.GitApplier) — Do detects each of those from the returned Act (hasStepKind) and never repeats the work. A Pipeline that declares none of them behaves exactly as before RFC-0002 §9 Phase 4 existed: Do prompts, applies, and records itself.
func (*CLI) Log ¶
Log writes the most recent limit recorded Acts, newest first, one per line: ID, creation time, verdict, and Intent.
func (*CLI) Replay ¶
func (c *CLI) Replay(ctx context.Context, actID string, verifier engine.Verifier, workspacePath string) error
Replay re-runs verifier against the recorded Act identified by actID — never calling an Executor again — and writes whether each verify Step reproduces its recorded Judgment. This is read-only, like Show and Log: it never mutates the Record, and it is a same-version replay guarantee only (replay/replay.go's package doc).
func (*CLI) Resume ¶
func (c *CLI) Resume(ctx context.Context, actID string, checkpoints checkpointLoader, repoPath string) error
Resume continues the Act checkpointed under actID — left behind by an attempt that was interrupted before reaching a terminal Judgment — via c.engine.Resume, then drives the same trust boundary Do does: approval, apply, and recording, whichever of those a Pipeline's own declared Steps did not already handle inside Resume.
type CommandCandidate ¶
CommandCandidate is one slash command ReadInteractiveLine can offer in its "/"-triggered dropdown menu: its full "/name" form (including the leading slash) and one-line description, both shown side by side in the menu exactly as /help already lists them (session.CommandInfo).
type ExecutorTracer ¶
type ExecutorTracer interface {
SetProgress(sink func(string))
}
ExecutorTracer is the optional seam an Executor may implement to stream its own progress somewhere while it runs. executor/claude implements it; an Executor that doesn't is simply never traced.
type InteractiveAuthority ¶
InteractiveAuthority implements engine.Authority by prompting a human on Out and reading their decision from In — the same PromptForApproval CLI.Do already used after Engine.Run returned, now callable from inside a Pipeline run so an approve Step can seek approval mid-run instead of only at the very end (RFC-0002 §9 Phase 4).
type InteractiveRenderer ¶
type InteractiveRenderer struct {
// contains filtered or unexported fields
}
InteractiveRenderer renders the REPL-level chrome around an interactive session — the prompt, a startup banner, and informational or error messages between slash commands. It is deliberately separate from ProgressReporter (progress.go), which narrates one Act's Engine-driven lifecycle: InteractiveRenderer never touches an Act, a Judgment, or engine.Reporter at all.
func NewInteractiveRenderer ¶
func NewInteractiveRenderer(out io.Writer) *InteractiveRenderer
NewInteractiveRenderer returns a renderer that writes to out, colored when out is an interactive terminal — the same detection ProgressReporter already uses.
func (*InteractiveRenderer) Banner ¶
func (r *InteractiveRenderer) Banner(root string, initialized bool)
Banner writes a one-time startup panel naming the project root a session is running against, this build's real version identity (BuildVersion — never a fabricated semantic version, ADR-0012), and whether /init has already scaffolded this project (initialized) — real, disk-derived facts, not decorative chrome. Piped or redirected output (r.color false, e.g. every existing test) gets the same three facts as plain lines, with no border or ANSI codes at all — the same "plain when non-interactive" rule renderDiff/renderVerdict already follow.
func (*InteractiveRenderer) Error ¶
func (r *InteractiveRenderer) Error(err error)
Error writes err as a single line, tinted red when color is enabled.
func (*InteractiveRenderer) Info ¶
func (r *InteractiveRenderer) Info(msg string)
Info writes a plain informational message, one line.
func (*InteractiveRenderer) Prompt ¶
func (r *InteractiveRenderer) Prompt()
Prompt writes the input prompt, without a trailing newline — the REPL's next read is expected to appear on the same line.
type ProgressReporter ¶
type ProgressReporter struct {
// contains filtered or unexported fields
}
ProgressReporter narrates an Act's lifecycle to out as the Engine runs it: gathering, each Execute/Verify round, and repair. It satisfies engine.Reporter, which is telemetry-only — a ProgressReporter only describes what the Engine already decided, never influences it (I1).
func NewProgressReporter ¶
func NewProgressReporter(out io.Writer) *ProgressReporter
NewProgressReporter returns a Reporter that writes human-readable progress lines to out, colored when out is an interactive terminal.
func (*ProgressReporter) BudgetExceeded ¶
func (p *ProgressReporter) BudgetExceeded(reason string)
func (*ProgressReporter) Close ¶
func (p *ProgressReporter) Close() error
Close stops any heartbeat still running and waits for its goroutine to exit. Every event method already stops the current heartbeat, so this matters for the one case none of them cover: a call that ends the whole run without a further event — a Verifier returning an infrastructure error, which ends the Act from inside runSteps. In a long-lived process (the interactive session's REPL, which reuses one Reporter per Act) a heartbeat left running would print elapsed-time lines over the next prompt; in a one-shot `foundry do` it would merely outlive its purpose. Close is idempotent and always returns nil — the io.Closer signature exists so engine.MultiReporter can fan Close out without knowing which concrete Reporters hold anything open.
func (*ProgressReporter) ContextGathered ¶
func (p *ProgressReporter) ContextGathered(entries, bytes int)
ContextGathered reports the size of what was assembled around the Intent. This is most of what an Executor is about to read, and therefore most of the explanation for how long the call takes — a number worth seeing before the wait, not after it.
func (*ProgressReporter) Executed ¶
func (p *ProgressReporter) Executed(iteration int, actualCostUSD *float64)
Executed is intentionally silent: ADR-0011's actual-cost signal is reported Evidence for `foundry show`/FOUNDRY_LOG, not live human narration — ProgressReporter's own progress lines are unchanged.
func (*ProgressReporter) Executing ¶
func (p *ProgressReporter) Executing(iteration int)
Executing names the round without naming a vendor: which Executor is actually about to run is reported by ExecutorStarting, from what the Router resolved. This line used to say "Asking Claude Code for a patch" unconditionally — false for every Act routed to any other Executor, and exactly the kind of unverifiable narration that makes a live run harder to trust rather than easier.
func (*ProgressReporter) ExecutorFinished ¶
func (p *ProgressReporter) ExecutorFinished(stepID, executor string, elapsed time.Duration, err error)
ExecutorFinished stops the heartbeat and reports the outcome of the call at the moment it happened — in particular a failure, which previously stayed invisible until it resurfaced (or was replaced by a later round's failure) in the Act's final error.
func (*ProgressReporter) ExecutorLine ¶
func (p *ProgressReporter) ExecutorLine(s string)
ExecutorLine renders one line of an Executor's own live output (the tools a coding-agent CLI is running, the turns it is taking) while a call is in flight. Indented and dimmed one level deeper than Foundry's own narration, because it is the Executor talking, not the Engine — and the distinction matters: only Foundry's own lines describe decisions the Engine made.
It deliberately does not stop the heartbeat: these lines arrive throughout a call that is still running, and the elapsed-time line remains the only thing that says how much of the deadline is left.
func (*ProgressReporter) ExecutorStarting ¶
func (p *ProgressReporter) ExecutorStarting(stepID, executor string, timeout time.Duration)
ExecutorStarting reports what was resolved and how long it may run, then starts the elapsed-time heartbeat that covers the call.
func (*ProgressReporter) Gathering ¶
func (p *ProgressReporter) Gathering()
func (*ProgressReporter) IntentDeclared ¶
func (p *ProgressReporter) IntentDeclared(text string)
IntentDeclared restates what was asked, at the top of the run. The Intent used to appear only in the summary block a successful run prints at the end, so a failing run — the one you actually need to read — scrolled past without ever showing what it had been told to do.
func (*ProgressReporter) ModelFailover ¶
func (p *ProgressReporter) ModelFailover(stepID, from, to string, class model.FailureClass, cause error)
ModelFailover narrates automatic model failover. The Engine has emitted this event since ADR-0013's sixth increment, but no Reporter in this repository implemented FailoverReporter, so a switch between models was never actually shown to anyone.
func (*ProgressReporter) RepairSkipped ¶
func (p *ProgressReporter) RepairSkipped(reason string)
func (*ProgressReporter) Repairing ¶
func (p *ProgressReporter) Repairing(reason string)
Repairing states the actual cause of the repair round. It used to print "Verification failed" for every round, including the rounds a failed generate Step earned, where no verification had run at all — see engine.Reporter.Repairing.
func (*ProgressReporter) StepStarting ¶
func (p *ProgressReporter) StepStarting(attempt, index, total int, stepID, kind string)
StepStarting narrates the Pipeline walk itself, so a run shows which of its declared Steps it is on rather than only the generate/verify events.
func (*ProgressReporter) Verified ¶
func (p *ProgressReporter) Verified(iteration int, judgment *domain.Judgment)
func (*ProgressReporter) Verifying ¶
func (p *ProgressReporter) Verifying(iteration int)
Verifying starts a heartbeat too: a Verifier runs a real build and test suite, which on a large repository is its own multi-minute silence.
type PromptHistory ¶
type PromptHistory struct {
// contains filtered or unexported fields
}
PromptHistory holds the lines a REPL has already submitted, most-recent- last, so ReadInteractiveLine can offer arrow-key recall (ADR-0012 Decision 3). NewPromptHistory's history is in-memory only, scoped to one process; LoadPromptHistory's also persists to disk, so a later session's arrow-key recall survives this process ending.
func LoadPromptHistory ¶
func LoadPromptHistory(path string) *PromptHistory
LoadPromptHistory returns a PromptHistory pre-populated from path's existing lines (one submitted line per line, oldest first), and remembers path so Add appends every new line back to it — ADR-0012's own v1 Decision 3 named persistent cross-session history as a later increment; this is that increment. A missing file, or one this process cannot read, is not fatal: history is a convenience that must never block a session from starting, so both cases just return an empty, path-remembering history instead of an error.
func NewPromptHistory ¶
func NewPromptHistory() *PromptHistory
NewPromptHistory returns an empty, in-memory-only PromptHistory.
func (*PromptHistory) Add ¶
func (h *PromptHistory) Add(line string)
Add appends line to the history, in submission order. A blank line (or one that is only whitespace) is not recorded — there is nothing useful to recall from it. If this PromptHistory was built via LoadPromptHistory, line is also appended to its file immediately (append-only, never rewritten wholesale, so a crash mid-session loses at most nothing already flushed) — best-effort: a write failure (a missing/unwritable directory, a full disk) is silently ignored rather than surfaced, since losing history persistence must never break the interactive session itself over a purely cosmetic convenience.
type TraceRelay ¶
type TraceRelay struct {
// contains filtered or unexported fields
}
TraceRelay forwards an Executor's live output to whichever Reporter is narrating right now. It exists because the interactive session builds its Executors once, at startup, but narrates one Act at a time through a Reporter created per slash command: without an indirection the long-lived Executor would be permanently bound to the first Act's Reporter, printing later Acts' output into a Reporter that is done. A relay with no destination set discards, so an Executor that outlives an Act narrates nowhere rather than into a closed Reporter.
func NewTraceRelay ¶
func NewTraceRelay() *TraceRelay
NewTraceRelay returns a relay with no destination yet.
func (*TraceRelay) Line ¶
func (r *TraceRelay) Line(s string)
Line is the sink to hand TraceExecutorTo. It forwards to the current destination, if there is one.
func (*TraceRelay) To ¶
func (r *TraceRelay) To(reporter engine.Reporter)
To points the relay at r, or at nothing when r is nil (what a caller defers once an Act is done). Safe to call while a call is in flight.