Every intra-doc link `cargo doc --workspace --no-deps` warned on now
resolves or is plain code: the private constant and helper, the removed
`InterpString::resolve`, the lithos `Message`, the sandbox-driver facets,
the `RunOptions::git_author` the cutover removed, and the stale
`platform_record_for` paragraph on the platform records. The `[@REF]`
segment of `fabro run`'s help text is allowed as help, not a link. A
`Rustdoc` job runs the same command with `-D warnings`.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
`run events --pretty` reads the flattened `git.identity` fields, and
shows a `run.diff` record as its summary and an `artifact.collected`
record as its path and size. The snapshot filters redact the base commit a
`Branch:` line names. The CLI snapshots now carry the `Base:` line, the
`run.branch` and `git.identity` stream items, the dry run's simulated
response and the two response files a dry run dumps.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Petri's retention decides whether a released workspace is kept or removed.
Fabro's lifecycle settings decide whether a sandbox keeps running after
the run and whether a delete may remove it; none asks for removal at the
run's end, and the sandbox tab, `fabro cp`, the run's delete and the
sandbox scenarios read the container after the run. So the mapping is
`Retention::Always` for every setting, named once as `engine::RETENTION`
with the reasoning, instead of a per-setting function that released a
finished sandbox.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
No store sits behind `[server.slatedb]` any more: the section leaves the
settings layer, the resolved server settings, the defaults, the API schema,
the TypeScript client, the install wizard and the docs, and `fabro install`
probes the bucket for the `artifacts/` prefix alone. A settings file that
still carries the section is rewritten once at startup by a temporary
migration that removes it with a backup beside the file.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
`fabro inspect` lists the stages with their output, response and diff as
the projection holds them; `fabro diff` resolves a patch reference through
the run's blob endpoint; the final output and `dump` decode a plain
reference as text and a `#json` reference as a value. The diff tests run
over a git-backed Petri run again, and the large-output dump tests print
many lines rather than one, since Petri caps a single line at 64 KiB.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The commit that creates a workspace's run branch records `run.branch`
(the base commit, or the first checkpoint in a workspace with no history)
and `git.identity`. Every checkpoint record after the first carries the
stage's diff from its parent commit, with the patch as a text blob. After
the checkpoint record, the transition hook lists the stage's workspace
through the scope's environment, on the host and in a sandbox alike, and
collects every file under `[run.artifacts] include` into the blob table as
an `artifact.collected` record, skipping a file already collected under
the same path and digest. At the run's end the hooks diff the branch's
last checkpoint against its base in the snapshot repository and record
`run.diff`.
`engine::retention` maps the environment's lifecycle settings onto
Petri's workspace retention instead of always keeping every workspace:
`preserve`, `stop_on_terminal = false` and the local provider keep them,
anything else keeps a failed scope's only. The hooks docs no longer name a
redundant link target, so rustdoc passes with warnings denied.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The CLI's integration tests seeded runs by appending legacy run events
and waited on legacy event names. Now every seeded run is a real dry
run: the fixtures start the run through the CLI, read the run id from
its output and wait for the stream's terminal lifecycle record. Waits,
assertions and snapshots read `RunStreamItem`s (`run.finished`, the
platform `run.lifecycle` record, `derived.parsed.kind == "question"`).
Test changes:
- support.rs: `run_completed_dry_run`, `wait_for_run_finished`,
`wait_for_lifecycle`, `wait_for_stream_item`; the `append_seeded_*`
writers, `wait_for_event_names` and the git-backed seeded fixtures
are gone (the checkpoint patch is not in the projection yet).
- diff.rs keeps only the help test; inspect.rs drops the git-backed
checkpoint test; events.rs, dump.rs, create.rs, attach.rs and
dry_run_examples.rs snapshots are re-recorded over Petri's rendering
with redactions for epoch millis, digests and commit shas.
- run.rs: the remote foreground mock serves stream pages and a run
state with a conclusion and a `report` stage response; the event
history test checks `run.finished` and the terminal lifecycle item.
- runner.rs / attach.rs: question ids containing `#` are percent-encoded
in answer URLs.
Production fixes the ports surfaced:
- petri_worker.rs: a cancelled run exits without reporting a failure.
- runner.rs: resuming a run that already finished fails its precondition
instead of starting a worker.
Left failing on purpose, each bound to a Petri-side gap reported to the
lead rather than to the port: sandbox_cp (4), sandbox_preview and
sandbox_ssh (the projection carries no sandbox instance), the artifact
collection tests in workflow::artifacts and run.rs (no artifact
collection for Petri runs yet), and the two dump blob-ref tests (blob
refs are not visible in the inspect output).
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Step 4 of the legacy executor deletion, fourth commit: with no writer
and no reader left, the legacy event log goes.
- `fabro-types`: `run_event` (`EventBody`, `RunEvent` and every props
struct), `EventEnvelope` and the `RunEventDetail*` types are deleted.
What the projection and the API still use moves out of the event
vocabulary: `AgentEventProps`, `AgentSessionActivatedProps`,
`AgentToolsAvailableProps`, `StagePromptProps`, `SessionCapability`
and the coding event names to `agent_props`; `RunNoticeLevel` and
`RunNoticeCode` to `notice`; `InterviewOption` beside the question
types; `RunRunnableSource` beside the run status. `Checkpoint` is
what Fabro records for a Petri run: `timestamp`, `current_node`,
`git_commit_sha`; the conclusion's stage summaries derive from the
projection's stages instead of the checkpoint's node maps.
- `fabro-store`: the Slate bridge (`RunDatabase`, the Slate `Database`,
`keys`, `record`, `EventPayload`) and the reducer (`run_state`) are
deleted. `Database` is the blob table and the run summary store over
one pool; the blob store is SQLite only; the run summary store keeps
the `runs` row a projector writes and lists, and finds the pull
request creation candidates over `platform_records`; `build_summary`
and `projected_usage` live in `run_summary`. The SlateDB dependency
is gone. Test fixtures build the store from its two SQLite stores.
- `fabro-workflow`: the `event` module (the `Event` enum, its
conversion, sink, emitter, redaction, stored fields and names),
`runtime_store`, `StageScope` and the legacy seeding test helpers are
deleted; the tests that seeded legacy runs read platform records or
a projection instead.
- `fabro-sandbox` owns `GitRetryReason`.
- The server builds the store without an object store; the legacy
`POST /runs/{id}/events` tests go, an interrupt answers
`interrupt_unsupported` in the tests as it does in the handler, and
the tests that read a run back through the Slate handle read its
projection or its platform records. The projection folds a block
that lands while the run is paused as the pause's prior block, and a
pause or unpause clears the pending control it answers; a control
request's check-and-append holds a per-run lock so two concurrent
cancels record one request.
- The CLI's final output is the response of the last stage that
produced one; the workflow tests read completed nodes from the
succeeded stages.
- The spec's `RunCheckpoint` carries the three fields the type keeps.
Still failing until the next commits: the CLI tests that seed runs
through `POST /runs/{id}/events` or wait for legacy event names, and
the two Ask Fabro resume tests (the sandbox instance gap).
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Step 4 of the legacy executor deletion, third commit: the legacy event
API and every reader of it go, so that the next commits can delete the
event log, its reducer and the types beneath them.
The API:
- `GET /runs/{id}/events` pages the run stream only
(`PaginatedRunStreamList` by `after`); the legacy `since_seq`,
`before_seq` and `order` cursors, the `oneOf` envelope, the legacy
`EventEnvelope`, `PaginatedEventList`, `RunEvent`, `EventSeq`,
`AppendEventResponse` and `RunEventDetailResponse` schemas,
`POST /runs/{id}/events`, `GET /runs/{id}/events/{seq}` and
`GET /runs/{id}/stages/{stageId}/events` are deleted. `GET
/runs/{id}/attach` and `GET /attach` frame `RunStreamItem`s only.
- The Rust and TypeScript clients regenerate; the removed models leave
the TypeScript package.
The readers:
- `fabro-client` drops the legacy run event listing, tail and attach
methods and `RunEventStream`; `list_run_stream_until` bounds a stream
read.
- `fabro-tool`'s `fabro_run_events` lists, searches and details the run
stream: `after` is the exclusive `stream_seq` cursor, `event_id` the
item's id, filters match the item's name and `recorded_at`.
- `fabro-dump` writes the stream to `events.jsonl`; `fabro dump` reads
it.
- The CLI's progress renderer keeps only what the run stream drives:
the legacy event conversion, the sandbox and setup displays and their
styles go. `fabro system events` prints stream items.
- The server's demo mode folds its agent fixture straight into the
session projection and answers the attach stub with a stream item;
the demo stage events endpoint is gone.
- The web app: every run is a Petri run. The legacy event hooks,
renderer props, stage popover summary, run phases derivation and
live-event payload handling are deleted or ported to `RunStreamItem`;
toasts and board refreshes read the stream's platform records.
- Tests: the legacy API round trips and pagination tests are deleted;
the CLI's MCP, attach and system event mocks serve stream pages; the
CLI test helpers read stream items.
Still failing until the later commits: the CLI tests seeded through
`POST /runs/{id}/events`, the server tests over the legacy store, and
the legacy type tests.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Step 4 of the legacy executor deletion, second commit. Ask Fabro's
sessions were the last writer of `run_events`: a session's creation, its
turns and their messages, tool calls and endings went into the run's
legacy event log, keyed by the run's sequence. They now have a log of
their own.
- `run_session_events` (migration `2026091802`): one row per session
event, numbered per session from 1, with the owning run, the turn, the
event name and its properties. `RunSessionEventStore` appends under the
write lock, lists a session from a sequence, names a session's owner
from its creation event, deletes a run's sessions with the run, and
publishes each committed event to its subscribers.
- `fabro_types::SessionEvent`: `seq`, `session_id`, `run_id`, `ts` and a
flattened body (`event` naming the kind, `properties` its fields), with
the same event names and property shapes the legacy events carried,
so the web app and the CLI read the same JSON. The property structs
move to `session_event`; `run_event::session` re-exports them under
their old names until the legacy event log goes.
- The API: `GET /sessions/{id}/events` pages `PaginatedSessionEventList`
by the session's own sequence, `GET /sessions/{id}/attach` replays and
streams `SessionEvent` frames (subscribed before the replay, so no
event falls between the two), the turn stream carries the same frames,
and an interrupt answers with the recorded event. The session
projection folds `SessionEvent`s; the legacy `find_session_owner` over
`run_events` is gone.
- The CLI's `run ask` and the web app's session stream read
`SessionEvent`; the web runtime no longer accepts the nested legacy
envelope shape.
The two session resume tests in the server keep failing for a reason
this commit does not touch: Ask Fabro reconnects to the run's sandbox
from the projection's sandbox instance, which the Petri projection does
not carry yet (`VIEWS.md`, the `scope.acquired` gap).
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Step 4 of the legacy executor deletion, first commit of several: step 4
spans commits because the legacy event log and its consumers cannot go
in one compiling change. This commit moves every writer off `run_events`;
the reducer, `EventBody`, the Slate bridge and the API's event types
still exist for the readers the next commits port or delete.
Writers:
- The server records a run's lifecycle (submitted, runnable, starting,
running, blocked, paused, control requests and effects, the terminal
status), its title, parent link, archive state, notices and pull
request state as platform records (`fabro_store::platform_records`),
through the new `server::run_records` module. Every append wakes the
projector and waits for its pass, so the read that follows a write
holds the record.
- Pull request creation is recorded as `pull_request.requested`,
`pull_request.created`, `pull_request.failed`, `pull_request.linked`
and `pull_request.unlinked`; the projection folds them into the run's
pull request and creation state.
- Answers to questions are recorded as `interview.answered` with the
answering principal and the answer text; the interview adapter no
longer posts legacy `interview.*` events (`QuestionSink` is now an
optional observer).
- The worker (`fabro run __run-worker`) records its lifecycle, notices
and pause state over `HttpPlatformRecords`; `HttpRunStore` for the
legacy event log and the worker's `run_store` are gone.
- `persist_created_run` appends `run.created` and `run.submitted`.
Readers:
- A stream follower (`server::stream_follower`) follows each live run's
stream (Petri events and platform records), folds lifecycle records
into the in-memory run state, forwards items to the global attach
broadcast, and syncs blocked and paused from the projection.
- Slack posts questions from the projection's pending interviews,
finishes them on `interview.answered` or `question_expired`, and sends
lifecycle notifications with `notification.sent` dedupe.
- `GET /runs/{id}/events` and the attach endpoints serve only the run
stream; the per-event, per-stage and `POST /runs/{id}/events`
endpoints and their tests are deleted.
- `Database::load_run_projection` reads the Petri projection only.
Deleted with the writers:
- The SQLite blob and run-history activation migrations and their
legacy Slate imports (`legacy_blob_import`, `legacy_run_history_import`,
the activation backup): a greenfield server has no Slate history to
import, and the run-history verification refused to start a server
whose runs have no legacy events.
- `fabro-workflow`'s `operations::archive` and `operations::run_store`.
- The server's legacy-event unit tests and the CLI's `HttpRunStore` tests.
The in-process answer transport is now set after the starting and
running records land, not gated on the live status still being
`Starting` (the records already moved it).
The manifest validation test for a `run.agent.mcps.<name>` catalog
reference now expects `unsupported.workflow_toml.run.agent.mcps.reference`:
Petri's Fabro frontend has no server catalog to resolve it against.
Legacy readers still fail their tests until the next commits: the
reducer and Slate tests in fabro-store, the fabro-workflow create tests
that read the run back through the legacy store, the CLI tests seeded
through `POST /runs/{id}/events`, the CLI's legacy attach and render
paths, the sessions API, the OpenAPI conformance test, and the web
fixtures.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Petri judges a workflow at admission, so Fabro's lint rules go:
`fabro-validate` (its 35 rules and the `LintRule` trait) is deleted, and
with it `fabro-acp` (only a rule and two legacy executor tests used it),
the model-resolution transform, the legacy `create`, `compile_create_run`
and `materialize_create_run` stages, and `fabro-graphviz`'s `condition`
and `fidelity` modules. `Diagnostic`, `RelatedDiagnostic` and `Severity`
move to `fabro_types::diagnostic`, the one shape every diagnostic takes.
Validation is now the same question the create handler asks. A new
server module, `petri_check`, builds Petri's check request from a
workflow bundle and the run's settings (every workflow of the bundle at
its bundle-relative path, the inputs, the run variables, the launch),
runs the check, and maps the diagnostics; Fabro adds one rule of its
own, `fabro.model.no_ready_provider`, refusing a model node when no
provider is ready. Admission, the validate and preflight endpoints and
the offline `fabro validate` all go through it:
- `validate_prepared_manifest` runs Fabro's structural pass (parse and
transform, whose diagnostics stay) and then Petri's check, on the
blocking pool from the handlers;
- the offline `validate_manifest` checks with no model client and with
an unbound input as a warning (`CheckRequest.unbound_is_warning`), so
a workflow validates before its inputs exist; a collected workflow
before upload checks with unbound inputs as errors, as before;
- preflight resolves each LLM node's selector against the ready
providers and the catalog for its probe, as the deleted transform did,
and no longer probes a model Petri refused;
- the graph render endpoint needs only the structural pass;
- a run manifest now carries its `[run.goal] file`, which Petri reads
from the bundle as it does for a version.
The transforms keep the authored model selector (`sonnet` stays
`sonnet`): Petri pins the catalog model in the admitted graph, not in
the graph Fabro displays or in the settings snapshot. Tests assert that,
and the CLI's validate, preflight and graph snapshots carry Petri's
diagnostics (`attractor.no_start`, `attractor.undeclared_node`,
`attractor.bad_on_failure`, ...) in place of the lint rules' text.
Known gaps, Petri's side: a `workflow.toml` whose `[run.environment]`
names an environment the server catalog defines but the file does not
is refused (`unsupported.workflow_toml.run.environment`), as admission
already refused it; an unbound input inside an included template
partial is a render error rather than the unbound-input warning.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
`fabro-mcp` held two things after the legacy executor went: the mapping
from Fabro's MCP server settings to the servers pebble starts, which
only `fabro exec` still uses, and a stdio MCP client the tests of
Fabro's own MCP server speak through. The mapping is now
`fabro-cli`'s `mcp_servers` module and the client its test support's
`McpStdioTestClient`; the crate is deleted. Its `config` module was a
re-export of `fabro_types::settings::run`, which callers import directly.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The legacy executor replayed a run from a checkpoint; Petri resumes a
run from its records instead, and the checkpoint timeline, rewind, fork
and retry were the operations that replay carried. The server dropped
their handlers with the executor; this removes the rest:
- the API spec's `/runs/{id}/retry`, `/rewind`, `/fork` and `/timeline`
paths with the `ForkRequest`, `ForkResponse`, `RewindRequest`,
`RewindResponse` and `TimelineEntryResponse` schemas, and the
generated TypeScript models;
- `fabro rewind` and `fabro fork` (with the checkpoint timeline printer
and the repo-origin check only they used), their reference pages and
the checkpoints guide's rewind and fork sections;
- `fabro-client`'s `rewind_run`, `fork_run` and `run_timeline`;
- the web app's Retry action on the run list and the run page.
`Run.retried_from` stays on the run type: a run that was retried before
the cutover would still name its source.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Delete `Engine`, `RunEngine`, `[workflow] engine`, `[server.execution]
engine`, `FABRO_SERVER_ENGINE` and `fabro server start --engine`. The run
spec records what Petri admitted as `admission: PetriAdmission`; the
create handler always admits through `Runtime::check`; `execute_run`
always launches the Petri worker (or executes in process under the test
override); the CLI runner takes only the Petri worker path, and its
legacy control arm, artifact uploader, signal pause handlers and
credential helpers go with it. The CLI's `attach` and `events` read the
run stream only.
Two gaps this surfaced are closed here: the check adapter binds the
server's run variables as Petri compile variables (`{{ vars.* }}` in a
prompt no longer fails admission), and deleting a run removes its Petri
records, lease, platform records, projection and stream.
Tests: the config engine tests are replaced (an engine key is unknown),
the API round-trip test covers `PetriAdmission`, the server and CLI
Petri scenarios drop their engine settings, and the API tests that read
legacy event names now read the run stream or the session events. The
remaining red tests are fixtures and scenarios of the legacy executor
and the legacy event store (`fabro-store` `slate` and `run_state`,
`fabro-types` legacy `run.created` JSON, the server's handler-registry
scenarios, the CLI dry-run snapshots), which the next steps of the F4.3
series delete or port.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
A Petri run on Docker or Daytona keeps its workspace inside the scope's
sandbox. Fabro's hooks now take the environment Petri hands them at
`scope_acquired`, run `git` inside the scope through it (the path Petri's
own sandbox-placed hooks take), and commit each stage on the run branch
with the same message and trailers as the host path. The commit leaves
the sandbox as a Git bundle, created against the newest ancestor the
snapshot repository already holds, split into 8 MiB parts (the plugin
transport reads one file up to 16 MiB), read out through the
environment's file transfer, fetched into the bare snapshot repository
on the host and named there under the checkpoint's ref. The repository
holds every checkpoint whatever the provider, and the platform records
name the same commits. The host path is unchanged; both sites share one
runner and the same commands.
Recovery is split: `recovery::plan` decides, over the records and the
snapshot repository alone, what every live workspace must sit on and
reconciles a lost record; `recover` applies it to host workspaces on the
server, as before, and reports a sandbox workspace's target as deferred.
The worker's hooks read the same plan at the scope's first acquisition
after a resume and bring the sandbox workspace to it before any attempt
runs there: verified or reset in a retained sandbox that still holds the
commit, else restored from a bundle of the checkpoint written into the
sandbox. Petri replaces a lease's lost sandbox on Fabro's request
(`LostSandbox::Replace`), so a removed container comes back fresh and
restored.
The checkpoint records are written for every provider now. A Docker
variant of the in-process hooks test moves a 20 MiB file through the
split transfer; the same test runs on Daytona when live credentials are
present. Three CLI scenarios run on a Docker environment: every stage's
checkpoint published from the container, a retained container whose
workspace drifted reset on restart, and a removed container replaced and
restored from the snapshot.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
A Petri run answered only cancel and answers; pause, unpause and steer
were ignored with a warning. `fabro_petri::controls::RunControls` now
wraps Petri's `ControlService` per run: `engine::run` installs its pause
gate over the run's hooks, observes the run through it and wires it to
the coordinator, on a start and a resume alike, so a run paused when its
worker died resumes paused.
The worker's control channel takes a `WorkerControls` enum: the legacy
hub and pause flag, or the Petri run's controls. On Petri, `run.pause`
holds admission, `run.unpause` releases it once the record is durable,
and `run.steer` goes to the one live agent stage (Fabro's steer names no
stage); with none or several it is refused with a `run.notice` record.
The paused state is mirrored to Fabro's lifecycle as `run.paused` and
`run.unpaused` events, so the server's live status and the projection
follow Petri's own records.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Plan item F3.4. `fabro_petri::host_tools` adapts Petri's `HostTools`
capability to `register_fabro_run_tools`: every native agent session of a
run gets the tools the legacy worker registers, bound to the worker's
client and the run id, so a child run a stage creates is parented to the
Petri run. The tools run under the run's tool hooks, are recorded under
the stage, and reach sub-agents through Pebble's inheritance.
`RuntimeSpec::run_tools` installs the capability; the worker sets it when
the run's settings enable `[run.agent] fabro_tools` and the worker token
carries `agent:run_tools`, the legacy worker's gate. The server's
in-process test path runs without them, like the legacy one.
The identity the tools need is the run id alone; no run tool records a
stage on an effect, so nothing derives Fabro's `node@visit` label. A
context for another run gets no tools.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
`run events` on a Petri run prints the run stream: raw, the envelope as
one JSON line per item; `--pretty`, Petri's events by `<subject>.<verb>`
with the stage's label (a visit's start and end with its elapsed time,
the route both ends of the edge, a fork's branches, a question with its
options and its answer, log lines, the agent's messages and tool calls,
the engine's finish) and the platform records by kind (the run's
creation, its lifecycle, a checkpoint's commit, a pull request, a
notice, who answered). `--follow` attaches from the last `stream_seq`
printed and reconnects from its cursor when the server ends the stream
before the run's terminal record.
`run attach` on a Petri run replays the stream through the progress
renderer (a new mapping from stream items onto the progress events the
renderer draws, sharing the coding-agent mapping with the legacy
envelope), follows it live from its cursor with the same reconnect, asks
a question the stream carries at the terminal, and exits with the status
the engine's finish or the terminal lifecycle record decides. `wait` and
`inspect` read the projection unchanged.
The CLI never names a Petri type: `PetriItem` reads the item as JSON
where Petri's contract keeps the event name, the subject and the parsed
progress payloads.
The CLI's Petri scenarios read the stream instead of the legacy events
(the lifecycle records, the question and who answered it, the expiry),
and three new ones cover a finished run through `events` (raw, tail,
and a `--pretty` snapshot), `attach`, `wait` and `inspect`; `attach`
answering a gate from the terminal; and `events --follow` to the run's
end.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Fabro's hooks on a Petri run wrap the hooks the runtime installed for
`[[run.hooks]]` and forward every point. In `prepare_result`, before the
finish is recorded, they commit the stage's files on the run branch of
its host workspace with Fabro's author identity and the run, execution,
firing and attempt as trailers, and publish the commit to a snapshot
repository beside the run's workspaces under a ref per checkpoint. A
stage that failed on its own terms is committed like a successful one; a
commit that fails is fatal: the outcome becomes a `checkpoint_failed`
failure, the run is cancelled through the coordinator handle, and the
transition refuses the firing's routes. In `transition` they write the
platform checkpoint record, keyed on the Petri position and the
checkpoint's operation identity, and a failed write is a recorded
problem.
On restart the server runs the recovery protocol before it relaunches a
worker: a run with a failed checkpoint is reported failed; otherwise
every live execution's last durable finish names the snapshot its
workspace is verified against, reset to, or restored from, with a lost
record reconciled from the snapshot repository, and a finish with no
snapshot fails the run rather than resume it on stale files.
The worker reaches the platform records over two new worker-scoped
endpoints; the server reaches the table directly. A test gate directory
lets the CLI scenarios hold a checkpoint at a named point.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
A Petri run in the worker, and in the server under its test override, now
gets Fabro's platform adapters instead of the standalone defaults:
- `fabro_petri::interview`: Petri's `Interviewer` over the questions API
and the worker's control channel. A human gate's question is posted as
the `interview.started` event a legacy stage emits, keyed by an id
derived from Petri's identity (node, execution, firing, occurrence,
ask), so the API, the web app and Slack list it; the answer posted to
the questions endpoint reaches the control interviewer the adapter waits
on and is mapped onto Petri's answer. An expiry the gate reports is
completed as `interview.timeout`, a cancel as `interview.interrupted`,
and an auto-approved run answers itself. The hook points the read side
takes over are marked.
- `fabro_petri::secrets`: Petri's `SecretProvider` over the vault's token
entries, so `{{ secrets.NAME }}` resolves at spawn and is masked in every
record; a sensitive answer registers as a dynamic secret.
- `fabro_petri::blobs`: Petri's `OutputStore` over Fabro's `blobs` table,
through the server's blob store or the worker's client.
- The Fabro home the server resolved travels to the worker as
`--fabro-home`, so the skills step reads it whatever the worker's
environment says.
`engine::RunRequest` takes the interviewer, its observers, the secret
provider and the blob table from the caller; `interviewer::Unattended` is
gone.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
When `fabro run __run-worker` finds its run's stored spec names Petri, the
new `petri_worker` module executes it through `fabro_petri::engine` over
`HttpRunStore`, leased for a launch id the worker mints and logs at start.
`--mode start` loads the admitted graphs through the client's blob read;
`--mode resume` continues the run from its records. The worker's existing
services carry over: the control channel's cancel and SIGTERM/SIGINT cancel
Petri's root invocation politely, a lost control channel cancels the run
and is reported once it settles, and pause, unpause and steer are received
and ignored with a warning until their adapters land. The model client
comes from the worker's catalog and vault snapshot for the providers whose
credentials resolve, and the lifecycle events (`run.starting`,
`run.running`, then `run.completed` or `run.failed`) go through the client
as the legacy worker's do.
Scenario tests against the real binary: a command-only Petri run executes
in the worker a foreground server launched, its records reach
`petri_records` over the HTTP store and its lease ends with the worker; and
a run whose server and worker are both killed mid-stage resumes in a new
worker after the server restarts, with one `run.completed`.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The worker shape of the integration plan (F1.3) needs a run's worker to
reach the run's Petri records over the server's API. This adds the
contract: six worker-scoped endpoints under `/api/v1/runs/{id}/petri/`
(open, release, list and append records of one log, write and read a
blob), their request and response schemas, and the generated Rust and
TypeScript clients.
A store error needs more than a code: `petri_run_leased` names the
holding owner and `petri_record_conflict` names the refused position.
`ErrorResponseEntry` gains an optional `meta` object for such
code-specific members, `ApiError` can carry it, and the client's
`ApiFailure` parses it beside the code so a caller can act on it.
Records travel as `{seq, recorded_at, record}`, the store's own unit,
with `seq` and `recorded_at` as `uint64`. The log path segment is the
log id's text (`coordinator`, `resources`, `execution <n>`), which the
generated client percent-encodes. The blob write reuses
`WriteBlobResponse`, since Petri's digest is Fabro's blob hash.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
A workflow version names its engine with `engine = "petri"` in the
`[workflow]` table of `workflow.toml`, and `[server.execution] engine`
(`FABRO_SERVER_ENGINE`, `--engine`) defaults it for every version that
names none. The choice, with what Petri admitted (the lowered root graph
and its children by blob and digest), is recorded on the run spec as
`RunEngine`, carried on `run.created`, and replayed into the projection.
A legacy run's spec omits the field, so existing specs decode unchanged.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
lithos-llm attaches a cost to every response at the client: the codec
keeps a provider-reported cost when the provider supplies one, and the
resolver fills the catalog's price for the route when it does not.
Pebble records that priced usage on every assistant turn and sums it,
so each AssistantMessage on the stream, and the store fold's live stage
usage, already carries the cost. Fabro's catalog re-pricing of the same
tokens was redundant, and is gone.
model_usage_from_llm, with_reported_cost, and every estimate_cost call
in fabro are deleted. The pebble handler's stage_usage groups pebble's
accounts by route and sums them with Usage::saturating_add, keeping the
cost and source pebble carried, so the terminal stage.completed usage is
the live fold's sum; it no longer fails when the catalog does not know a
provider. A one-shot prompt stage records the response's own usage and
cost as lithos-llm returned it. The per-model price cards in fabro-llm's
API module stay.
Tests: the pebble handler sums Catalog and Provider costs per row and
leaves a row's and the total's cost unknown once an answer was unpriced;
the store fold shows the same tokens and cost live and at completion,
and None at both for an unpriced answer; the agent integration test
compares the whole completed Usage with the live fold, cost included;
a one-shot prompt stage on a mocked OpenAI-compatible provider reports a
Catalog cost that lithos-llm's resolver attached, with no fabro pricing.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Re-pin lithos-llm to 55add4596b861a0623d00c3a54aa5c147c8d504b and
pebble to c91810fe51aece80359b9cd8efea971af0c46925, where token usage
and cost travel together as Usage { tokens: TokenCounts, cost:
Option<Cost> }. Fabro now carries that one type everywhere it used to
carry BilledTokenCounts, BilledModelUsage, UsdMicros, or a token count
beside a cost_usd_micros.
fabro-types: billing.rs is usage.rs with ModelRef, ModelUsage { model,
usage }, sum_usage, and usage_is_empty; billing_rollup.rs is
usage_rollup.rs with ProjectionUsageStage, ProjectionUsageByModel,
ProjectionUsageRollup, and usage_rollup_from_projection. Every usage
field is named usage: StageProjection.usage and usage_by_model,
Outcome<Option<ModelUsage>>, stage.completed and stage.failed usage and
usage_by_model, prompt.completed usage, run.completed and run.failed
usage (total_usd_micros is gone), Conclusion.usage, StageSummary.usage,
Run.usage. RunSize buckets by Cost.
fabro-workflow: model_usage_from_llm prices tokens from the catalog with
a Catalog cost source, with_reported_cost keeps a provider cost, and the
pebble handler's stage_usage groups pebble's accounts by model and sums
rows with Usage::saturating_add, so a total has a cost only when every
priced part was priced. The store fold's live usage is the agent's
usage plus its descendants'.
API: the OpenAPI spec deletes BilledTokenCounts, BilledModelUsage,
CompletionUsage, CompletionCost, TokenUsage, and RunBillingSummary,
adds TokenCounts, Cost, Usage, and ModelUsage, and renames every
billing schema, property, tag, path, and operation to usage. fabro-api
reuses lithos-llm's and fabro-types' types through with_replacement,
with a round-trip test per replacement.
Old stored runs get no migration: their pebble events in the old shape
read back with zero usage, and their rebuilt projections lose agent
usage.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Since #852, `fabro exec --verbose` only turned on the request/response
middleware on the LLM client and no longer printed tool calls, tool
results, or the transcript. Pebble #18 gives pebble-cli-core rendering
options, so `--verbose` now runs the prompt through
`run_prompt_with(..., RenderOptions::verbose())`: each tool call's
arguments and result in full under its `[tool]` and `[result]` lines,
plus the transcript. The middleware is enabled as before. Without the
flag the renderer gets the default options, so the output is unchanged.
The twin shell test now scripts the tool call and the final answer as
two turns, so the answer on stdout is the scripted one rather than the
twin's fallback echo, and it asserts that stderr carries no result,
reasoning, or verbose blocks. A new twin test runs the same prompt with
`--verbose` and asserts the tool and result blocks and the request dump.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
One usage rule: a stage's usage is its session tree's, the root and every
subagent, live and at completion. The worker's event sink folds pebble's
SessionProjection over the events it records and the stage's billing and
files come from that fold at stage end, so the completed values are what
the run showed live. The store's live usage is the fold's tree usage, and
completion brings the catalog's price for the same tokens instead of
resetting them to the root's.
Fabro keeps catalog pricing: the root at its route, each descendant at its
own route where the catalog knows it and at the root's otherwise, a
provider-reported cost standing in where pebble has one. The rows travel
as billing_by_model on stage.completed and the stage projection, and the
billing rollup splits by_model by them.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Pebble now owns the trait an application hands its MCP support to reach a
port inside the environment. SandboxPortRoutes implements PortRoutes over
the run sandbox's preview-URL facet: a missing facet is Unsupported, a
driver failure is Failed with the driver error as its source. The mcp
feature no longer pins sandbox-driver, so fabro's sandbox-driver pin moves
on its own from here.
The new pebble rev also puts the summary call's usage and cost on
CompactionCompleted, which the CLI progress test literal names.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The CLI's native Git runner and the server's run_git_plan each hand-rolled
the same mechanics: kill-on-drop, a wall-clock timeout, output capture,
and (only in the CLI) process-group teardown, bounded capture, and
cancellation. Add fabro_proc::SupervisedCommand, which owns stdio, the
process group, the timeout, cooperative cancellation, and bounded
capture, and put both runners on it. The server gains group teardown on
timeout, so helpers a stuck clone or fetch spawned no longer outlive it;
the CLI keeps discarding output on failure and gains nothing but less
code. The hardened -c overrides become one named list in the CLI.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
owned() polled tokio's ctrl_c() only for the duration of one Git
acquisition. On Unix that listener permanently replaces the default
SIGINT disposition, so once acquisition finished nothing handled Ctrl-C
and fabro create, fabro run --detach, and fabro run before attach
silently ignored it while waiting on the server. Introduce an
Interruption handle that the command entry points create from the run
arguments: it installs a listener only when --workflow-git or
--target-git is in play, guards create (and start for fabro run) as one
phase, and tracks owned Git tasks so interruption waits for their
cleanup before returning. attach keeps installing its own listener.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
For --workflow-git selections, target resolution ran before the remote
workflow ref was verified to exist. On Docker and Daytona environments a
path target that is a GitHub checkout is observed via
observe_git_run_target, which may silently push the attached branch, so
a typo in --workflow-ref produced a remote side effect with no run
created. Resolve the remote workflow after parent and environment
validation but before target observation, restoring the pre-existing
workflow-then-target order, and cover it with a caller checkout whose
unpushed branch must stay unpublished.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Remote acquisition walked the entire depth-1 checkout and failed on any
symlink that dangled or resolved outside the root, even when the link
was nowhere near the selected workflow. Submodule-style dangling links
and links into the host are common in workflow repositories and made
--workflow-git fail where the same commit collected fine locally. The
bundler already root-checks every file it opens; the only unchecked
reads were the selected TOML (or a graph selector's sibling TOML) during
location resolution. Check those in collect_workflow_versions and drop
the O(repo) walk. walkdir stays a dev-dependency for the dump tests.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
RemoteWorkflowRevision::parse already enforced which ref namespaces a
value may name, but RefCandidates re-derived the branch/tag split from
the string and treated everything under refs/ that was not refs/heads/
as a tag, relying on an invariant checked in another file. Parse now
yields Branch, Tag, or Name variants and resolution matches on them
directly, replacing the Option/Option candidate encoding and its
impossible (None, None) input state.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Target resolution accepted any remote default HEAD that passed the ref
selector grammar, then failed inside GitRunTarget::validate with a
generic branch-grammar error when the default branch was something like
heads/main or tags/release. Validate the default branch as a working
branch name up front and point the user at --target-branch, since they
passed no branch at all.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>