Commit graph

5290 commits

Author SHA1 Message Date
Bryan Helmkamp
bd59f52e22
Build the run's display graph from Petri's admission
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>
2026-09-19 11:41:58 -04:00
Bryan Helmkamp
52aed8c642
Read the run's display graph off Petri's admitted graph
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>
2026-09-19 11:41:38 -04:00
Bryan Helmkamp
eca2812602
Fix what the gates found after the removal sweep
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>
2026-09-19 10:15:15 -04:00
Bryan Helmkamp
51138cda57
Drop the dependency edges with no production use
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>
2026-09-19 09:33:31 -04:00
Bryan Helmkamp
05a14a6c9b
Delete the checkpoint endpoint, fabro parse, and the fabro-workflow shims
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>
2026-09-19 09:31:09 -04:00
Bryan Helmkamp
0752d4c7c4
Delete the stage artifact upload endpoint
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>
2026-09-19 09:23:47 -04:00
Bryan Helmkamp
b9b4e28efa
Update the second copy of the sandbox clone note in AGENTS.md
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-19 09:18:41 -04:00
Bryan Helmkamp
06f9cb8361
Delete fabro-sandbox's clone and push chain
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>
2026-09-19 09:18:29 -04:00
Bryan Helmkamp
efb43b45aa
Delete the executor-era error and Git helpers in fabro-workflow
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>
2026-09-19 09:10:34 -04:00
Bryan Helmkamp
c0fb71a467
Delete the interviewers the Petri cutover left unused
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>
2026-09-19 09:05:52 -04:00
Bryan Helmkamp
0e18253d7e
Move the Petri pin to 13e1044
Petri accepts Fabro's platform-only environment keys silently, so a
catalog environment no longer warns on every admit. Only the Petri
source lines move in the lockfile.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-19 07:39:13 -04:00
Bryan Helmkamp
01713c6aac
Hand Petri only the environment keys it reads
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>
2026-09-19 07:27:57 -04:00
Bryan Helmkamp
22cc96da6b
Pin Petri at d43683e for the environment and MCP catalogs
The merged Petri main carries the Fabro frontend's layered environments,
the launch's environment selection and the MCP catalog variable this
branch relies on, over the Pebble and lithos-llm pins Fabro's main holds.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-19 07:10:05 -04:00
Bryan Helmkamp
978a5b1b7e
Merge branch 'petri-integration' into petri-followup-envcat
# Conflicts:
#	Cargo.lock
#	Cargo.toml
2026-09-19 07:10:02 -04:00
Bryan Helmkamp
b1d95faa57
Hand Petri the server's environment and MCP catalogs
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>
2026-09-19 07:08:08 -04:00
Bryan Helmkamp
c2fea043cd
Regenerate the provider client model after the codecs merge
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>
2026-09-19 06:49:05 -04:00
Bryan Helmkamp
3f64d6f25d
Move the Petri pins to 1fef017 for the lithos-llm 43a42ac and Pebble 67c9f48 unification
Petri 1fef017 pins the same lithos-llm and Pebble revisions Fabro's main
moved to in #883, so the workspace links one copy of each again. The
lockfile drops the second lithos-llm and pebble-agent/pebble-coding-agent
entries the merge carried while the two pins disagreed.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-19 06:39:59 -04:00
Bryan Helmkamp
776c50307f
Merge remote-tracking branch 'origin/main' into petri-integration
# Conflicts:
#	Cargo.lock
#	Cargo.toml
#	lib/components/fabro-validate/src/lib.rs
#	lib/components/fabro-workflow/src/handler/llm/fallback.rs
#	lib/components/fabro-workflow/src/handler/prompt.rs
#	lib/components/fabro-workflow/src/operations/start.rs
#	lib/components/fabro-workflow/src/pipeline/pull_request.rs
#	lib/components/fabro-workflow/src/transforms/model_resolution.rs
#	lib/components/fabro-workflow/tests/it/integration.rs
#	lib/components/fabro-workflow/tests/it/pebble_agent.rs
2026-09-19 06:37:02 -04:00
fabro-releases[bot]
6e9be17e13 Bump version to 0.361.0-nightly.0 2026-09-19 09:31:42 +00:00
Bryan Helmkamp
cb26c5603d
Answer steer and interrupt with the worker's acknowledgement
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>
2026-09-18 23:54:11 -04:00
Bryan Helmkamp
86ac13713b
Merge branch 'petri-followup-fork' into petri-integration 2026-09-18 23:18:14 -04:00
Bryan Helmkamp
22ebbf73e4
Cover fork, retry, rewind and the timeline with real-binary scenarios
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>
2026-09-18 23:17:25 -04:00
Bryan Helmkamp
8d4a1bf894
Add the fork, rewind, retry and timeline commands to the CLI
`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>
2026-09-18 23:17:25 -04:00
Bryan Helmkamp
e7d55d6ec7
Serve fork, rewind, retry and the timeline on the runs API
`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>
2026-09-18 23:17:25 -04:00
Bryan Helmkamp
f18f02206b
Restore the timeline, fork, rewind and retry operations over checkpoints
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>
2026-09-18 23:17:25 -04:00
Bryan Helmkamp
0cd0df43ae
Seed a forked run from a source's records, checkpoints and snapshots
`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>
2026-09-18 23:17:02 -04:00
Bryan Helmkamp
a664b0ccce
Merge branch 'petri-followup-interrupt' into petri-integration 2026-09-18 22:45:31 -04:00
Bryan Helmkamp
696acc18d6
Merge branch 'petri-followup-interrupt' into petri-integration
# Conflicts:
#	lib/apps/fabro-cli/src/commands/run/petri_stream.rs
2026-09-18 22:45:06 -04:00
Bryan Helmkamp
ea85359a12
Name the stage and Petri's reason in a refused interrupt's notice
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>
2026-09-18 22:40:51 -04:00
Bryan Helmkamp
1978257aa5
Interrupt a live agent stage's model turn over Petri
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>
2026-09-18 22:38:17 -04:00
Bryan Helmkamp
bfe94038fd
Record the two dry-run snapshots the earlier gate timed out on
`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>
2026-09-18 22:30:57 -04:00
Bryan Helmkamp
f3df72e089
Merge pull request #883 from fabro-sh/bump-lithos-llm-codecs
Bump lithos-llm to 43a42ac and migrate catalogs to the codecs schema
2026-09-18 22:23:29 -04:00
Bryan Helmkamp
572f89a6f2
Read the tools, question details, script and condition off Petri's records
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>
2026-09-18 22:17:15 -04:00
Bryan Helmkamp
08b7a4fdd9
Record the snapshots the Petri pin and the record positions changed
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>
2026-09-18 22:17:15 -04:00
Bryan Helmkamp
3e065b806b
Bump lithos-llm to 43a42ac and migrate catalogs to the codecs schema
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>
2026-09-18 22:07:00 -04:00
Bryan Helmkamp
ac81543482
Name the new question option fields in the interview tests
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>
2026-09-18 21:38:18 -04:00
Bryan Helmkamp
f0cb4ef54f
Move the Petri pin to 639ce3e
Petri main now carries the fork entry point, the interrupt control, the
incremental replay and the records the views asked for. Only the Petri
source lines move in the lockfile.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-18 21:32:49 -04:00
Bryan Helmkamp
a4ffba0d0f
Merge branch 'petri-followup-projection' into petri-integration 2026-09-18 21:23:12 -04:00
Bryan Helmkamp
34819050e3
Place the run branch and git identity records with their checkpoint
`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>
2026-09-18 20:38:22 -04:00
Bryan Helmkamp
58df7473f8
Declare the batch sizes ahead of the projection tests' statements
The nightly clippy gate denies an item after a statement.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-18 20:34:55 -04:00
Bryan Helmkamp
563914b932
Spell the projector cache's idle period in minutes
The nightly clippy gate denies a Duration built from a smaller unit than
it reads in.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-18 20:33:16 -04:00
Bryan Helmkamp
29441e4ddc
Keep the steer resolution within the workspace lint baseline
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>
2026-09-18 20:09:42 -04:00
Bryan Helmkamp
a70750f3f6
Steer a Petri run by stage
`SteerRunRequest` takes an optional `stage`: the label the projection
shows (`node@visit`, or `node/e<execution>@visit` when two executions
share one) or the node's name. The server passes it on the worker control
message; the worker's `RunControls` resolves a label to the live agent
firing and steers that firing, and a node name through Petri's own
live-stage index. Unnamed, the one-live-agent rule stays, and the refusal
now names the live stages by their labels. `fabro steer --stage` sets it.
A controls scenario runs two agent stages side by side, sees the unnamed
steer refused with both named, and steers each apart, one over the API
and one through the flag.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-18 19:59:23 -04:00
Bryan Helmkamp
1ed30db1d6
Keep a live run's replay and view between projector passes
Each projector pass replayed the run whole through `replay_since` to rebuild
the engine and invocation state the derivation needs, so a pass cost the
run's length. The projector now keeps, per live run and behind the run's
pass lock, Petri's `RunReplay` and the view as the last committed pass left
it (`projector/cache.rs`), and a pass advances the replay over the records
past the ones it consumed: it reads and folds only the new records. The
SQLite store answers `read_from` with `seq >= ?`, and the signalling store
forwards it.

The rules hold as before. Records first: the cache moves only after the view
transaction commits, and a pass that commits nothing (a platform record
landed under it, a fault before the transaction) keeps the events it
derived as pending for the next pass. The cache is never checkpointed and
never a source of facts: it is dropped when the run records its finish,
after ten idle minutes, when the stored view moves under it, when the run is
deleted, and with the process; the first pass after that rebuilds it by a
full replay, filtered to the held positions. A torn tail fails the advance,
which leaves the replay where it stood, so the view holds and the pass is
retried. `PassReport::replayed_records` says how many records a pass fed
through the derivation.

Two projection tests: the records of a parallel run land in batches and
every pass replays at most its batch, with the finished run's cache dropped;
a restart and the idle period drop the cache and the next pass replays the
run so far once, then only its new records, and the view equals the rebuild
throughout. `test_support` exposes whether a cache is held and the idle
sweep.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-18 19:58:03 -04:00
Bryan Helmkamp
eceae84e1f
Move the Petri pins to c216cf2 for the kept replay and read_from
Petri c216cf2 adds `events::RunReplay`, a run's replay kept between reads
that folds only the records past the ones it consumed, and
`RunLogs::read_from`, a log read from a seq with a default over `read`.
Nothing Fabro builds changes at this pin; the projector's cache lands next.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-18 19:57:45 -04:00
Bryan Helmkamp
c730aca50c
Route SIGUSR1 and SIGUSR2 in the Petri worker to the run's controls
The worker's signal handlers pause and unpause the run through its
`RunControls`, the same path the server's pause and unpause take, so a
signal holds admission, records Petri's `run.paused` and the lifecycle
mirror, and releases it on the unpause. The legacy pause state the plan
named no longer exists in the tree; nothing was left to delete. A controls
scenario sends both signals to a real worker and reads the records back.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-18 19:45:25 -04:00
Bryan Helmkamp
a4af6fac92
Show the sandbox's ready duration and retention outcome on the run
`RunSandboxInstance` carries `ready_duration_ms` from the root scope's
`scope.acquired` and `retained` from its `scope.released`, so the view
says how long the sandbox took and whether it still exists after the run.
The OpenAPI schema, the TypeScript client and the web sandbox tab's
overview show both; the host sandbox scenario asserts them on a real run.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-18 19:34:56 -04:00
Bryan Helmkamp
0fe066d420
Fix the rustdoc link warnings and gate rustdoc in CI
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>
2026-09-18 19:24:34 -04:00
Bryan Helmkamp
6aec33c4f5
Settle clippy and formatting after the sandbox merge
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-18 18:39:58 -04:00
Bryan Helmkamp
b354a2f948
Render the branch, identity, diff and artifact records and re-record the run snapshots
`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>
2026-09-18 18:39:58 -04:00