Every Petri run takes its scope through a sandbox-driver plugin
executable that Petri finds on PATH, so the test jobs need
sandbox-driver-host and sandbox-driver-docker installed at the rev the
workspace pins. The three jobs share one from-source install through an
actions/cache entry keyed on the OS and the rev.
The Linux test job also pre-pulls Petri's default runner image, which
the suite's Docker scenarios leave to Petri: the plugin pulls it on
first use, but a 1 GiB pull inside a run's timeout is a flake.
The stdio plugin job was built for the deleted fabro-sandbox layer. It
becomes the Docker providers job: the `docker_` scenario variants and
the fabro-petri suite, with the fabro-sandbox and fabro-workflow steps
whose tests no longer exist removed.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
CodeQL read the assertion messages as a log of the credentials'
Debug output. The test proves that output never holds the key, so the
messages added nothing.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Clippy's absolute-paths lint on the rendered prune error, the sync
directory reads the prune test documents, and a redundant rustdoc link.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
A live-server scenario runs a one-stage workflow on Docker whose command
writes a file into the workspace, opens an Ask Fabro session on the
finished run, and sends one turn. The session attaches to the container
Petri created, stopped at the run's end, starts it again, and its tool
reads the file inside it; the turn succeeds, the tool's output and the
model's reply carry the file's content, and the twin's follow-up request
shows the model read it from the tool. Ask Fabro's tool policy is
read-only, so the shell tool is hidden from the model and refused; the
turn reads the file with the `read_file` tool, scripted on the twin,
instead of a shell `cat`.
The scenario skips, and says why, without the Docker plugin or a daemon,
as the other Docker scenarios do.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Run deletion called the driver's `provider.delete(id)` under the run's
`petri.run` scope, a delete of Fabro's own over a sandbox whose lease
record Petri owns. It now goes the way `petri sandbox prune` goes:
`fabro_petri::prune` builds the run's Petri runtime over the server's
store (the run key, the run directory, the sandbox backend) and calls
Petri's prune, which opens the run for writing, checks each lease's
provider fingerprint, writes the delete intent and the tombstone beside
the run's other records, and lets each provider remove its managed
workspace, a host workspace included.
A run a live process holds answers 409 unless the delete is forced; a
lease Petri could not prune answers 409 with the problem text, or is
warned and skipped under force or a delete that already started. The
server drops the worker's handles before the prune, on the store
instance the prune opens, so the lease a stopped worker held is released
first. The projection reads only the coordinator and execution logs, so
the resource records change nothing it reports.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The worker's environment carried only the host, Docker and Daytona plugin
variables from the server's own environment, so a run on a third-party
provider kind never learned where its plugin was, although the server
kept `[server.sandbox.providers.<kind>]` plugin settings for its own
attach. The launch spec now derives `PETRI_SANDBOX_<KIND>_PLUGIN` and
`PETRI_SANDBOX_<KIND>_SHA256` from every enabled kind's plugin settings,
and `PETRI_SANDBOX_PLUGIN_DEV=1` when any of them sets `dev`, set after
the allowlist so the settings win over an ambient variable of the same
name and the allowlist stays the fallback.
The server's default plugin binary name is `sandbox-driver-<kind>`, the
name Petri looks up, since one executable serves both sides.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The grep test reads paths as the driver reports them for a resolved
absolute path; the local preflight check test gives the manifest a
source directory that exists; the Docker attach scenario accepts that
the in-process app has no daemon record for the run-tools client an Ask
Fabro turn builds after the sandbox attach, and asserts the turn got past
the sandbox. The inventory's lazy connection is boxed for clippy's
variant-size lint.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
A server scenario runs a command workflow on Docker through Petri's
plugin (skipped without the plugin or a daemon), then reaches the
container without Petri: the sandbox tab describes it under its
`petri.run` label, Run Files writes, lists and reads a file in its
workspace after starting the stopped container, a preview URL opens to a
port in it, and an Ask Fabro turn runs against it through the OpenAI
twin. A unit test attaches through the ownership seam with a scripted
provider: the run's own container attaches, another run's and one that
carries only Fabro's retired `sh.fabro.*` labels are refused as not
owned.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Nothing imports it any more: the Pebble glue lives in
fabro-pebble-sandbox, the server reaches run sandboxes through
sandbox_access, and Petri creates every run sandbox. The crate, its
test-support, its integration tests and every dependency edge go with
it. The `[server.sandbox.providers.<kind>.plugin]` settings stay: the
server still launches a plugin executable through them to attach to a
sandbox of a non-bundled kind.
AGENTS.md names the new crate and the direct-access pattern in place of
`RunSandbox`.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The sandbox handlers describe, list, download, upload, open a terminal
and build SSH and VNC access on the `Arc<dyn Sandbox>` the server attaches
to the run's record, with paths resolved against the recorded working
directory. Run Files holds the handle beside that directory and runs its
git through the driver's git facet and Fabro's exec policy;
fabro-workflow's sandbox git takes the same pair, and its
`GitCommandError` carries the driver's error. Run deletion deletes by id
through the provider scoped to the run's `petri.run` label, so a foreign
sandbox is refused and a designated host directory is left in place. Ask
Fabro wraps the attached, running handle. The legacy access shim is gone.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
`fabro-server/src/sandbox_access.rs` is the server's own path to a run's
sandbox: it connects the record's provider (the driver's Host, Docker and
Daytona providers in process, a plugin executable for any other kind),
keys ownership on the `petri.run` label Petri stamps on every sandbox it
creates, attaches by the recorded id, and for a host record designates
the recorded directory again when the id lives only in the worker's
registry. The Docker client resolves its endpoint from the same variables
Petri forwards to its plugin, so both meet on one daemon.
The doctor's Docker check and the Daytona credential probe move here with
`DaytonaCredentials`, and the `/sandboxes` inventory is rebuilt over the
driver's `list`, narrowed to sandboxes that carry Petri's run label.
Preflight asks the provider for its health instead of creating and
deleting a throwaway sandbox in Fabro's own shape, which no run uses; the
git retry policy behind the repository probe moves into run_manifest.
The callers still on fabro-sandbox's reconnect read their access through
a `legacy_provider_access` shim until they move.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Petri creates and owns every run sandbox through the sandbox driver, so
what Fabro still needs around a driver handle is the Pebble glue: the
Environment pebble's coding agent runs its tools through, the exec policy
(stop grace, working directory, StripAll, the termination mapping, the
redacted output tail), pebble's port routes over the driver's preview
URLs, the secret redactor, the path helpers, and a log rendering that
appends a failed command's redacted tail. This crate holds that glue,
moved from fabro-sandbox, over `Arc<dyn Sandbox>` plus a working
directory instead of `RunSandbox`, with a `MockSandbox` double behind
`test-support`.
`fabro exec` creates its host sandbox directly on the driver's Host
provider and activates it; Ask Fabro wraps the attached handle. Both
keep the provider alive beside the sandbox where the session's processes
are the provider's process groups.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Fabro's own DOT parser was left with one job after create-time compile
moved to Petri: walking a workflow's file references for the bundler and
the workflow-version store, and reading a name, a goal and two counts.
Petri's frontend parses the same language, so the parser goes and a small
crate reads the graph through Petri's.
`fabro-dot` is that crate: `WorkflowGraph::parse` over
`petri_frontend_attractor::dot` and its semantic model (defaults applied,
subgraphs flattened, chains expanded), `references(position)` as the one
walker over the static-reference vocabulary (each reference with its node
and position, file references checked to be template-free), and
`normalize_for_graphviz`, the re-emit of Fabro DOT with dotted attribute
keys quoted, which the SVG render needs. It sits beside `fabro-petri`
rather than inside it because `fabro-petri` depends on `fabro-workflow`,
which depends on `fabro-workflow-version`: the version store cannot reach
`fabro-petri` without a cycle, and the bundler should not pull the engine
in to read a graph.
Deleted: `fabro-graphviz`'s lexer, grammar, AST, semantic pass and
`parse_ast` (1,829 lines, plus the `nom` dependency); the DOT model in
`fabro-types::graph` (`Graph`, `Node`, `Edge`, `AttrValue`,
`shape_to_handler_type`), with only `ReferenceKind` kept, moved to
`fabro_types::reference`; `fabro-template`'s `visit_graph_references` and
the `GraphReference`/`GraphPosition` types, with the template-syntax rule
(`validate_static_reference`) kept there; the pull-request body's DOT
fallback summary, which was unreachable because the DOT source only
travels with the run spec whose display graph the summary already reads.
`fabro-graphviz` is now the render alone, over `fabro-dot`.
Parity: the old and new walkers were run over every `.fabro` and `.dot`
file in the repository (118) before the deletion. Every reference set is
identical. Five files differ in what Petri reads more correctly: a
backslash before a newline inside a quoted string is a line continuation
(four files, inline prompt text only), and a node named only by an edge
counts as a node (`test/edge_only_node.fabro`, 3 nodes rather than 2, so
the `fabro validate` snapshot moves). The checked-in bundles' shapes and
references are pinned by a snapshot in `fabro-dot`.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The rest of the change whose deletions the previous commit carries (its
`git add` stopped at an already-removed path): `fabro_types::RunGraph`
and the `fabro-petri` builder that reads it off the admitted graph, the
server's create, validate, preflight and render paths on Petri's check
alone, the consumers moved to the new shape, the OpenAPI `RunGraph`
schemas with their parity tests, the regenerated TS client, and the
docs naming Petri's diagnostic codes.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Every run is admitted by Petri, whose check lowers imports, file
references, templates and the model stylesheet, lints the workflow and
pins its models. Fabro then re-parsed the same workflow through its own
legacy pipeline (parse, transforms, structural validation) only to fill
`RunSpec.graph` for the read side. That second pass is gone: the run's
display graph is `fabro_types::RunGraph`, built once in `fabro-petri`
from the admitted graph's metadata (the workflow name and goal from the
graph params; each declared stage's label and handler kind; one edge per
routing arm as written, lowering artifacts left out), and stored on the
spec at create beside the DOT as `graph_source`.
Deleted: `fabro-workflow`'s `pipeline`, `transforms`, `file_resolver`,
`operations::{source, validate}`, `run_materialization` and the legacy
`compile_admitted_run`; the server's `compile_admitted`, the structural
manifest pass, `preflight_model` and the model probe `run_llm_check`
(Petri's admission raises `attractor.model.unknown`); `fabro-graphviz`'s
stylesheet parser; most of `fabro_types::graph` (the DOT model keeps
what the bundler, version registration and template walker read). The
DOT parser stays for the bundler and the SVG render.
`POST /validate`, `POST /preflight`, `POST /graph/render`, `fabro
validate` and `fabro preflight` run on Petri's check alone, so their
diagnostics carry Petri's codes (`attractor.unbound_input`,
`unsupported.template.unbound_input`) where Fabro's
`template_undefined_variable` and `goal_self_reference` were. A refused
workflow's summary still names the DOT as written. The OpenAPI `RunSpec`
schema gains `RunGraph`, `RunGraphNode` and `RunGraphEdge`, reused from
`fabro-types` with parity tests; the TS client is regenerated.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The CLI artifact scenario seeded its run through the deleted upload
route; it now runs a real Petri workflow whose hooks collect the
artifacts, and the fabro artifact list and cp assertions read those.
A real command retry is not producible from a command node (a plain
failure or a timeout routes onward), so the retry dimension of the old
fixture goes; the stage, node, and retry filters, the tree copies, the
cross-stage ambiguity, and the filename collision stay covered. The
archive guard test drops its upload row (the blob write row covers an
octet-stream mutation). A dangling doc comment and two absolute paths
clippy flagged are fixed.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Each edge was checked with rg over the crate's sources outside its
test paths. fabro-cli keeps git2, regex, ulid, and shlex as
dev-dependencies for its integration tests. fabro-automation keeps
chrono, tokio, and tracing: its migrations compile into the crate
through #[path]. petri_testkit was already optional behind fabro-petri's
test-support feature and dual-listed as a dev-dependency. The workspace
loses the agent-client-protocol and AWS entries no crate references;
jsonschema stays for the fabro-api and fabro-tool tests. Cargo.lock
was refreshed by a plain build and only loses entries.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
GET /runs/{id}/checkpoint duplicated what /state serves; the hidden
fabro parse command had no user; records, run_status, outcome, and
usage_rollup in fabro-workflow only re-exported fabro_types. The
importers now name fabro_types directly. format_cost keeps its two
callers (the pull request body and the CLI stage display) and moves to
fabro_types::usage; the usage rollup tests move beside the function in
fabro-types, with test_usage in its test support.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Artifacts reach the blob table through the hooks, so the POST on
/runs/{id}/stages/{stageId}/artifacts, its octet-stream and multipart
handlers, the RequireStageArtifact extractor, the client's upload
functions, and the generated TypeScript operation go. The spec loses
the operation, the multipart variant writeRunBlob never served, and
the batch manifest schemas; fabro-types loses ArtifactUpload, the
batch upload's only input type. Every list and download path stays,
and the server tests seed the artifact store directly to cover them.
fabro-server drops multer.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The engine prepares every run's checkout, so fabro's clone
orchestration, the per-checkout GitHub credentials, the run-branch
setup, the push retries, and the push policies had no production
caller. RepoWorkspace::plan still validates the clone request and now
refuses one that asks for a clone; initialize creates an empty
workspace root. SandboxWorkspaceLayout and snapshot_info stay: the run
record projection in sandbox_spec.rs reads them. The run tool
regression keeps its assertion (a child targets the parent's pushed
run branch) over a plain git fixture instead of the deleted setup. The
Docker, Daytona, and Daytona-wire clone layout tests go: they proved
only the legacy clone. fabro-sandbox drops base64, uuid, fabro-proc,
serde, strum, and sandbox-driver-daytona-config; chrono is test-only.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The failure classifiers, the handler and publish error builders, the
FailureDetail projections, and the LLM error conversions served the
deleted executor; Petri classifies failures now. Error keeps the
variants the create and read side construct, and the Engine variant
replaces the three-stage Stage shape. git_identity goes: the hooks
record git.identity through fabro_checkpoint. git.rs keeps the
observe, head, non-interactive push, and sync helpers the server and
fabro-manifest call, and loses the push half. fabro-llm loses the
failure signature hint whose only reader was the deleted classifier.
fabro-workflow drops regex, strum, and fabro-checkpoint.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
ConsoleInterviewer, RecordingInterviewer, ReplayInterviewer,
QueueInterviewer, CallbackInterviewer, and ask_with_timeout had no
production caller once every run executes on Petri. review_target_line
moves to lib.rs for the CLI's attach prompt. fabro-interview drops
dialoguer and fabro-util.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The settings layer carried every key of every catalog environment, so a
run in an environment with `lifecycle`, `labels`, `cwd`, `network` or a
Dockerfile warned `ignored.workflow_toml.environments.<id>.<key>` on
every admit. Those keys are the platform's and stay with the server's own
resolution; the layer now carries the provider, `image.docker` under
`docker` and `daytona`, `resources` under `daytona`, and `env`.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
A worker whose server is killed outright retried its control-stream
connect forever at a 5 s capped backoff, so it never finalized, never
stopped its sandbox, and lived until reboot. The worker now tracks the
start of each run of continuous connection failure and gives up after
60 s (10 s once its parent is pid 1), through the existing fatal
control-loss path that interrupts interviews and cancels the run. After
that fatal fires, the runner keeps driving the cancelled pipeline for a
bounded grace so `conclude` can stop the sandbox before the process
exits, instead of dropping the pipeline future mid-flight.
The test harness's stale-daemon reaper only matched `fabro server`
titles bound under `/tmp/.tmp*`, but `TestContext` roots are
`.ft-<label>-*` under `std::env::temp_dir()`, so daemons bound there
were never reaped. The regex now also matches sockets below a `.ft-`
directory component under any parent, and the unit test covers both
roots plus real-looking paths and `.ft-` outside a directory component.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Petri's Fabro frontend refused a bundle naming an environment it did not
declare and every MCP catalog reference, so the fixtures declared
`[environments.local]` and the server's catalogs never reached Petri.
Pin Petri at c874b86, where the frontend reads `[environments.<id>]` and
`[run.environment]` from every settings layer (bundle over project over the
host's layer, key by key), takes the environment a launch selected over the
layers, and resolves `[run.agent.mcps.<name>] id = "..."` against a catalog
the host binds. The server hands Petri its environment catalog as
`[environments.<id>]` tables of the settings layer it already passes, the
intent's environment as the launch's selection (`Launch::environment`, as
the intent overrides the bundle in Fabro's own resolution), and its MCP
catalog as `RuntimeSpec::mcp_catalog_toml`, one inline entry per definition
keyed by id. Offline validation hands Petri the seeded catalog the same
way, so `fabro validate` accepts `[run.environment] id = "local"`.
The fixtures drop the `[environments.local]` tables they carried for this;
the secrets test keeps its own, on purpose. Scenario tests cover a bundle
naming a catalog environment (its image lowered, and run on Docker when the
plugin and a daemon are there), a bundle's own table winning key by key,
the server refusing an unknown environment before Petri, and a catalog MCP
reference whose tool the agent session lists (an echo server under
`test/mcp/`).
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The generator escapes the apostrophe in the adapter description the same
way it does in every other generated comment; #883 committed the
hand-edited form.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The worker control bus was publish-only: the steer and interrupt
endpoints answered 202 once the control was forwarded, and a refusal
showed up only later as a `run.notice` on the run's stream.
A steer or an interrupt now carries a request id. The worker answers it
over the control stream it arrived on with `{request_id, outcome}`,
where the outcome is `delivered` (with the stage's label) or `refused`
(with the code and the reason). The server keeps the outstanding
requests in a registry and waits up to 5 s for the answer: the endpoint
answers 202 `{"outcome":"delivered","stage":…}`, 409 with the refusal's
code (`no_live_turn`, `no_such_stage`, `steer_refused`,
`interrupt_refused`) and message, or 202 `{"outcome":"pending"}` when
the worker gave no answer in time. The `run.notice` record on refusal
stays, under the same code, so a steer to a stage that is not running is
now `no_such_stage` there too. Pause and unpause are unchanged.
The in-process test path answers a steer or an interrupt from the run's
own controls at once. `FABRO_TEST_CONTROL_ACKS_MUTED=1` on the server
mutes the worker's answers, so a test can see the pending fallback.
`fabro steer` prints the worker's answer, and a refusal is its error.
The OpenAPI spec documents the 202 body and the 409 codes; the Rust and
TypeScript clients are regenerated.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Through a real server and its worker: a three-stage run forked at its
first stage continues with the other two on the first stage's restored
file and commits on the new run branch after the source's commits; a
retry of a run whose last stage failed transiently reruns that stage and
succeeds; a rewind archives and supersedes its source, and an archived
run is refused; the timeline lists every checkpoint record with its
commit; a fork at a checkpoint inside a parallel branch is refused and
creates nothing, while one after the join continues.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
`fabro timeline <run>` prints the checkpoint table (or JSON); `fabro fork
<run> [target]` and `fabro rewind <run> <target>` make and start the new
run and name it, `--list` showing the timeline instead; `fabro retry
<run>` starts the retry and prints its id. The top-level help snapshot
and the generated CLI reference follow.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
`GET /runs/{id}/timeline` lists the run's checkpoints with their Petri
positions, stages, commits and diff summaries, and its fork origin.
`POST /runs/{id}/fork` resolves a target on that timeline, creates the
new run, seeds it through `fabro_petri::fork` and queues it in resume
mode, so its worker restores the checkpoint into a fresh workspace and
continues from the position; a position inside a parallel branch is
refused with 400 before the run exists. `POST /runs/{id}/rewind` is that
fork of a terminal run followed by the source's archive and its
`run.superseded` record (207 when the archive fails); `POST
/runs/{id}/retry` forks a terminal run at its last checkpoint, rerunning
the stage that failed. The projection carries `forked_from`. The Rust
and TypeScript clients gain the four calls.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The operations the cutover removed come back in their Petri shape, with
no engine of their own: the timeline is the run's checkpoint records
labelled by stage (`RunTimeline::build`), and a target (`@ordinal`, a
node, or `node@visit`) resolves to one of them. A fork's run row is the
source's spec under a new id with `fork_source_ref` naming the source and
the checkpoint's commit (`persist_forked_run`), and `retried_from` when
it is a retry. A rewind needs a terminal source and records
`run.superseded` on it; a retry needs a terminal source and reruns the
last checkpointed stage when the run failed on it.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
`fabro_petri::fork` is the seam rewind, fork and retry are built on (plan
item F5.1): `fork` calls Petri's `host::fork_from` over the server's run
store to seed the new run's records up to a checkpoint's position, writes
the source's checkpoint records for every kept attempt under the new run
at their positions, seeds the new run's snapshot repository per workspace
with those checkpoints' refs alone (fetched from the source's repository
under its run scratch), and records the new run branch
(`fabro/run/<new id>` from the position's commit) with its Git identity.
`check` refuses a position Petri would refuse (an unknown execution, or
one inside a child invocation) before anything is written; `stage_labels`
reads the projector's fold state so a timeline can label checkpoints and
resolve `node@visit` targets.
The resume then restores the fresh workspace itself: `scope_acquired` now
brings a host workspace to its durable snapshot too (verified after a
restart, restored from the seeded repository for a fork), through
`recovery::bring_host_to`, which restores a directory with no history
instead of resetting it. The projection folds `forked_from` from the
fork's `run.started`.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The worker's `run.notice` for an interrupt it could not deliver now reads
"Interrupt of stage `gate` refused: the stage has no model turn to
interrupt" (or "Interrupt refused: …" when the control named no stage),
so the web and the CLI can show which stage and why, with the reason as
Petri's `ControlError` spells it. The gate scenario asserts both messages.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Petri 639ce3e added `ControlService::interrupt_firing` and the `LiveTurns`
capability a host installs beside the pause hooks. Fabro now drives it:
`RunControls::interrupt(stage, text)` resolves its stage the way a steer
does (a label, a node name, or the run's one live agent stage) and stops
that stage's current model turn, keeping the session; the text, when
given, is the stage's next input. `engine::run` installs the live-turn set
as a runtime capability, so without it no interrupt could ever land.
The worker maps `run.interrupt` and `run.interrupt_then_steer`, both of
which now carry an optional `stage`, to that call. The control bus is
one-way, so a refusal is recorded the way a refused steer is: a
`run.notice` on the run's stream whose code says why (`no_live_turn` when
Petri refuses a stage with no turn in flight, `no_such_stage`,
`interrupt_refused`).
The server's `POST /runs/{id}/interrupt` and `POST /runs/{id}/steer` with
`interrupt=true` forward the control and answer 202, replacing the 501
`interrupt_unsupported` stub. The interrupt endpoint takes an optional
body (`stage`, `text`), refuses a finished run with 409
`run_not_interruptible`, and forwards an interrupt of a blocked run, since
an agent stage may be running a turn beside the question and the worker
judges each stage itself. `fabro events --pretty` prints the delivered
interrupt and the stage's `attractor.turn.interrupted` report.
Verified on the twin: `fabro steer --interrupt` during a long tool call
ends the turn, the text is the agent's next request, and the stream
carries the `$interrupt` record and the interrupted-turn report; an
interrupt of a gate stage is refused with `no_live_turn` and the gate's
question is untouched.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
`dry_run_parallel` and `dry_run_styled` timed out under load in the run
that found the other eight, so they were not recorded with them. Alone they
show the same one-line change: the Start stage's completion now precedes the
run branch line, the order the positioned records give every run.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The Fabro halves of D1, D2 and D3. A stage's `agent_tools` is the union, by
name, of the `attractor.tools` payloads its native sessions record, with
`invoked` flipped by the envelope's `ToolCallStarted`; the payload carries
Petri's origin category, so Pebble's category is `subagent` for a sub-agent
tool and `other` for the rest. A pending question carries each option's
description and preview and the question's context, and its reference as
the review target when Fabro's validation admits it; the interview dock and
the Q&A renderer show the previews beside the descriptions, and the attach
prompt prints both under each choice. The web's command view reads the
script from the node's `meta.script`, the decision renderer the matched
condition from `meta.edges[edge].condition`, and `run events --pretty`
prints the condition on the transition line and one line per session
naming its tool count. The command view notes what the output capture did
not keep, from the final `step.finished` loss metrics.
The web fixtures are recaptured at the pin, so they carry the new facts.
VIEWS.md loses the two gap rows Petri filled and names the sources; the
README's list of what the fold leaves default shrinks to match.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The pin to Petri 639ce3e moves the run's format version from 6 to 7, which
the attach JSON snapshot records. The other eight snapshots had recorded the
run branch and Git identity lines before the Start stage's completion: the
order the clock gave them before "Place the run branch and git identity
records with their checkpoint" positioned the two records after the firing's
finish. That commit refreshed only two files, and these eight already
differed the same way at the commit before the pin bump; they now record the
one order every run produces.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Move the lithos-llm pin from 55add459 to 43a42ac28e9d9bcf40a91abc02be4f12ca274ebb,
and the three Pebble pins from a39f43e to 67c9f48, Pebble `main`, which pins
that same lithos-llm revision so Cargo holds one lithos-llm crate. lithos-llm
`main` (ca19fac) is one commit further; that commit touches only its nightly
workflow, so this pin stays on the revision Pebble unifies with.
The `openai`, `anthropic`, `gemini`, and `openai-compatible` features are
gone upstream; each expanded to `runtime`, which `bedrock` implies, so the
four names leave the fabro-llm feature list. Every other manifest already
names `runtime`.
The catalog schema now names one adapter and many codecs per provider.
`adapter` defaults to `http`, `codecs = [...]` replaces `codec` and defaults
to `["openai-chat"]`, and the loader rejects the old `codec` key and the four
protocol-named adapter ids. Every inline catalog in tests and docs moves to
the new shape: the `openai-compatible` + `openai-chat` pair is dropped as the
default, `adapter = "openai"` + `codec = "openai-responses"` becomes
`codecs = ["openai-responses"]`, and the one test that swaps in a custom
adapter id now adds the line instead of replacing one. The settings
reference, the API schema's `Provider.adapter` description, and the SDK page
describe the new fields; the three `docs/superpowers/plans/` files that show
the old shape are dated, unchecked historical plans and are left as they are.
The implied agent profile for an operator provider that declares none used
to read the removed protocol adapter ids; it now reads the provider's first
codec (Anthropic Messages and Gemini map to their harnesses, the `bedrock`
adapter to Anthropic, everything else to OpenAI), with a test for the codec
path.
Absorbing the rest of the range: OpenRouter and Fireworks now ship enabled,
so the two fabro-llm tests that used OpenRouter as the disabled fixture use
`bedrock-openai`, and the docs and comments that said the two ship disabled
are corrected. The built-in catalog grew past 100 enabled model rows
(Vercel, TypeSafe, and the enabled OpenRouter and Fireworks rosters), so the
pagination shape test walks `page[offset]` to the last page instead of
assuming one page fits.
`cargo update -p` on the four crates also re-resolved a few already-locked
edges to match the lithos-llm lockfile: `windows-sys` 0.61.2/0.60.2 ->
0.59.0 under dirs-sys, errno, nu-ansi-term, quinn-udp, rustix,
rustls-platform-verifier, tempfile, terminal_size, and winapi-util;
`windows-core` 0.61.2 -> 0.62.2 under iana-time-zone; `errno` 0.2.8 ->
0.3.14 under signal-hook-registry; and `indexmap` 2.13.0 as a new public
dependency of lithos-llm. No package version was added or removed.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Petri's QuestionOption gained optional description and preview fields
at 639ce3e; the four test initializers now set them to None.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
`run.branch` and `git.identity` are written by the checkpoint that creates
the run branch, before that firing's finish is appended, and carried no
position, so the stream ordered them by the millisecond clock: on either
side of the finish from one run to the next. Both now take that
checkpoint's stage position, and the existing ordering rule places them
after the firing's finish and before its routes, beside its checkpoint
record. The two CLI snapshots that had each recorded one of the two
orders now record the one order every run produces.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Split the label lookup out of `RunControls::steer` into two helpers and a
`let else`, which is what clippy's single-pattern lint asks for, with no
change in behaviour.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>