Pebble reports an MCP server whose connection closed mid-session once,
as McpServerDisconnected, and carries startup_ms on McpServerReady and
McpServerFailed. The workflow event sink mirrors the disconnect onto a
new agent.mcp.disconnected run event shaped like agent.mcp.failed, and
passes startup_ms through on agent.mcp.ready and agent.mcp.failed. The
raw pebble event is not stored for these, so the timing would otherwise
be dropped at the boundary.
The stage projection's McpServerStatus gains a `disconnected` kind next
to `ready` and `failed`. The fold keeps the server's tool count and
sticky invoked flag and only moves the status. The OpenAPI
McpServerStatus oneOf gains McpServerStatusDisconnected, and the
fabro-api round-trip test covers its JSON shape. A new
session_projection_parity test folds the same MCP events through
pebble's SessionProjection and fabro's stage projection and compares
them, including pebble's `disconnected`.
ToolErrorKind::Timeout needs no fabro change: the kind is stored as
pebble serializes it and never matched.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Pebble's McpServerReady and McpServerFailed events now carry startup_ms.
The workflow event sink destructured both variants by name, so it stops
listing every field. The pin is temporary: it moves to pebble main once
lithoscomputer/pebble merges the branch.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Tool-created runs no longer layer the caller's ~/.fabro/settings.toml
[run] defaults or project and machine run settings; only the values in
the request are transmitted, matching fabro run. The PR body stated this
but the public MCP and child-run docs did not, so callers relying on an
auto_approve or model default would see runs pause for approval or use
the default model without explanation. Note the behavior in both pages
and point at the explicit spec fields to use instead.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
A child created without a target copied the parent's full Git target,
including the sha admitted for the parent. Clone-based providers never
fall back to branch HEAD, so a child created after the parent pushed new
commits was checked out at the parent's starting commit and never saw
the work it was meant to review or continue.
Inherit the repository and branch only, so the child resolves the
branch's current remote HEAD at admission; the parent's pinned commit and
tag stay on the parent. Callers that want a pinned child pass an explicit
target. Folder and none targets are unchanged.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The replaced manifest builder resolved the run's repository identity from
the workflow's run.scm settings before falling back to the checkout's
origin. The new standalone derivation always used the checkout's origin,
so a fork checkout of a workflow that names its upstream repository
silently targeted the fork and pushed there.
Read the run.scm layer from the resolved workflow.toml and project.toml
(or from the inline workflow.toml bytes) and pass it through both the CLI
and the standalone run-tool adapter. When the configured repository is
not the checkout's origin, nothing can be proven about it, so derivation
now fails with a message naming that mismatch instead of the generic
"push the commit" hint.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Standalone fabro_run_create ignored the environment's provider and always
produced a Git target or failed, so a Local environment with no explicit
target was rejected by admission and a directory without Git metadata
hard-failed, while fabro run derived a folder target and a none target
for the same inputs.
Move the CLI's provider-aware derivation into fabro-manifest as a shared
helper with a typed error, and have the standalone adapter look up the
selected environment and call it. The helper also distinguishes a failed
remote query from an unpublished commit, so an offline ls-remote no
longer reports "push the commit and try again" when the branch is
already on the origin.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The MCP tool schema for the object-form create spec was a hand-written
literal that had to be kept in step with the deny_unknown_fields struct
by hand, and only the target field had a parity test. A field added to
the struct deserialized fine but stayed invisible to clients because the
advertised schema forbade it.
Derive JsonSchema for CreateRunSpec so the field list and
additionalProperties come from the struct, keep hand-written schemas only
for the two custom-deserialized types (the workflow source union and the
run target union), and extend the parity test to validate a fully
populated spec against the advertised schema.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Two inline file paths that differ only by case, or a file that is also
an ancestor directory of another, used to surface as platform-dependent
low-level I/O errors naming a private temporary directory, and the
case-only case succeeded on Linux while failing on macOS. Validate both
shapes in fabro_run_create input validation so callers get a clear
message before any staging or registration happens.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Inline workflow sources were routed through the checkout-selector
collector, which rewrites any extensionless relative path to a
.fabro/workflows/<name>/workflow.toml lookup. A supplied entrypoint such
as "review" therefore failed with "workflow was not found" even though
its bytes were in the file map.
Add a dedicated inline collector in fabro-manifest that treats the
entrypoint as an exact key, checks the file paths for filesystem
collisions before staging anything, and stages the bytes in a private
temporary root only for the duration of collection. The server adapter
now delegates to it instead of staging files itself.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The worker folder-target guard opened a run reader and mapped every
failure, including a run that no longer exists, to HTTP 500 with an
error log. Load the projection through the store's lookup instead so a
missing run is a 404 with its own error code, and run the check after
environment selection so ordinary environment errors are reported
first.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Deduplicate shared-filesystem capability checks and simplify workflow-source dispatch and types. Move Git observation and local package collection onto spawn_blocking, and flush inline workflow files before collection.
Simplify validated source and input types, derive inline size-limit messages from shared constants, add target schema-parity coverage, and remove dead producer pass-through parameters.
Pebble main `222d17f` merges the sandbox-driver re-pin fabro was
pointing at by branch commit. Same tree as `9ec23d0`, so this is a
lockfile-only change.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The packager logged the full packaging error chain at WARN. That chain
embeds caller-supplied workflow and prompt source: the graph parser's
diagnostic includes the unparsed remainder and the TOML parser prints
the offending line. The logging strategy prohibits user file contents
in tracing events at every level, and this adapter runs inside
`fabro mcp` and run workers at the default filter.
Log the collector error's own path-only message at DEBUG, since a
malformed request is an expected input error, together with the
entrypoint and file count. Wrap the blocking-task join error with
`context` instead of interpolating it. A test installs a TRACE-level
subscriber around the blocking path and checks that the fixture's
source marker, which the full chain does contain, never reaches the
log.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Supplied files are staged on the host filesystem and WorkflowLocation
probes the fixed sibling name `workflow.toml` there. A request that
supplied `Workflow.toml` beside its graph therefore attached the config
on a case-insensitive host (and then failed the not-supplied check),
while the identical request on ext4 registered a version with no
config. The outcome of a content-addressed registration depended on
the server's filesystem.
After collection, every version is checked against the supplied map:
when no exact sibling `workflow.toml` was supplied, no supplied key may
alias that name under the same case and normalization rules the tool
already applies to supplied keys. A supplied sibling config still
attaches only to the graph it selects, matching checkouts, since
several graphs may share one directory.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Every packaging failure collapsed into one generic message, so an LLM
caller that omitted a child workflow, referenced a prompt with the
wrong case, or exceeded the canonical size limit could not tell what to
fix. The collector's error type already separates variants whose
messages carry only paths and counts from the ones whose sources quote
supplied content.
collect_supplied_workflow_versions now returns the typed collector
error, with new variants for a referenced file missing from the
package root, a collected file the caller did not supply, and staging
I/O failures. The bundler reports missing files with their
package-relative path so the collector can recognize them. The packager
renders the full cause chain for path-only variants and stops at the
last path-only level, plus a hint, for graph, TOML, and template
failures whose diagnostics quote source.
The tool-side raw byte total remains a cheap lower bound; the canonical
limit now surfaces with its own message instead of the generic one.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
ServerWorkflowVersionPackager was a pure adapter over
fabro_manifest::collect_supplied_workflow_versions that touched no
server state, yet it lived in fabro-server and was imported from there
by the standalone MCP server and the CLI run worker. fabro-manifest can
depend on fabro-tool without a cycle, so the adapter now lives beside
the collector as SuppliedWorkflowVersionPackager and fabro-server no
longer exports a non-server module for it.
The adapter also cloned every version's file map out of a closure it
already owned. CollectedWorkflowClosure::into_versions hands the
versions over by value inside the blocking task instead.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
WorkflowLocation dispatches any `.toml` path to the config loader, so a
supplied entrypoint such as `sub/run.toml` was accepted, its graph
became the version entrypoint, and the config file was registered under
its own name. Runtime only reads WorkflowVersion::config_path(), the
fixed sibling `workflow.toml`, so the version's goal, environment, and
Dockerfile settings were silently dropped on every run.
In workflow-version projection, reject a config whose collected path is
not the graph's sibling `workflow.toml`. This applies to every caller
that packages versions, including `fabro run <dir>/other.toml`, which
previously registered the config and then ignored it; failing at
packaging replaces a silent drop. Manifest bundling for the legacy run
path does not project versions and is unchanged.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
FabroWorkflowVersionCreateParams deserialized `files` through a
duplicate-rejecting map, but both production routes (rmcp Parameters
and the native LLM tool dispatch) deserialize from an already-parsed
serde_json::Value in which duplicate keys have collapsed last-wins. The
only test that exercised the guard used serde_json::from_str, the one
entry point production never uses, so the safeguard was misleading.
Remove the attribute and its byte-level test, and drop the
deserialize_unique_map export that existed only for it. The canonical
WorkflowVersion wire type keeps its own duplicate-key rejection, which
does run on the byte-level HTTP route.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
validate_workflow_source_paths ran inside workflow_files for every
collected version, including the pre-existing checkout callers behind
automation materialization and `fabro run`. A repository on a
case-sensitive filesystem whose graph legitimately references two paths
that differ only by case or Unicode normalization packaged before this
branch and would have started failing.
The check is also redundant for the supplied-content path that
motivated it: the tool request validates the full key set before
staging, and the supplied collector confines collected keys to that
set. Remove it from the collector so existing callers are unchanged.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The refactored ancestor-collision loop iterated the HashMap of folded
keys, so when a version contained more than one ancestor collision the
reported pair depended on the hasher seed. The same request could
produce different 422 bodies from POST /workflow-versions on repeated
submissions.
Collect the input into a Vec and walk it in order for the ancestor
pass, matching the previous behavior, and add a test with two
collisions that runs the check repeatedly.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
validate_workflow_source_paths folded case before applying NFC, but
case folding is not closed under canonical equivalence: a decomposed
sequence and its precomposed form can fold to different strings. Two
supplied paths that a normalization-insensitive filesystem treats as
one entry therefore passed the collision check, and staging silently
overwrote one file with the other.
Apply NFC first, then fold, then normalize again, and add the Greek
pair that reproduced the gap.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The pre-resolution containment check in the version bundler was a
no-op: ManifestPath::from_absolute happily returns a `..`-prefixed path
for locations outside the package root, so an escaping
stack.child_workflow reference reached WorkflowLocation resolution,
which probes and parses config files on the host before the real
containment check in read_package_file ran. The request still failed,
but the TOML parser's diagnostic quoted the host file.
Check that the normalized reference stays under the package root before
resolving it, and extend the supplied-workflow test to plant malformed
host files that any parser would quote.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Move supplied-content packaging into fabro-manifest beside the checkout
collector, and narrow the injected seam to a packager that returns the
dependency-ordered closure so ClientBackend registers versions with the
client it already owns.
Validate the tool input once through a ValidatedWorkflowVersionCreate
newtype, matching the other tools, instead of re-validating at three
layers. Reuse the fabro-types unique-map deserializer and the shared
"not available" error helper, derive budget messages from the limit
constants, and render the tool result through the shared summary+JSON
path used by sibling tools.
Share one extension dispatch between WorkflowLocation::resolve and
from_exact_path, compute the bundler's normalized reference once, key
path-collision checks by a Cow so the canonical exact check no longer
allocates, and log the full packaging error chain before returning the
curated tool message. Replace the hand-rolled axum test server with
httpmock and declare the new unicode dependencies at the workspace.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The step's filter named `fabro-agent` and `fabro-llm`. This branch removes
`fabro-agent`, and `fabro-llm` has no ignored tests, so the step ran nothing
and nextest exited 4 on the empty selection. The agent loop's tests run in
the ordinary suite now and pebble's own suite covers the loop.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Main merged the sandbox-driver adoption (#849) in a later form than this
branch was stacked on: the driver's own exec types replace fabro-sandbox's,
shell quoting moved to fabro-util, the sandbox lifecycle collapsed, and the
driver's events are stored as run events. This branch had deleted
`fabro-agent` and put the coding agent, the environment adapter, and the
steering hub on pebble.
The resolution takes main's sandbox API and re-applies pebble on top: the
`RunSandbox` `Environment` adapter moves to `pebble_environment.rs` (main's
`environment.rs` is the sandbox spec) and runs commands through `ExecSpec`
and `ExecControls`, feeding pebble's output sink from the driver's; the
driver-era `sandbox.*` names leave the known-event list, as on main, so a
stored event with that name and no driver shape is `Unknown` rather than an
error; `program_exit_code` matches pebble's non-exhaustive termination; the
Docker and Daytona smokes use main's constructor and credentials; the
remaining `fabro_agent` paths point at fabro-sandbox.
Pebble's `mcp` feature pins sandbox-driver, and the preview-url trait
objects only cross when both sides name one revision, so pebble moved to
main's `a92c0db6` (lithoscomputer/pebble#10) and fabro pins that pebble
revision until it lands on pebble main.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Pebble's command line is a library now. `fabro exec` builds its agent as
before, with fabro's client, sandbox, MCP servers, skills, search, and
redaction, and hands it to pebble's session: the events rendered as they
happen, the answer on standard output, the summary after it, the agent shut
down for the reason the prompt ended with, and the terminal approval prompt
for tools the permission level does not allow. Fabro's own progress printer,
approval prompt, summary, and MCP report are gone. The event stream of
`--output-format json` stays on standard output. Standard output now carries
the final answer alone rather than every assistant message; `--verbose` no
longer prints tool results, since the session's renderer shows tool failures
only. The lockfile moves tempfile to the version pebble pins, and the SQLite
backup migration uses the replacement for the constructor that version
deprecates.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Pebble's `SteeringBus` now owns the map of live sessions, the buffer for
steers that arrive between sessions, fan-out of steers and interrupts, the
close-the-door detach, and the hold that keeps a paired session open. The
hub keeps what only fabro knows: pair records, principals, stage ids, and
the run events that put bus activity on the run's stream in the order its
consumers expect. `PebbleControlHandle` is gone, since the coding agent's
control handle is a bus session natively; the ACP session joins the bus
through a small adapter and carries pebble's steering message end to end,
so a human steer keeps its author on the ACP `agent.steering.injected`
event. A pair message that evicted an older steer is now accepted and the
eviction recorded, where before it was queued and reported as refused.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Pebble now folds a session tree's events into a serializable projection
with a per-prompt delta. Fabro's stage projection stays as it is: it is the
wire contract the API serves and is applied to incrementally, so pebble's
value cannot stand in for it without changing that contract. The new
parity tests replay one retained session across two stages through both
folds and pin a stage's live account to the prompt delta pebble reports,
its subagent rows to pebble's, and a stored projection to a replayed one.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Steps 4, 6, and 7 of .ai/plans/pebble-absorbs-embedder-concerns.md,
pinning pebble fc907a1 with its `search-providers` feature.
The sandbox's `Environment` adapter uses pebble's `environment::support`
for the glob grammar check, the tree-order sort of a listing, and the
capture accounting, in place of its own copies; the adapter itself stays.
The stage reads the compactions a prompt performed from the report, as a
breakdown of the usage it already billed. `web_search.rs` keeps the
vault-backed credentials and the Brave-over-Venice preference, and hands
pebble's `Brave` or `Venice` provider a fabro HTTP client; the providers
and their tests are pebble's now.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Steps 5 and 8 of .ai/plans/pebble-absorbs-embedder-concerns.md, pinning
pebble 49da137.
Agent stages and `fabro exec` ask pebble for the profile's instruction
files from the repository root down to the working directory
(`MemoryDiscovery::from_git_root`), which fabro lacked: it read the
working directory alone. Skill directories are pebble's to resolve too:
the user's skills directory, then `.fabro/skills` and `skills` under the
repository root. Prompt stages keep reading the working directory alone,
through the same discovery and loader, so `agent_memory.rs` keeps only
that call; the filename table is pebble's now.
A retained thread's export comes from `export_for_reuse`, which closes
the session and hands back an export whose cursor is already past the
close, in place of export, shutdown, and a cursor advance by hand. Ask
Fabro resumes a stored record with `resume_after`, the rule it applied
under the older name.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Step 3 of .ai/plans/pebble-absorbs-embedder-concerns.md, pinning pebble
6cdb30a with its `mcp` feature.
Pebble starts the stage's MCP servers while the agent is built, registers
their tools under `mcp__{server}__{tool}` with `ToolSource::Mcp`, and
closes them with the agent, for all three placements: a child of the run
worker over stdio, a server over HTTP (streamable or SSE), and a server
launched in the run sandbox and reached through the sandbox's preview
URL. `fabro_mcp::pebble::pebble_server` maps `McpServerSettings` onto
pebble's `McpServer`, keeping fabro's `/sse` path for sandbox-hosted SSE
servers; `RunSandbox::port_routes` hands pebble the driver's `PreviewUrls`
facet as the route to a sandbox port. The stage's event sink mirrors
`McpServerReady` and `McpServerFailed` onto the run's `agent.mcp.ready`
and `agent.mcp.failed` events, as it mirrors `RouteFailover` onto
`agent.failover`, and stores no second copy of a mirrored fact.
Deleted: `sandbox_mcp.rs`, the MCP branches of `pebble.rs` and `fabro
exec`, and fabro-mcp's client, connection manager, HTTP helpers, and SSE
transport, whose tests moved to pebble. fabro-mcp keeps the settings
re-export, the mapping, and a stdio client behind `test-support` for the
tests of fabro's own MCP server. `fabro exec` reports each server's
outcome from the agent's snapshot. The Daytona Playwright live test now
drives the sandbox-hosted server through an agent.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Steps 1 and 2 of .ai/plans/pebble-absorbs-embedder-concerns.md, pinning
pebble 1a5abe4.
Files touched come from `PromptReport`: pebble computes them from every
successful write, edit, and patch across the prompt, subagents included,
so the event-fed `FileTracking` and the tracking half of
`WorkflowEventSink` go. The stage unions the reports of its prompts.
Route failover is pebble's. The stage resolves its plan from the catalog
as before and hands pebble the remaining routes through
`fallback_routes`, each with its controls and the stage's output limit.
Pebble keeps the conversation, moves it to the next route, requeues
pending steering, and continues the prompt; the stage's plan follows the
route the report says the prompt ended on, re-activates the session
there, and mirrors pebble's `RouteFailover` as the run's `agent.failover`
event with the same payload as before. `prompt_with_failover`,
`resume_agent_on_route`, and the route bookkeeping in `LiveAgent` go.
One-shot prompt stages still walk the plan themselves.
`agent.route.failover` and `agent.tool.rounds.exhausted` join the derived
event names; both variants were falling back to `agent.event`.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Step 0 of .ai/plans/pebble-absorbs-embedder-concerns.md. Pebble's
fabro-exec-sink-max-turns branch is merged into its main (pebble 4db661c,
on the embedder-concerns branch until it lands), so fabro and petri pin
one line. Both prompt budgets survive the merge because they count
different things.
- Agent hooks use `with_max_tool_rounds`, which is what the
`max_tool_rounds` setting names and what petri does: `max_tool_rounds`
model turns may run, and the hook proceeds on a turn that still asks for
tools, so pebble gets `max_tool_rounds - 1` rounds and `ToolRoundsExhausted`
fails open. Zero rounds proceeds without an agent, as the old loop did.
Agent stages set no turn budget; the stage timeout and stall watchdog
bound them.
- Prompt stages load project memory through pebble's `ProjectMemory`, the
loader agent stages already run over `with_memory_files`, instead of a
hand-rolled copy of its budget, deduplication, and truncation.
- `fabro_sandbox::SecretRedactor` puts fabro's secret scanner on pebble's
text seams (process output tails, failed tool messages) for agent
stages, Ask Fabro, hook evaluators, and `fabro exec`. The final redaction
pass over every stored `RunEvent` stays; this does not replace it.
- Agent stages state their compaction policy explicitly: the 80 percent
threshold and six preserved turns fabro's own agent loop applied.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>