The CLI's native Git runner and the server's run_git_plan each hand-rolled
the same mechanics: kill-on-drop, a wall-clock timeout, output capture,
and (only in the CLI) process-group teardown, bounded capture, and
cancellation. Add fabro_proc::SupervisedCommand, which owns stdio, the
process group, the timeout, cooperative cancellation, and bounded
capture, and put both runners on it. The server gains group teardown on
timeout, so helpers a stuck clone or fetch spawned no longer outlive it;
the CLI keeps discarding output on failure and gains nothing but less
code. The hardened -c overrides become one named list in the CLI.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
remote_workflow_run_starts_once_create_leaves_submitted_and_failures_do_not_refetch
lived in cmd/create.rs but drove fabro run in four of its five
iterations and asserted the start call, which is fabro run's contract.
Split it: cmd/create.rs keeps the single create invocation that must
leave the run submitted without starting it, cmd/run.rs owns the
run-driven success and failure iterations, and the workflow and remote
repository fixtures move to the shared command test support module.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
owned() polled tokio's ctrl_c() only for the duration of one Git
acquisition. On Unix that listener permanently replaces the default
SIGINT disposition, so once acquisition finished nothing handled Ctrl-C
and fabro create, fabro run --detach, and fabro run before attach
silently ignored it while waiting on the server. Introduce an
Interruption handle that the command entry points create from the run
arguments: it installs a listener only when --workflow-git or
--target-git is in play, guards create (and start for fabro run) as one
phase, and tracks owned Git tasks so interruption waits for their
cleanup before returning. attach keeps installing its own listener.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
For --workflow-git selections, target resolution ran before the remote
workflow ref was verified to exist. On Docker and Daytona environments a
path target that is a GitHub checkout is observed via
observe_git_run_target, which may silently push the attached branch, so
a typo in --workflow-ref produced a remote side effect with no run
created. Resolve the remote workflow after parent and environment
validation but before target observation, restoring the pre-existing
workflow-then-target order, and cover it with a caller checkout whose
unpushed branch must stay unpublished.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Remote acquisition walked the entire depth-1 checkout and failed on any
symlink that dangled or resolved outside the root, even when the link
was nowhere near the selected workflow. Submodule-style dangling links
and links into the host are common in workflow repositories and made
--workflow-git fail where the same commit collected fine locally. The
bundler already root-checks every file it opens; the only unchecked
reads were the selected TOML (or a graph selector's sibling TOML) during
location resolution. Check those in collect_workflow_versions and drop
the O(repo) walk. walkdir stays a dev-dependency for the dump tests.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
RemoteWorkflowRevision::parse already enforced which ref namespaces a
value may name, but RefCandidates re-derived the branch/tag split from
the string and treated everything under refs/ that was not refs/heads/
as a tag, relying on an invariant checked in another file. Parse now
yields Branch, Tag, or Name variants and resolution matches on them
directly, replacing the Option/Option candidate encoding and its
impossible (None, None) input state.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Target resolution accepted any remote default HEAD that passed the ref
selector grammar, then failed inside GitRunTarget::validate with a
generic branch-grammar error when the default branch was something like
heads/main or tags/release. Validate the default branch as a working
branch name up front and point the user at --target-branch, since they
passed no branch at all.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
ls-remote ran in the caller's working directory while fetch, cat-file,
checkout, and rev-parse ran inside the temporary checkout, so the lookup
honored repository-local config (url.*.insteadOf, credential.*, http.*,
core.sshCommand) that the fetch never saw, and a broken .git in the
caller's directory failed the lookup outright. Initialize the scratch
repository first and run every command from it, so all steps see the
same configuration. Target resolution uses a short-lived scratch
repository of its own and still creates no checkout.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The native Git runner SIGKILLed its child's process group after every
command, including successful ones. Git spawns credential-cache--daemon
into the same group, so each ls-remote or fetch destroyed the cache it
had just warmed and every later command re-ran the full helper chain.
Kill the group only on timeout, cancellation, or failure, and drop the
redundant kill/wait on an already-reaped child.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The hardened -c list for CLI-owned Git acquisition disabled hooks, LFS
filters, submodules, and maintenance but omitted core.fsmonitor, so a
user's global fsmonitor hook (or the builtin daemon) still ran during the
temporary checkout. Match the sandbox's hardening and cover it in the
hooks/filters test.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Remove validation that ran twice on the same inputs: clap already
enforces the flag co-occurrence rules, and the native Git layer no
longer re-checks selectors, branch names, refs, and commit SHAs that
selection parsing already validated. Remote selector shape rules now
delegate to the shared WorkflowPath validator.
Reuse fabro_proc for the process-group kill and liveness probe instead
of calling nix directly, dropping the extra nix features. Fold the
duplicated branch/tag candidate derivation into one RefCandidates type,
label each Git command explicitly instead of inferring it from argv,
hoist the duplicated workflow resolver call in create_run, and merge the
two directory target arms now that the default is just the caller path.
Share the run-argument parser and workflow/commit fixtures across the
unit tests through a test_support module, drop an integration test that
duplicated one cell of the cross-product test, and make the malformed
slug vectors assert the clap rejection they exercise.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Add a `sandbox_tests!` scenario that initializes a repository inside the
sandbox from a script stage, commits, and prints the author and committer
the commit object carries. It runs on the local host and, when the plugin
executables are on PATH, on the host and Docker sandbox plugins, with
conflicting `GIT_*` variables inherited from the launching shell.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
A run now resolves a single author and committer identity once, after its
GitHub credentials are selected and before anything can commit, and uses it
for every commit it creates. Resolution order: a complete explicit
`run.git.author`; the run's GitHub App bot account
(`<slug>[bot] <id+slug[bot]@users.noreply.github.com>`); the authenticated
user of the run's PAT; the generic `Fabro <noreply@fabro.sh>`. A partial
explicit author overlays the fields it supplies. Only the selected
credential is consulted; a failed lookup is a setup error. A standalone
installation token falls back to the generic identity with a warning.
The resolved identity is carried on `RunOptions` and `EngineServices`,
recorded as a `git.identity.resolved` event and `RunProjection.git_identity`
so resume reuses it, and exposed through the run state API. Engine
checkpoints and metadata commits read it through `RunOptions::git_author`.
Every workflow execution path receives it as `GIT_AUTHOR_NAME`,
`GIT_AUTHOR_EMAIL`, `GIT_COMMITTER_NAME`, and `GIT_COMMITTER_EMAIL`, applied
last so it wins over inherited host variables and `[run.environment]`
entries: prepare steps, command stages, native agent shell tools, and ACP
launches. The identity is injected even without a Git origin, and the old
local `git config user.*` write is removed.
fabro-github gains `GET /user` and `/users/{slug}[bot]` lookups with mocked
tests for success, unauthorized, malformed, and transient cases. Real-Git
integration tests commit in the primary checkout, a clone, and a fresh
repository under conflicting local config, `[run.environment]`, and host
variables, and prove concurrent runs do not leak identities. CLI workflow
tests cover host script stages and ACP launch env through `fabro run`.
Docs and generated option metadata now describe the credential-derived
defaults instead of the stale `fabro`/`fabro@local` values.
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>
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.
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>
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>
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>
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>
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>
Each behaviour the pebble backend owes a run now has one test that drives
it through the workflow engine against a scripted OpenAI-compatible model:
- an agent stage under every harness profile (openai, anthropic, claude-5,
gemini, kimi) writing a file with that profile's own tool spelling, with
the event sequence, files touched, response, usage, and cost checked; the
codex vocabulary applies a patch through the OpenAI twin's custom tool
call, which `TwinToolCall::custom` now scripts
- steering delivered mid-stage, an interrupt with a steer, run cancellation,
and the executor-enforced stage timeout
- a question answered through the interviewer, a subagent whose events carry
its parent's session id, and an MCP tool served by a stdio server
- failover to a second provider after a tool ran, continuing the recorded
conversation without running the tool again
- a failing event sink ending the stage with the sink's error
- Ask Fabro resuming a stored record across turns, with the cursor moved
past the run's event log when the record's own cursor fell behind
- Docker and Daytona smokes running an agent stage through the provider
sandboxes
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
SandboxSpec had a Local variant beside the provider spec, and a local
sandbox was created by hand over a bare Host provider: no workspace, no
provider connection, its own reconnect, and its own push rule for the
designated directory. The local kind is now one more SandboxSpec:
SandboxSpec::local names the directory on a HostDirectory spec with a
skip clone, and provider_sandbox builds it like a plugin kind, creating
the directory when missing since the Host provider requires it to exist.
Every RunSandbox carries a workspace; a handle wrapped as is gets the
workspace of its own working directory.
The push rule is one rule for every checkout: a checkout fabro cloned
pushes with the credentials it was cloned with, and any other checkout
pushes when it has an origin, with whatever credentials it carries. A
local run therefore pushes the same way before and after a resume;
before, a reconnected local sandbox carried an attached workspace that
never pushed while a fresh one did.
Reconnect uses the recorded id for every kind. The recompute of a local
id from its directory, kept for records written before directories had
ids, is gone, and test fixtures that wrote made-up local ids derive them
through test_support::local_sandbox_id instead. A local run's record now
carries its workspace layout like every provider-chosen directory, and
the sandbox.initializing event precedes the driver's create events for
local as for every other kind.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
RunSandbox grew its lifecycle methods one adopter at a time and ended up
with several names for each step. Reconnecting from a run record had
four entry points (reconnect, reconnect_for_run,
reconnect_for_run_with_events, reconnect_driver_for_run) that all
forwarded to the last one. Bringing a sandbox back had two (start and
activate) over the same make_ready, and releasing it had two (delete and
cleanup) over the same release. Two more methods had no callers at all:
set_autostop_interval, which nothing set after the driver took over
lifecycle timers, and resume_setup_commands, which resume stopped using
when checkout moved to the git facet.
There is now one of each. reconnect_for_run takes the record, the
provider access, an optional run id, and an optional event context;
callers that need none pass None. activate is the single "make usable"
step: a running sandbox only learns its platform when it has not yet, a
stopped or paused one is started and its Bash verified, and resume calls
it like every access-time caller. delete is the single release; for a
designated host directory it frees the handle and leaves the directory in
place, as cleanup did. The tests and server call sites follow the
renames; behavior is unchanged except that resuming an already running
sandbox no longer re-runs the Bash probe.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
format_lines_numbered renders a file the way the agent's Read tool shows
it: every line prefixed with its number, from an offset for a limit. That
is the agent's presentation, not a sandbox concern, and it only lived in
fabro-sandbox so RunSandbox::read_file(path, offset, limit) could call
it. The function now lives in fabro-agent next to the Read tool, with its
tests, and the Read, ReadManyFiles, and Kimi ReadFile tools number the
text they get from read_file_text themselves. RunSandbox::read_file goes
away; the sandbox returns bytes or text and nothing else.
fabro-sandbox also re-exported shell_quote through a one-line wrapper so
callers could reach it from the sandbox crate or from fabro-agent. The
audited implementation is fabro_util:🐚:shell_quote; the six
importers now use it directly and the wrapper and both re-exports are
gone.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Replace fabro's hand-written agent loop with pebble's `CodingAgent` and
delete the `fabro-agent` crate.
Workflow: `PebbleBackend` builds one agent per stage over `RunSandbox`,
binds the stage's hooks as tool middleware, the interviewer as the
human-input provider, and a durable `EventSink` that writes every agent
event through the run event log before the agent goes on. Full-fidelity
threads continue across stages through `export`/`resume_from_export`.
Model failover takes the session record after the failed prompt and
continues it on the next route with `ResumeMode::UseModel`, so no tool
effect repeats. The steering hub targets pebble's control handle, with
a steering lease holding completion open while a human is paired.
Events: `EventBody::Agent` carries pebble's `CodingAgentEvent` envelope;
the per-variant bodies, the transcript projection, and the fabro-only
context-window, tool-summary, and skill types are gone in favor of
pebble's. The OpenAPI schemas, generated Rust and TypeScript clients,
and web readers follow.
Ask Fabro: the session runs a `CodingAgent` under a read-only permission
policy and a system prompt transform. Its conversation lives in a new
`run_session_records` table and resumes on the recorded model with the
event cursor advanced past the run log.
`fabro exec` builds the same agent over a local sandbox with pebble's
permission middleware and an interactive approval service.
The catalog fills in `metadata.agent.profile` for operator providers
that declare none, so pebble's lookup is the one resolution path.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
A dry run's stored history ends with the sandbox stop, which now carries
the driver's event instead of fabro's provider and duration fields.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>