Phase 5 of the AWS ECS/Fargate host (spec docs/superpowers/specs/2026-11-13-aws-ecs-host-design.md, plan docs/superpowers/plans/2026-11-15-aws-fargate-workspaces.md).
What it does
A project can run as a Fargate task on an environment from phase 4.
Add Project / Config tab: choose an AWS connection, region and environment (joined, ready ones only); repos (URL, branch, folder) replace host folders; CPU/memory (valid Fargate pairs), x86/ARM, storage Hybrid (default) or Ephemeral, ephemeral size, Spot, idle stop, push-unsaved-work-on-stop (default on). Docker access, VPN, port mappings and the local LLM gateway show as unavailable on AWS, with the reason.
Start registers the project's task definition (only when its fingerprint changed), puts everything project-shaped into one Secrets Manager secret (task definitions are readable by every workspace user, so they hold nothing but secret references), creates the Hybrid EFS access point, runs the task and connects through the SSM tunnel to the in-container agent. A running task is reattached, never duplicated.
The task outlives the app. Reopening reconnects (even without Docker); the AWS death watch maps stop reasons (Spot, failed to start, idle).
Stop guard: checks every repo for uncommitted/unpushed work and offers to push it to triple-c/wip/<date> (default), stop without pushing, or cancel.
In the container (TRIPLEC_HOST=aws): secret files are unpacked before the agent starts, repos clone in the background, Hybrid keeps ~/.claude, ~/.claude.json and ~/.ssh on EFS, idle stop runs in the task (real I/O only; never stops with unsaved work), and SIGTERM pushes WIP when enabled.
Policies: new workspace-user-tasks.json (register own task definitions, read own secrets). Users can't retire task definitions (AWS only allows that on *), so removal leaves them for an admin.
The permission check gets a 2-minute limit for AWS throttling, and Settings says it can take a few minutes.
Live (us-west-2, as a principal holding only the workspace-user policies): cold start ~94–104 s, HEALTHY ~135 s, reattach 2 s with no second task, clone, ~/.claude persists across restarts, guard dry run, SIGTERM path, idle stop. Account left clean; ≈$0.15.
Bubblewrap can't create namespaces on Fargate, so the Claude Code sandbox is off on AWS.
Still to verify live
The final idle-stop path (held-open terminal, push-before-stop, idle exit reason) — the extended live test is written; it needs a run with fresh credentials.
Phase 5 of the AWS ECS/Fargate host (spec `docs/superpowers/specs/2026-11-13-aws-ecs-host-design.md`, plan `docs/superpowers/plans/2026-11-15-aws-fargate-workspaces.md`).
## What it does
A project can run as a Fargate task on an environment from phase 4.
- **Add Project / Config tab:** choose an AWS connection, region and environment (joined, ready ones only); repos (URL, branch, folder) replace host folders; CPU/memory (valid Fargate pairs), x86/ARM, storage **Hybrid** (default) or **Ephemeral**, ephemeral size, Spot, idle stop, push-unsaved-work-on-stop (default on). Docker access, VPN, port mappings and the local LLM gateway show as unavailable on AWS, with the reason.
- **Start** registers the project's task definition (only when its fingerprint changed), puts everything project-shaped into **one Secrets Manager secret** (task definitions are readable by every workspace user, so they hold nothing but secret references), creates the Hybrid EFS access point, runs the task and connects through the SSM tunnel to the in-container agent. A running task is reattached, never duplicated.
- **The task outlives the app.** Reopening reconnects (even without Docker); the AWS death watch maps stop reasons (Spot, failed to start, idle).
- **Stop guard:** checks every repo for uncommitted/unpushed work and offers to push it to `triple-c/wip/<date>` (default), stop without pushing, or cancel.
- **In the container (`TRIPLEC_HOST=aws`):** secret files are unpacked before the agent starts, repos clone in the background, Hybrid keeps `~/.claude`, `~/.claude.json` and `~/.ssh` on EFS, idle stop runs in the task (real I/O only; never stops with unsaved work), and SIGTERM pushes WIP when enabled.
- **Policies:** new `workspace-user-tasks.json` (register own task definitions, read own secrets). Users can't retire task definitions (AWS only allows that on `*`), so removal leaves them for an admin.
- The permission check gets a 2-minute limit for AWS throttling, and Settings says it can take a few minutes.
## Testing
- `cargo test --locked`: 1,344 passed · `vitest`: 1,281 passed · `tsc`/build clean · agent crate tests pass · image builds.
- Live (us-west-2, as a principal holding only the workspace-user policies): cold start ~94–104 s, HEALTHY ~135 s, reattach 2 s with no second task, clone, `~/.claude` persists across restarts, guard dry run, SIGTERM path, idle stop. Account left clean; ≈$0.15.
- Bubblewrap can't create namespaces on Fargate, so the Claude Code sandbox is off on AWS.
## Still to verify live
The final idle-stop path (held-open terminal, push-before-stop, idle exit reason) — the extended live test is written; it needs a run with fresh credentials.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
workspace_env() builds every project-shaped KEY=VALUE pair (backends, CA vars,
MANAGED_AUTH_KEYS blanking, custom env, instructions, Claude Code settings);
create_container keeps COLORTERM, HOST_UID/GID and the host lookups (CA
resolution, keychain token) and formats the pairs. Golden vectors recorded from
the pre-refactor block pin the order and values.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
aws::workspace prepares an AWS project before its task runs: one
Secrets Manager secret holding every project-shaped value (P1; files
as one TRIPLEC_FILES key per R1), the Hybrid EFS access point, and a
task definition whose environment is only TRIPLEC_HOST and COLORTERM
(R2), with every payload key as a valueFrom secret reference.
A revision is reused only when it carries the caller's owner tag, this
project's tag and the expected fingerprint label (other users can
register into any triple-c-* family), always by its exact
family:revision ARN. Payload keys and secrets entries are sorted so
unchanged settings never register a new revision. Names (family,
secret, access point root, log stream prefix, file system token) live
in tofu_modules and use owner_key.
Adds aws-sdk-efs and aws-sdk-cloudwatchlogs (released with the pinned
aws-sdk-ecs; lock gains only their own entries).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- The fingerprint hashes a canonical rendering of the built request
(Debug-redacted environment and HashMap log options rendered
separately, labels excluded) instead of the plan plus a template
constant, so a builder change can't silently keep old revisions.
- Page ListTaskDefinitions so a longer family can't crowd out the
caller's revisions.
- Project ids that aren't [A-Za-z0-9-] get a hash suffix in names, so
ids that sanitise alike can't share a secret.
- Name a secret scheduled for deletion; only mount an available file
system; drop the unused FileSystemRef.arn.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
aws/tasks.rs: TaskApi (+ SdkTaskApi) with RunTask, DescribeTasks (tags),
ListTasks by family, StopTask and a GetLogEvents tail. find_or_run reuses
the project's live task only when its tags name the caller and the
project, and otherwise runs the exact family:revision with the three
ownership tags (FARGATE, or the FARGATE_SPOT capacity provider for Spot),
with a client token so SDK retries cannot start a second task.
wait_ready polls with injectable timing and fails with the stop reason
and the last 50 log lines. Quota refusals become AwsError::QuotaExceeded
and are never retried. classify_stop maps stop codes per the spec.
host/watch.rs: ExitSource::Aws polls an injected project-to-task lookup
every 15 s, per connection+region+cluster, 100 tasks per call; a group
that can't be asked is skipped. has_exited's Agent arm is unchanged (R3).
container_watch::run/spawn take their ExitSource.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
RuntimeApi (GetParameter, GetRole, GetSecretValue, DescribeAccessPoints,
task definition retirement, DeleteSecret with a 7-day window,
DeleteAccessPoint) with the decisions in tested functions over it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ProjectHost::Aws and the tunnel registry: start looks for the project's
live task before preparing (a running task is attached to with the token
read back from its secret, never rotated), waits for a new access point,
runs, waits, tunnels and checks the agent; a task the start ran is stopped
on any later failure. Stop, remove (resources; data only on request),
reconcile (background re-attach) and the AWS death watch, with exits
confirmed through the registry. Docker-only Reset, migration, staleness
and container info refuse or skip AWS projects; an AWS project with
resources can't be moved off its host. Shutdown closes every tunnel.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A start stores the task ARN when RunTask returns (or the task is found),
reconcile finds an unrecorded live task, and a failed attach leaves the
project in Error rather than hiding a running task. The winner of a start
race puts its agent token back in the secret. A background re-attach
registers only if the project still runs that task and was not stopped
meanwhile. Removing a project whose connection is gone needs explicit
confirmation. Names come from tofu_modules; files are read with a hard cap.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ensure_started cancels a background re-attach instead of waiting for it,
and every register bumps the project's generation, so a late re-attach can
never replace a newer tunnel. A task StopTask refused after a failed start
keeps its ARN. A re-attach that gives up clears the pending flag only if
still current; close_if_task checks and takes under one lock.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
With TRIPLEC_HOST=aws the entrypoint decodes TRIPLEC_FILES into
/tmp/.host-ssh and /tmp/.host-ca and unsets it (and TRIPLEC_REPOS) before
the agent's environment snapshot; links ~/.claude, ~/.claude.json and ~/.ssh
into /mnt/triple-c-home for Hybrid storage; clones TRIPLEC_REPOS as claude
after git credentials and ~/.ssh are set up; runs triple-c-wip-push from the
TERM trap; and starts triple-c-idle-watch, which signals the entrypoint
itself (PID 1 is docker-init under initProcessEnabled). Docker mode is
unchanged: every new step is gated on TRIPLEC_HOST=aws.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Clone repos in the background after the agent starts (progress in
/run/triple-c-clone; an unfinished clone keeps the workspace from idling),
give the task definition stopTimeout 120 for the SIGTERM WIP push, reserve
the TRIPLEC_ env prefix on both hosts, and stop root writing through
claude-controlled links (~/.claude.json written as claude only via the exact
Hybrid link; ~/.ssh merged only through the exact Hybrid link, Docker paths
unchanged). Keep the user's ssh command and add BatchMode, cover dot-folders,
refuse undecodable or over-long file names by name, and upload LFS objects
before the --no-verify push.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
check_unsaved_work runs triple-c-wip-push --dry-run through the agent as
claude (checked:false with the reason when the workspace can't be asked);
stop_project_container takes wip: push runs the push first and a failure
aborts the stop with its output. aws_task_log reads the current or last
task's stream in the environment record's log group. remove_aws_resources
removes a stopped project's AWS resources and clears its runtime handles,
so it can move off AWS. add_project takes the host fields; an AWS project
takes repositories, no folders, and no runtime handles from the caller.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Add Project picks Local or an AWS connection, a region and a ready,
joined environment, with repositories instead of folders. The Config
tab's Host section edits CPU/memory (valid pairs only), architecture,
storage, ephemeral size, Spot, idle stop, WIP push and repositories, and
can remove the AWS resources or move the project back to this machine.
Docker access, VPN, port mappings and the gateway backend are disabled on
AWS with the reason; the sandbox shows Off on AWS for now. Stop on an AWS
workspace with unsaved work asks first (Push to WIP branch preselected);
a failed start shows the task's last log lines; removing an AWS project
can delete its saved files, and asks again when its connection is gone.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The stop guard's choice now reaches the workspace: after a successful
push (or for Stop without pushing) the app records pushed/discard in
/run/triple-c/stop-reason as root, and the entrypoint's shutdown hook
then skips its own push; a marker that can't be written is reported
and the stop goes on. The push runs before the auth bridge is stopped;
a guarded stop of a workspace that already stopped succeeds. The
Fargate table allows only 512/1024/2048 MiB with 256 CPU, pinned on
both sides by a shared fixture. Removal's missing-connection refusal
carries a stable prefix. OpenAI Compatible is allowed on AWS, with a
warning for local addresses and the app's own gateway. HostSection
locks resource actions unless Stopped, the add dialog drops the
environment on a region change, and the repository editor keeps rows
in progress across a refresh.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A failed StopTask removes the stop marker it recorded, so a later stop
pushes as configured. The unchecked guard's Stop anyway sends no choice
and records nothing, so the workspace's own shutdown push still runs as
the dialog says. The idle watcher keeps a pushed/discard marker instead
of overwriting it; the remove-resources dialog sends discard only when
there may be saved files.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
EFS authorizes the elasticfilesystem:TagResource that a tagged
CreateAccessPoint needs against the parent file system's ARN, not the new
access point's: a workspace user's first Hybrid start was denied
("not authorized to perform: elasticfilesystem:TagResource on the
specified resource"). Verified live with a probe role: access-point/*
is denied; file-system/* is allowed, as is * with each of
elasticfilesystem:CreateAction, aws:RequestTag and aws:TagKeys.
TagOwnAccessPointsAtCreate now names file-system/* with the same
conditions (CreateAction = CreateAccessPoint, owner request tag, the
three tag keys); its check and owner-scoped test follow.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
aws::live_tests::environment::a_fargate_workspace_round_trips (ignored):
a public environment deployed by the environment admin, a role holding
only the workspace-user files onboarded to it and driving ProjectHost::Aws
through a profile connection: start (Hybrid, 1 vCPU / 2 GB, one public
repo), exec, background clone, ~/.claude persistence across stop/start,
attach without a second RunTask, the stop guard's dry run, the SIGTERM
WIP path, idle stop at 5 minutes, bubblewrap, denials on another owner's
revision and secret, and project removal. TRIPLEC_E2E_IMAGE names the
image; a private ECR image gets a test-only pull policy on the exec role.
A guard stops tasks, retires revisions, deletes access points and the
secret before the fixture destroys the modules.
aws/README.md gains the phase 5 results; the spec's To verify items are
answered. Open: DeregisterTaskDefinition is authorized against * only,
so the workspace tier cannot retire revisions (the test's last assertion
fails until that is decided).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Ruling R8. ECS authorizes ecs:DeregisterTaskDefinition against * only,
with no tag condition (verified live), so the workspace tier's
RetireOwnTaskDefinitions could never match, and allowing it on * would
let any user deregister any revision in the account. DeleteTaskDefinitions
is resource-level but deletes only INACTIVE revisions, so it goes too.
Removing an AWS project now stops the task, schedules the secret and
(with discard) deletes the access point, and leaves the task definition
revisions registered, saying so in the log: they hold no secret values,
cost nothing, and RunOwnTasks still needs the owner tag. aws/README.md
tells admins how to deregister them. The policy statement, its checks and
owner-scoped rows, the RuntimeApi methods, the fakes, the removal UI copy
and the live test's final assertion follow.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Review round 1 of Task 9:
- the phase-5 body runs under a 45-minute timeout and each exec under
180 s, so a hang becomes a panic the guard catches; the body future is
boxed (moved into the timeout it overflowed the test thread's stack,
which aborts without any guard);
- scripts/aws-e2e-workspace.sh builds and pushes the image to a throwaway
ECR repository, runs the test and deletes the repository in an exit
trap; the test's guard deletes it too (TRIPLEC_E2E_ECR_REPO);
- the guard survives a poisoned lock, closes the tunnel and restores
AWS_CONFIG_FILE;
- new live probe: a bare elasticfilesystem:TagResource on the user's own
file system is denied; README records it and the final run (R8 removal
end state), and the admin command uses xargs -r.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Removing a project's AWS resources now deletes its secret with
ForceDeleteWithoutRecovery (R10) so the next start can create it again,
and the EFS option is renamed: it removes the access point, while the
files stay on the user's own file system until offboarding (R9).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- The agent marks requests with X-Triple-C-Background: 1 (and the
attach/tunnel sessions they open) as not activity (R11).
- triple-c-idle-watch no longer requires zero open execs/tunnels: idle is
last_activity older than N minutes, no claude, no scheduler job, no
running clone, and no unsaved git work. With the WIP push on, it pushes
as claude before signalling and stays up (backing off) when the push
fails; with it off, dirty repos keep the workspace up.
- An idle stop exits 75 so ECS reports it distinctly from a crash; the
TERM trap no longer pushes again after the watcher's push.
- start_clone_repos writes done from an EXIT trap.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The auth bridge reads /proc/net every 2 s; on the agent that now goes
through ContainerTarget::exec_oneshot_background, which sends
X-Triple-C-Background: 1 so the agent does not count it as activity.
Docker behaviour is unchanged. The idle-stop hint says what idle means
now (R11) and what happens to unsaved work.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- classify_stop is wired into the death watch: stopped_among returns
each task's StopKind, the tunnel registry keeps it, and ContainerExited
carries a StopNote. Spot -> 'Spot interruption'; TaskFailedToStart ->
Error with the reason; the entrypoint's idle exit (75, read from the
workspace container's exit code) -> 'stopped after being idle'; a user
stop is applied silently (P8). is_quota_error, dead, moved to tests.
- The SDK config has a TimeoutConfig (30 s operation / 10 s attempt); the
watch's per-group poll and StopTask also have tokio bounds.
- Watcher tests cover silent sessions, unsaved work with the WIP push on
and off, a failing push and the idle exit status; docs updated.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- New reconcile_aws_projects command (handler, capability, TS wrapper)
that App runs on mount regardless of Docker; reconcile_project_statuses
now skips AWS projects. Projects reconcile concurrently, 4 at a time,
each still bounded at 15 s.
- reconcile_task falls back to find_project_task when the stored task is
gone or stopped, before calling the project Gone (m3).
- An AWS project with no registered tunnel gets a placeholder target id
(http://unattached.invalid/<id>/) instead of the last run's loopback
URL, so a reused port can't match the wrong project; the death watch
still reports it by that id until a re-attach registers (m9).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- validate_repo_url rejects https URLs with userinfo (user:pass@ or
token@), pointing at the project's Git HTTPS token setting (m4).
- validate_branch rejects HEAD (m5).
- The FIXED_ENVIRONMENT doc comment sits on FIXED_ENVIRONMENT again (m6).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The task has no ~/.aws from this machine and SSO can't refresh there;
the note points at static keys or a bearer token (m10). The auth bridge
stays enabled on AWS (its poll is now a background request).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
It unsets every caller AWS_* variable (keeping only where the profile's
files are, now documented), sets AWS_PROFILE explicitly, attempts the ECR
delete in the EXIT trap unconditionally (RepositoryNotFound tolerated,
bounded at 120 s), and exits 130/143 on INT/TERM (M21).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The idle wait now keeps an attach open (silent) and runs the auth
bridge's background exec every 2 s; the task must still stop itself and
ECS must report it as an idle stop (exit 75 -> StopKind::Idle).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The connection-wide 30 s operation limit counts retry backoff, so a
throttled SimulatePrincipalPolicy got about six of its ten adaptive
attempts. The IAM client used by the check now has a 120 s operation
limit, and Settings tells the user a check can take a few minutes.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- clone_repos records `<folder>\tcloning` as each clone begins; triple-c-wip-push
skips a folder whose last state is `cloning` while the clone is not done (dry run
reports "cloning": true; RepoState/TS carry it, the stop guard lists it as
"still cloning — nothing to push" and never asks because of it).
- The idle watcher re-checks quiet() after a successful push and goes back to
watching if someone came back during it.
- The entrypoint installs its stop trap before the idle watcher and the background
clone start, so a TERM in between still runs aws_before_stop.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The guard pushed and recorded `pushed` while terminals (and Claude in them) were
still open; the TERM hook then skipped its push, so later edits were lost. Stop now
closes the project's exec sessions and auth bridge first, then pushes, records the
marker and stops the task. A failed push still aborts the stop with the status
restored; the auth bridge is re-armed and the error says the terminals were closed.
The script also runs under `/usr/bin/timeout -k 5` ending 10 s before the client's
timeout: the agent does not kill an exec its client gave up on.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The death watch's stopped marks are keyed by target id, and an unattached AWS
project's id (http://unattached.invalid/<id>/) is reused by every later unattached
spell, but only an endpoint URL's mark was ever removed. A stale mark could make
the watch report a live, billed task as stopped. Registering a tunnel now clears
the project's placeholder mark, and reconcile forgets it before asking ECS.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Its 2 s sha256sum poll now carries X-Triple-C-Background, so an open viewer does
not keep an idle AWS workspace up. Opening and saving a file stay foreground;
Docker is unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
remove_aws_resources cleared access_point_id even without discard_data, though the
access point stays; the project could then move hosts and orphan it. The id is
now kept, so a later start reuses it, a later removal with "Also remove the
access point" deletes it, and moving the project is refused until then (the
message and the Host section say so).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Phase 5 of the AWS ECS/Fargate host (spec
docs/superpowers/specs/2026-11-13-aws-ecs-host-design.md, plandocs/superpowers/plans/2026-11-15-aws-fargate-workspaces.md).What it does
A project can run as a Fargate task on an environment from phase 4.
triple-c/wip/<date>(default), stop without pushing, or cancel.TRIPLEC_HOST=aws): secret files are unpacked before the agent starts, repos clone in the background, Hybrid keeps~/.claude,~/.claude.jsonand~/.sshon EFS, idle stop runs in the task (real I/O only; never stops with unsaved work), and SIGTERM pushes WIP when enabled.workspace-user-tasks.json(register own task definitions, read own secrets). Users can't retire task definitions (AWS only allows that on*), so removal leaves them for an admin.Testing
cargo test --locked: 1,344 passed ·vitest: 1,281 passed ·tsc/build clean · agent crate tests pass · image builds.~/.claudepersists across restarts, guard dry run, SIGTERM path, idle stop. Account left clean; ≈$0.15.Still to verify live
The final idle-stop path (held-open terminal, push-before-stop, idle exit reason) — the extended live test is written; it needs a run with fresh credentials.
🤖 Generated with Claude Code
EFS authorizes the elasticfilesystem:TagResource that a tagged CreateAccessPoint needs against the parent file system's ARN, not the new access point's: a workspace user's first Hybrid start was denied ("not authorized to perform: elasticfilesystem:TagResource on the specified resource"). Verified live with a probe role: access-point/* is denied; file-system/* is allowed, as is * with each of elasticfilesystem:CreateAction, aws:RequestTag and aws:TagKeys. TagOwnAccessPointsAtCreate now names file-system/* with the same conditions (CreateAction = CreateAccessPoint, owner request tag, the three tag keys); its check and owner-scoped test follow. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>