* feat(core): add prs[] array to Session for multi-PR support — Phase 1 metadata layer
* feat(core): update lifecycle-manager for multi-PR per session — Phase 2
* feat(web): add prs[] to DashboardSession and wire up multi-PR display — Phase 3
* feat(plugins): append to prs metadata field on gh pr create — Phase 4
* fix: address all code review issues — PR number, enrichment pipeline, claimPR append, stale PRs, V1 compat, SCM calls, dead code, isDraft, session validation
* fix: address Greptile review — state machine aggregation, URL dedup, React keys, review comments, typecheck
* fix: remove useless assignment in lifecycle-manager — lint error
* fix: add missing prs field to Session objects in cli and plugin test files
* fix: add prs field to integration-tests helpers and fix V1 flat-file fallback in agent-claude-code
* fix: use stack approach for claimPR prs field — claimed PR becomes primary
* feat(core,web): multi-PR per session — enrichment, status dots, bug fixes (#1821)
- Session metadata now stores a comma-separated prs list; all PR URLs
parsed into session.prs[] on load, backwards-compatible with single-PR
- Enrichment loop hydrates every PR in the list each poll cycle via the
existing batch GraphQL call
- Fixed closed-PR filter reading from session lifecycle state instead of
per-PR enrichment cache, which misclassified secondary PRs
- Fixed isDraft never propagating from enrichment cache to dashboard.pr
and dashboard.prs[i]
- Fixed prs[0] and dashboard.pr being separate objects — enrichment on
one now syncs to the other
- SessionCard PR badges now show a colored status dot per PR: green (CI
passing/merged), yellow (CI pending), red (CI failing/changes
requested), grey (unenriched/draft/closed)
- Corrected color token mapping: passing uses --color-status-merge
(green), pending uses --color-status-pending (yellow)
* fix(core): add missing prs field to code-review-manager test fixture
* feat(web): per-PR rows with colored chips, titles, and click-to-switch on session card
- Replace single-PR badges with one row per PR when session has multiple PRs
- Each row shows: status-colored chip, PR title (truncated), diff size
- Chip colors: green (CI passing/merged), yellow (CI pending), red (CI
failing/changes requested), grey (closed/draft/unenriched)
- Repo initials badge (e.g. AO, CM) shown per row when PRs span multiple repos
- Clicking a row selects it — description, alerts, and action buttons in the
card body all switch to that PR's context
- Clicking the chip opens GitHub; clicking title/diff area selects without
navigating away
- Single-PR sessions are completely unchanged
* fix(web): address multi-PR card review — effectivePR for merge button, color-mix chips, left-border selection
* fix(web): derive prs from pr in makeSession test helper
* fix(core): fall back to raw[pr] when prs absent in claimPR
* feat(web): PR switcher in session detail header for multi-PR sessions
* fix(core): treat ciStatus/reviewDecision 'none' as passing in multi-PR aggregation
* fix(core): stale-session cleanup checks all prs before killing multi-PR session
* fix(core): all-none reviewDecision aggregates to none, not approved
* fix(core): prs accumulation in Node hook, bump WRAPPER_VERSION, clear stale enrichment on claimPR
* fix(core,web): webhook matching for secondary PRs, restore lifecycle PR number fallback
* fix(web): merge endpoint resolves target PR from session.prs for secondary PRs
* fix(core,web): clamp selectedPRIndex, guard multi-PR partial cache miss, clear prs on takeover
* fix(core,web): scope webhook secondary PR match by owner/repo, handle JSON metadata in Node hook
* fix(web): guard merge route PR lookup by owner/repo to prevent number collision
* fix(web): forward owner/repo from merge button to activate PR disambiguation guard
* fix(web): propagate owner/repo through BottomSheet and AttentionZone merge triggers
* test(web): update onMerge assertion to expect owner/repo arguments
* fix(core,web): resolve 5 multi-PR logical gaps — conflict detection, partial cache miss, isDraft aggregation, event context prs, AttentionZone labels
* fix(core,cli,plugins): add prs field to NotificationEventContext fixtures across all packages
* fix(web): add owner/repo guard to primary PR branch in webhook session match
* fix: handle unknown update versions and flat interactive config
* ci: pin GitHub Actions to SHAs
* fix: support new orchestrator from flat config
* ci: skip dependency review on unsupported forks
* revert: run dependency review on all PRs
* docs(design): add dashboard design-language exploration + mockups
Capture the design-language direction explored for the AO dashboard as
live HTML mockups plus rationale, under docs/design/.
- dashboard-language.md: the language — blue=orchestrator / orange=agents
identity, rationed semantic color, Schibsted Grotesk + JetBrains Mono,
unified status system, layout patterns, and how it diverges from DESIGN.md.
- mockups/kanban.html: canonical fleet board (frameless lifecycle columns).
- mockups/session.html: canonical session detail (framed xterm + pluggable
inspector rail: Summary / Changes / Browser; review-comment "Address").
- mockups/{concepts,refined,*-icons}.html: exploration / icon comparisons.
- mockups/mascot.png: the blue conductor mascot.
Reference mockups only (CDN fonts + inline styles); production must use the
Tailwind tokens in globals.css per C-02/C-07.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(web): migrate dashboard to mission-control design language (supersedes DESIGN.md)
Replace the "Warm Terminal" system with the "Mission Control" language from
the merged design exploration (docs/design/dashboard-language.md + the kanban
& session mockups). One coherent system, dark control-room aesthetic, rationed
color (blue = orchestrator/you, orange = working agent, amber = needs-input,
red = failing/stuck, green = mergeable, else grayscale).
Tokens & fonts
- globals.css @theme/.dark: add literal palette (--bg #0a0b0d, --bg-side,
--card #15171b = only bordered surface, --term #0c0d10, --line/--line-2,
text ramp --t1..--t4, --blue/--orange/--amber/--red/--green) and re-point the
existing --color-* semantic tokens to it (no token renames — alias + migrate).
Flat surfaces, no warm gradients/glows. Dark theme preserved.
- Self-host Schibsted Grotesk (UI) + JetBrains Mono (machine) via
next/font/local — no external font CDN. Geist Sans removed.
Status system (one spectrum, used everywhere)
- lib/status-spec.ts (getStatusSpec) + <StatusBadge> render the kanban card
badge, sidebar dot, and session topbar pill from a single source; working dot
breathes (CSS-only @keyframes breathe).
Fleet board (kanban.html)
- Frameless tinted columns Working -> Needs you -> In review -> Ready to merge
with per-column semantic top-glow; the card is the only bordered surface
(hairline ring, no status left-rail). Sidebar-shows-all-projects + SSE 5s
unchanged.
Session detail (session.html)
- Framed xterm terminal: theme object tied to tokens (orange cursor, blue
selection, token-mapped ANSI) — terminal content unstyled.
- New pluggable inspector rail <SessionInspector>: Summary (PR -> review
comments -> Activity -> Overview) / Changes / Browser. Review comments keep
the soft-blue Address action (askAgentToFix passing comment + file:line).
- Topbar: shared status pill, Kill, solid-blue Orchestrator primary; the PR
popover is now mobile-only (desktop has the inspector). Blue mascot mark.
Tests: add status-spec / StatusBadge / SessionInspector / AppMark tests; update
column-label, theme, layout-metadata and PR-location assertions; mock
next/font/local in vitest. typecheck / test / lint / build all green.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* wip(web): compact card footer + cleanup before mockup-fidelity pass
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(web): mockup-faithful redesign pass — sidebar brand, resizable panels, cleaner chrome
Addresses review feedback to follow docs/design/mockups/{kanban,session}.html closely.
Sidebar (ProjectSidebar + mc-sidebar.css)
- Brand + blue mascot mark moved to the TOP of the sidebar (was in the topbar).
- Cool sidebar background (var(--color-bg-sidebar) #08090b) — removed the warm tint.
- Removed the yellow session-count chip (plain mono count) and the Review button.
- Per-project action icons now reveal on hover only.
- Consolidated the footer's four buttons into a single Settings gear + popover
(show killed / show done / show session id / theme).
- Right edge is drag-resizable.
Board (Dashboard + AttentionZone + SessionCard + mc-board.css)
- Topbar brand removed; "Board" header + mockup subtitle; blue primary CTA.
- Frameless tinted columns with per-column glow; compact informational cards
(status badge · id · hover terminal, 2-line title, branch w/ git icon, thin
PR/CI footer). Removed inline quick-reply/alerts/CI-chips/merge+review buttons
from cards (actions live on the session page); kept hover kill + terminal link.
- Split Done card into SessionCard.parts.tsx (≤400 lines).
Session detail (SessionDetail* + SessionInspector + TerminalControls + mc-session.css)
- Topbar: ‹ Kanban back button, title with branch to its RIGHT (git icon, no odd
truncation), shared status pill, Kill, blue Orchestrator. Brand removed from the
session shell header too.
- Terminal header: removed the "Connected" status text; matches the mockup
(Terminal label · id · zoom · fullscreen). Terminal flush (no stray left margin).
- Terminal head and inspector tab bar are both 47px so their rules align.
- Inspector: PR → review comments → Activity timeline → Overview; left edge
drag-resizable. Split header helpers into SessionDetailHeader.parts.tsx.
Shared infra
- useResizable hook (pointer drag → CSS var on :root, localStorage-persisted,
no inline style); .resize-handle styles; per-screen mc-*.css loaded after
globals.css in layout.tsx; sidebar/inspector widths read --ao-sidebar-w /
--ao-inspector-w with sensible min/max + double-click reset.
typecheck / web tests (1005) / lint (0 errors) / build all green; resize verified.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(web): sidebar/topbar polish from review feedback
- Notification bell icon 13→17px (was tiny inside its 34px button).
- Sidebar project rows: count shows at rest in the right slot; per-project
action icons are now an absolutely-positioned hover-reveal group (out of
flow) so the project name keeps full width and the row stays single-height
(fixes the inflated rows / vertically-stacked icons).
- Session status dot nudged 1px to vertically center against the mono label.
- Orchestrator session page no longer renders the inspector rail (full-width
terminal — nothing to inspect).
typecheck / web tests (1005) / lint (0 errors) green.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(web): notification icon size + sidebar/topbar review fixes
- Notification bell on the session topbar was squeezed to a 6px sliver: the
text-button padding rule applied to the icon-only button inside its fixed
34px width. Give the bell its own 34px square / zero-padding rule → 17px icon.
- Remove the redundant sidebar toggle from the session topbar on desktop
(mobile keeps the drawer hamburger); the sidebar owns collapse/expand.
- Collapsed sidebar now shows an expand button so it can be reopened.
- Trim sidebar padding + session-list indent so project/agent rows get more
width.
typecheck / web tests (1005) / lint (0 errors) green; collapse↔expand + icon
size verified.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(web): sidebar session names truncate with ellipsis; rename pencil no longer overlays
- The session label was an inline <span>, so text-overflow:ellipsis never
applied — long names hard-clipped. Make it display:block so it ellipsizes.
- The rename pencil now absolutely positions at the row's right edge; at rest
the label gets the full row width, and on hover the link gains right-padding
so the text re-truncates and the pencil sits in the reserved space instead of
covering the name.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(web): project name reserves space for hover action icons (no overlap)
On hover the per-project action icons reveal over the right slot; the toggle now
gains right-padding so the project name re-truncates with ellipsis instead of
running underneath the icons — same pattern as the session rows.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(web): remove kanban topbar toggle + redesign empty state to mission-control
- The board (kanban) topbar still rendered a desktop sidebar toggle; make it
mobile-only (the sidebar owns collapse/expand), matching the session topbar.
- Empty state (Skeleton EmptyState): ghost columns now use the live board's
labels (Working / Needs you / In review / Ready to merge), 4 columns, and the
frameless tinted-trough look — removing the old dashed/warm-tinted columns and
the stale 5-category set. Orchestrator glyph recolored to the blue accent via
tokens (was hardcoded orange).
typecheck / web tests (1005) / lint (0 errors) green.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(web): show project name in titlebar, not the raw project id hash
The board topbar special-cased projects named 'Agent Orchestrator' to display
the project id (e.g. agent-orchestrator_649ba24578) — unreadable gibberish.
Always show the human-readable project name instead.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(core,web): detectPR branch guard, multi-PR prLabel aggregation, reset selectedPRIndex on session change
* fix(web): render terminal with xterm WebGL renderer (fixes broken box-drawing)
The dashboard terminal used xterm's default DOM renderer, which draws each
row as a separate DOM line and cannot tile box-drawing / block glyphs across
cells. Agent TUIs (Claude Code's bordered panels, the "N shell command"
expansion) rendered with broken/gappy frames and a grey blob — independent of
font or lineHeight (verified: even a complete font at lineHeight 1.0 breaks in
the DOM renderer).
Load xterm's WebGL renderer (@xterm/addon-webgl), which GPU-draws box/block
glyphs into each cell so frames connect and shaded regions stay solid. Loaded
rAF-deferred after open() with a DOM fallback on context-loss / when WebGL is
unavailable (headless, blocklisted GPU). This matches how superset renders
the same agent TUIs.
The WebGL addon only ships for the xterm 6.1.0-beta train, so bump
@xterm/xterm 6.0.0 -> 6.1.0-beta.256 and the addons to the matched beta set
(the same pair superset runs in production).
Also drops the earlier no-op fontWeight/fontWeightBold/letterSpacing change —
those values equalled xterm's defaults and had no visible effect.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(web): enforce dark-mode contrast floor so agent blocks aren't grey blobs
Claude Code's expanded "Ran N shell command" block is painted on an ANSI
white background (ESC[47m). The terminal theme sets `white` ≈ `foreground`
(#c5ccd3), and dark mode used `minimumContrastRatio: 1` (no floor), so the
block's text was the same colour as its background — rendering as an
unreadable grey blob (only independently-coloured links/emoji showed through).
Raise the dark-mode floor to 4.5 (WCAG AA), matching the rationale already
used for light mode (7). xterm then auto-adjusts only the foregrounds that
fail the floor, leaving all other agent colours untouched. Verified in an
isolated harness with the real theme: white-on-white block goes from
invisible to legible.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(web): load Unicode 11 addon so emoji/wide-char widths match tmux
Agent TUIs and tmux lay out tables treating emoji like ✅/❌ as 2 cells
wide (modern Unicode). xterm defaults to Unicode 6 width tables, where
those are 1 cell — so the grid shifts a column after every emoji, breaking
table borders and leaving stray text fragments (visible only in the web
terminal; a direct tmux attach via a modern terminal renders it fine).
Load @xterm/addon-unicode11 and set unicode.activeVersion = "11" so xterm's
width tables agree with tmux/the agent. Same beta train as the other addons.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(web): rescale overlapping glyphs so out-of-font chars don't overlap text
JetBrains Mono is subset to Latin glyphs, so characters it lacks (arrows
→ ← ↔, CJK, emoji, powerline) are drawn from a fallback font whose advance
can exceed our monospace cell — the glyph then bleeds into the next cell and
overlaps the following character. Set rescaleOverlappingGlyphs so xterm shrinks
any glyph wider than its cell back to fit. Fixes the whole class of out-of-font
overflow, not just arrows.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(web): ship full-coverage JetBrains Mono (restore arrows/box/blocks)
The self-hosted JetBrains Mono was subset to ~229 Latin glyphs, dropping
arrows (→ ← ↔), box-drawing, block elements and other symbols agents emit.
Those fell back to a system font at the wrong cell width — overlapping
(pre-rescale) or shrunk/blurry (post-rescale). Replace it with the full
JetBrains Mono variable face (OFL-1.1, wght 100–800, ~111 KB woff2, same
metrics) so those glyphs render in-font, full-size and crisp at the correct
monospace width. rescaleOverlappingGlyphs stays as a safety net for the few
glyphs even the full face lacks (CJK, emoji, powerline PUA).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Laraib-1629 <laraibahmed73@gmail.com>
Co-authored-by: suraj-markup <sk9261712674@gmail.com>
Co-authored-by: itrytoohard <ayetrytoohard@gmail.com>
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
Co-authored-by: Priyanshu Choudhary <57816400+Priyanchew@users.noreply.github.com>
* feat(core): add opt-in gh CLI tracer and migrate scm/tracker plugins
Introduces execGhObserved() in @aoagents/ao-core: a thin wrapper around
execFile("gh", ...) that writes a JSONL trace row to $AO_GH_TRACE_FILE
on both success and failure. Captures status line, HTTP status, ETag,
rate-limit headers, duration, stdout/stderr byte counts, exit code, and
signal. No-op when the env var is unset, so default behavior is
unchanged.
Migrates three call sites to the observer:
- scm-github/graphql-batch.ts — PR-list guard, commit-status guard,
GraphQL batch query
- scm-github/index.ts — gh() and ghInDir() helpers
- tracker-github/index.ts — internal gh() helper
This is Phase A1a of experiments/PLAN.md: tracer infrastructure +
migration. The full GhRunner contract (Promise<GhResult>,
GhRunnerError.ghResult on reject, body capture, redaction, 64 KB cap)
lands in A1b along with the scorecard baseline.
Also adds experiments/ reference docs: the v2.3 plan, the gh-CLI call
catalog, two ETag verification writeups, and a trace harness + summary
script.
* docs(experiments): add A1a validation status and A1b blockers
Record the five A1b pre-freeze blockers surfaced by Adil's 1,487-row
baseline and an independent drill run: graphql-batch missing -i,
extractOperation flag mis-bucketing, analyzer not segmenting burn by
reset window, CLI-subcommand opacity (GH_DEBUG=api stderr vs coarse
/rate_limit bracket — not equivalent), and sessionId/projectId not
threaded through plugin callsites. Note bare gh() helper cleanup as
known-open follow-up.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix(tracer): close A1b blockers 1-4 — graphql-batch visibility, operation naming, analyzer segmentation
- Add -i flag to executeBatchQuery in graphql-batch.ts and split HTTP
headers from JSON body before parsing, making all gh.api.graphql-batch
rows visible to status and rate-limit analysis (was 186 invisible rows)
- Fix extractOperation() in gh-trace.ts to walk past -* flags before
picking the operation segment, eliminating the gh.api.--method bucket
- Add per-reset-window burn segmentation to both analyzers so runs
straddling a reset boundary produce per-window deltas instead of a
single invalid cross-reset delta
- Add experiment scripts: analyze-trace.mjs (deep trace analysis) and
drill-tracer.mjs (standalone tracer exerciser)
- Document Gap 1 decision in PLAN.md: accept CLI subcommands as opaque
for A1, bracket A2 runs with /rate_limit snapshots for coarse burn
- Add progress timeline to PLAN.md showing A→B→C track dependencies
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* docs(experiments): add A2 baseline matrix runbook
Practical execution plan for the Phase A2 scenario x scale x topology
matrix: 7 priority cells, per-cell procedure, /rate_limit bracketing
for Gap 1 subcommand burn, output format for baseline.md, and the
scorecard that gates Track B.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix(tracer): guard stderr/stdout against undefined, bound operation cardinality
Addresses code review findings:
1. Guard Buffer.byteLength and parseIncludedHttpResponse against
undefined stderr/stdout — fixes 48 SCM test regressions where
mocked execFile paths don't populate stderr
2. extractOperation() now takes only the first path segment of REST
URLs (e.g. "repos" from "repos/acme/repo/pulls/123/...") to keep
operation bucket cardinality bounded and stable across runs
3. Fix A2 runbook /rate_limit snapshots to produce valid JSON using
jq's now|todate instead of appending raw timestamp
4. Add blocker 5 dependency to runbook prereqs and per-session cells
All 140 SCM tests pass (0 failures).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* docs(experiments): add rate-limiting research artifacts
Baseline measurements, discussion notes, benchmark harness spec,
and updated master plan from two independent trace runs at 5-6 sessions.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* feat(experiments): add benchmark harness for GH rate-limit measurement
Three modes: setup (spawn sessions, wait for PRs), measure (trace API
calls over a fixed window, produce scorecard), report (recompute from
existing trace). Node.js stdlib only, shells out to ao CLI and gh CLI.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix(scm-github): handle 304 Not Modified in ETag guard catch blocks (B1)
`gh api -i` exits code 1 on HTTP 304 responses, causing the catch blocks
in checkPRListETag and checkCommitStatusETag to assume the resource changed
and trigger unnecessary GraphQL batch queries every poll cycle.
Fix: inspect stdout/stderr in the catch block for the 304 status line before
falling back to "assume changed". Also unifies the 304 detection regex to
handle HTTP/1.1, HTTP/2, and HTTP/2.0 status lines, and adds rateLimit
introspection to the batch GraphQL query.
Benchmark result (quiet-steady, 5 sessions, 15 min):
- GraphQL points/hr: 260/5,000 (5%) — down from 820–1,416 pre-fix
- ETag guard 304 rate: 100%
- GraphQL batch calls during measurement: 0
Also fixes the benchmark harness to create placeholder tmux sessions with a
claude symlink so the lifecycle actually polls sessions instead of
short-circuiting to "killed".
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* docs(experiments): update plan and notes with B1 benchmark results
B1 fix validated at 5, 10, and 20 sessions in quiet-steady state:
- 5 sessions: 260 GraphQL pts/hr (5% budget)
- 10 sessions: 640 pts/hr (13%)
- 20 sessions: 680 pts/hr (14%) — sub-linear scaling confirmed
- 50-session projection: ~800-1000 pts/hr (16-20%)
- ETag guard 304 rate: 100% at all scale points
- graphql-batch calls: 0 during measurement at all scale points
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* feat(core): log gh wrapper invocations for D1
* fix(core): preserve wrapper logging for dash-prefixed gh args
* feat(core): add gh wrapper cache for PR discovery and issue context (D4)
Add read-through caching to the ~/.ao/bin/gh wrapper, targeting the two
largest agent-side waste buckets identified in D4 analysis:
1. PR discovery (gh pr list --head): infinite TTL for positive results.
598 calls → ~10 per 10-session run (98% reduction).
2. Issue context (gh issue view): 300s TTL.
75 calls → ~20 per 10-session run (73% reduction).
The wrapper now caches successful read-only responses in
$AO_DATA_DIR/.ghcache/$AO_SESSION/ and serves them on subsequent
identical calls. Negative results (empty []) are never cached.
gh pr create populates the PR discovery cache immediately.
Also lifts PATH wrapper installation from individual agent plugins into
session-manager, making it universal for all agents including Claude Code:
- session-manager injects PATH + GH_PATH into every runtime.create()
- session-manager calls setupPathWrapperWorkspace() for all agents
- Removes duplicate buildAgentPath/setupPathWrapperWorkspace boilerplate
from codex, aider, opencode, and cursor plugins
Includes D4 implementation plans in experiments/.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* docs(experiments): add full capacity discovery (5→50 sessions) and CI churn results
Complete scaling curve measured: 50 sessions uses only ~28% of GraphQL
budget with 100% ETag guard hit rate at every scale. Poll cycle lag
identified as first bottleneck (66s at 50 sessions vs 30s target).
CI churn benchmark shows ETag invalidation is a latency problem, not
a rate-limit problem (+9% GraphQL, +4.4x p50 latency).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* docs(experiments): record real-agent catastrophe and Track D handoff
5-real-agent run on todo-app exhausted GraphQL bucket in 31 min (~9572 pts/hr,
~37x quiet-steady at the same session count). AO polling consumed ~10 calls;
the rest came from agents themselves via the metadata-only ~/.ao/bin/gh
wrapper, which has no tracing. Captures findings, adds Track D (agent-side
gh consumption) plus B5 (migrate remaining bare gh callsites to
execGhObserved), and includes the runbook + benchmark scripts Adil will
build on for the cross-machine reproduction.
* feat(core): add cache-hit/miss tracing to gh wrapper (D4)
The wrapper trace now logs a cacheResult entry for every cacheable
command: hit, miss-stored, miss-negative, or miss-error. This makes
benchmark runs conclusive — you can count cache hits vs real gh calls
directly from the JSONL trace instead of inferring from rate-limit
deltas.
Bump wrapper version to 0.4.1.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat(scm-github): replace repo-scoped Guard 1 with PR-scoped ETag checks (D4)
Guard 1 now checks GET /repos/{owner}/{repo}/pulls/{number} per PR
instead of GET /repos/{owner}/{repo}/pulls?... per repo. This means:
- Only changed PRs flow into the GraphQL batch
- Unchanged PRs are served directly from the enrichment cache
- shouldRefreshPREnrichment returns a refresh plan (prsToRefresh +
cachedResults) instead of a boolean
When 1 of 10 PRs changes, the old guard refreshed all 10 via GraphQL.
Now only the 1 changed PR is fetched; the other 9 are served from cache
at zero GraphQL cost.
Trade-off: more REST guard calls (1 per PR instead of 1 per repo), but
304 responses cost zero rate limit points.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Forward AO_AGENT_GH_TRACE to session runtimes
* revert: remove PR-scoped ETag guards (Change 3)
Reverts 25ae6013. The per-PR Guard 1 added more REST calls (1 per PR
instead of 1 per repo) without meaningful GraphQL savings at 10-session
scale. Core REST delta went from 16 to 142 while GraphQL rate stayed
flat. The repo-scoped guard is sufficient for current workloads.
Preserves the subsequent 6fc64f4f commit (AO_AGENT_GH_TRACE forwarding).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(core): use real gh binary in execGhObserved, bypass wrapper
execGhObserved() was calling bare "gh" which resolved to ~/.ao/bin/gh
(the wrapper) when that directory was in PATH. This caused:
- AO-side gh calls going through the agent wrapper
- All trace rows with aoSession=null polluting the agent trace
- Cache functions silently failing (no AO_SESSION in AO process)
Now strips ~/.ao/bin from PATH and resolves the real gh binary
(e.g. /opt/homebrew/bin/gh) at startup. Cached after first resolution.
AO process → execGhObserved → real gh → AO_GH_TRACE_FILE
Agent process → ~/.ao/bin/gh wrapper → AO_AGENT_GH_TRACE + cache
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(core): harden gh wrapper caching and agent-side tracing
Cache correctness:
- Include --json fields in cache key (prevents stale partial responses)
- Only cache stdout, not stderr (prevents warning contamination)
- Fix trailing newline inconsistency in PR discovery cache
- Support --key=value arg syntax for all cached flags
- Remove PR create cache pre-population (hardcoded fields, no JSON escaping)
- Log miss-write-failed when ao_cache_write fails (previously silent)
Agent trace improvements:
- Add operation field to invocation rows (gh.pr.list, gh.issue.view, etc.)
- Add durationMs, exitCode, ok to cache outcome rows
- Log passthrough for all non-cached code paths (pr/create, default case)
- Replace exec with child process in default case to enable post-call tracing
Bump wrapper version to 0.6.0.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(runtime-tmux): re-export PATH after shell init to survive macOS path_helper
macOS zsh runs path_helper during shell startup which resets PATH,
wiping entries set via tmux new-session -e. This caused ~/.ao/bin
to be lost, so the gh/git wrappers were never intercepting agent
calls — no caching, no tracing, no metadata auto-updates.
Fix: send `export PATH=...` via send-keys after the shell has
initialized but before the launch command, ensuring PATH sticks.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(runtime-tmux): use launch script for PATH re-export instead of send-keys
The previous send-keys approach sent 1000+ literal keystrokes for the
PATH value, which broke terminal input buffers and caused stuck quote
prompts. Instead, include the PATH export in the launch script file
which is executed directly — no terminal buffer issues.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* docs(experiments): add AO-side gh rate-limit trace report
5-session, 15-minute trace analysis with full call breakdown,
ETag guard effectiveness, anomaly investigation, and ranked
reduction opportunities.
Key findings:
- GraphQL at 41%/hr with 5 sessions (bottleneck at ~12 sessions)
- 47% of calls are individual REST fallbacks that batch should cover
- Review thread GraphQL calls (55/15min) can be folded into batch
- detectPR() and guard failures are working as designed
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* docs(experiments): add AO rate-limit reduction plan with Step 1
Step 1: Remove individual REST fallback from determineStatus().
110 calls (65 pr view + 45 pr checks) eliminated per 15-min window.
Batch enrichment covers all PRs every 30s — fallback is unnecessary
insurance for an event that never occurred in real traces.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* experiments(m2): drop agent-trace gate for claude-code
Claude Code uses native PostToolUse hooks (.claude/settings.json), bypassing
the ~/.ao/bin/gh PATH wrapper, so AO_AGENT_GH_TRACE stays empty even when
Claude makes gh calls. The previous smoke gate required AGENT_ROWS>0 and
aborted every claude-code M2 batch at smoke.
- limit-finder.sh: add REQUIRE_AGENT_TRACE env + --no-require-agent-trace flag,
gate the AGENT_ROWS integrity check behind it.
- m2-ab-run.sh: bump SMOKE_DURATION to 420s; auto-pass --no-require-agent-trace
when AGENT=claude-code; simplify smoke_check to gate only on AO_ROWS>0
(B1 lives in AO-side scm-github, measured by AO trace).
Follow-up tracked as task #39: instrument Claude's hook/tool path or document
that AO_AGENT_GH_TRACE does not cover Claude Code.
* feat(tracker-github): cache issue reads in-process (5 min TTL)
The lifecycle worker polls getIssue/isCompleted repeatedly for the same
issue across a session. Trace data from a 5-session tier-5 bench run
showed the same (repo, issue) pair fetched 64+ times with >97% duplicate
rate — ~744 of 4,059 AO gh calls in 10 minutes were redundant issue views.
Adds an in-process Map<string, CachedIssue> per createGitHubTracker()
instance, keyed by `${repo}#${id}`, TTL 5 min, bounded to 500 entries
(LRU evict-oldest on overflow).
- getIssue: read-through cache, populate on miss
- isCompleted: routes through getIssue (was a separate narrow gh call)
- updateIssue: invalidate the entry before mutating
- createIssue: unchanged, naturally populates via the existing getIssue
- Failures are not cached
Cache lives inside createGitHubTracker so each create() returns an
isolated cache (test isolation comes for free).
Expected reduction: ~744 → ~15 gh issue view calls per tier-5 run.
Tests: 41 existing + 10 new cache tests, all passing.
* feat(scm-github): cache 5 gh pr view callsites with per-method TTLs
The lifecycle worker repeatedly polls each PR for state, summary, reviews,
and review decision. Trace data showed gh pr view was the single largest
AO-side endpoint at 1,280 calls per 5-session tier-5 run with >97% duplicate
rate (e.g. PR #184 polled 86× for --json state alone in 11.5 minutes).
Adds an in-process per-instance cache inside createGitHubSCM(), keyed by
${owner}/${repo}#${prKey}:${method} so different field-sets stay isolated.
Per-method TTLs balance reduction against staleness on decision-influencing
fields:
- resolvePR: 60s (identity metadata only)
- getPRState: 5s
- getPRSummary: 5s (includes state)
- getReviews: 5s
- getReviewDecision: 5s
assignPRToCurrentUser, mergePR, and closePR each invalidate the entire PR
cache for that PR after the mutation, so AO never sees stale state from its
own writes. Failures are not cached.
getCIChecksFromStatusRollup and getMergeability are intentionally NOT cached
here — those need ETag-based revalidation, not blind TTL, and will land
separately.
Expected reduction: ~1,165 of ~1,280 gh pr view calls per tier-5 run.
Tests: 73 existing + 12 new cache tests, all 153 passing.
* feat(scm-github): cache CI checks, mergeability, pending comments, detectPR
Completes the AO-side hot-read caching alongside the prior PR view cache.
All use 5s TTL per the approved policy for decision-influencing fields —
well under one lifecycle poll cycle so state transitions are still seen
next pass.
- getCIChecks (gh pr checks): 5s TTL
- getMergeability (composite pr view + CI + state): 5s TTL on the composite
- getPendingComments (gh api graphql review threads): 5s TTL —
ETag doesn't help on GraphQL per Experiment 2
- detectPR (gh pr list --head BRANCH): 5s TTL, POSITIVE-ONLY.
Empty results are never cached so a freshly created PR is discovered
on the very next poll. The branch-keyed cache entry is invalidated
by mergePR/closePR alongside the number-keyed entries.
Combined with the prior PR view cache, covers the top 6 AO-side gh
operation categories that accounted for ~85% of calls in tier-5 traces.
Tests: 85 existing + 9 new cache tests, all 162 passing.
* experiments(m2): parse REPO from yaml before using it in banner
m2-ab-run.sh referenced $REPO in the header banner before parsing it,
causing 'unbound variable' abort under 'set -u'. Parse it right after
CONFIG_FILE is set.
* test(core): mock full Issue shape in plugin-integration cleanup tests
After tracker-github routed isCompleted() through getIssue() to share
the issue cache, these mocks needed the full Issue shape (number, title,
body, url, state, stateReason, labels, assignees) instead of the narrow
{state} shape that worked when isCompleted made its own --json state call.
* perf(scm-github): tune cache TTLs based on trace replay
Replayed feat run1 + main run2 tier-5 traces (4059 + 1748 rows, 38 min, 5
sessions each) against the shipped cache logic. Three TTLs were materially
under-tuned for the actual lifecycle poll cadence:
- detectPR: 5s → 30s (was 0.5% hit rate; per-branch poll cadence
is ~90s, so 5s caught nothing. 30s catches
intra-cycle bursts when multiple sessions
share a branch. Positive-only stays.)
- getReviewDecision: 5s → 10s (within "10-30s TTL or ETag" policy)
- getPendingComments: 5s → 10s (same policy class)
All three are still well under one poll cycle; freshness contract unchanged
in practice. Other TTLs (5s on state/CI/mergeability, 60s on resolvePR,
5min on issue) hit the targets they were set for and stay as-is.
Replay results before/after:
- feat run1: 53.7% → 57.8% reduction (2179 → 2345 hits of 4059 calls)
- main run2: 47.4% → 52.6% reduction
- Net: ~55% AO-side gh calls eliminated across both traces
Adds experiments/cache-replay.mjs — a counterfactual replay tool that
walks an execGhObserved JSONL trace and simulates per-method cache hits
with the shipped TTLs. Useful as a regression check when tweaking cache
policy.
Tests: 162/162 passing.
* docs(experiments): add cache freshness check runbook
Seven-step manual runbook to validate the cache TTL contract doesn't
cause workflow lag. Covers each cached method with:
- exact gh CLI trigger command
- what to observe in the dashboard / lifecycle log
- pass/fail threshold (TTL + 30s poll cycle)
Companion to experiments/cache-replay.mjs — replay measures how much
we saved, runbook measures whether we lost anything in the process.
* docs(experiments): add Step 2 — consolidate review comment fetching
Single GraphQL call replaces GraphQL + REST for review comments.
Include comment data in agent reaction message to eliminate
agent-side gh read calls. Update future steps.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* docs(experiments): add duplicate API traffic analysis
Three independent sources hit GitHub API for the same PRs:
1. Dashboard serialize.ts — individual REST calls, no batch, no cache
2. CLI lifecycle manager — batch + guards
3. Web lifecycle manager — same batch + guards, 3s offset
~50% of all API traffic is pure duplication. Dashboard and dual
lifecycle managers are the root causes.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* docs(experiments): add full cache architecture to duplicate traffic analysis
Three independent cache layers across two processes with zero shared
state. Web process creates its own plugin registry, SCM plugin, lifecycle
manager, and dashboard cache — all hitting GitHub independently.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* docs(experiments): add shared PR enrichment plan
Persist batch enrichment + review comments to session metadata files.
Dashboard reads from disk instead of making its own GitHub API calls.
Remove web's duplicate lifecycle manager.
Eliminates ~268 calls / 15 min (58% of all traffic). Dashboard data
gets fresher (30s vs 5min). Single writer (CLI lifecycle), web only reads.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* docs(experiments): update Step 1 — remove all three fallback paths
Remove fallback in determineStatus(), maybeDispatchCIFailureDetails(),
and maybeDispatchMergeConflicts(). All three follow the same pattern:
batch cache hit → use it, cache miss → skip (wait 30s for next batch).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* docs(experiments): promote Step 3 (remove dead reviews field) + detail Step 5 (issue caching)
Step 3: Remove reviews(last: 5) from batch query — fetched but never
consumed, reduces GraphQL complexity on every batch call.
Step 5: Persist issue data to session metadata at spawn — eliminates
27 gh issue view calls per 15 min (both processes re-fetch independently).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat(core): remove individual REST fallback from lifecycle polling
Remove fallback paths in determineStatus(), maybeDispatchCIFailureDetails(),
and maybeDispatchMergeConflicts() that made individual REST calls when the
batch enrichment cache missed. The batch runs every 30s — a cache miss
means the data arrives on the next cycle, not that it's lost.
Also add populatePREnrichmentCache() call to check() so single-session
checks also use the batch path.
Eliminates ~110 individual pr view/pr checks calls per 15-min window
(24% of all AO-side traffic).
* feat(core): consolidate review comment fetching into single GraphQL call
Add getReviewThreads() to SCM interface — returns all review threads
(human + bot) with isBot flag from a single GraphQL query. Lifecycle
manager splits locally for separate reaction pipelines.
- Eliminates the REST getAutomatedComments() call (40 calls / 15 min)
- Reaction messages now include inline comment data (file, line, author,
body, URL) so agents don't need to re-fetch via gh api
- Default config messages updated to not tell agents to call gh
- getAutomatedComments kept as optional for backward compatibility
* perf(scm-github): remove unused reviews(last: 5) from batch query
The batch query fetched reviews with author, state, submittedAt but
the data was never consumed — only used in a validation check.
The reviewDecision scalar field provides everything AO needs.
Reduces GraphQL complexity cost on every batch call.
* docs(experiments): add post-optimization trace report (Steps 1-3)
5-session, 17-minute trace after removing REST fallback, consolidating
review comments, and removing dead reviews field.
Results: GraphQL 35%/hr (was 41%), REST <1% (was 3%), automated
comment REST calls eliminated. 54% of remaining traffic is redundant
(duplicate lifecycle manager + dashboard individual calls).
* docs(experiments): add trace file gist link to post-optimization report
* feat(core,web): shared PR enrichment — dashboard reads from metadata
CLI lifecycle manager now persists batch enrichment data and review
comments to session metadata files (prEnrichment + prReviewComments
keys). The web dashboard reads from metadata instead of calling
GitHub API.
Changes:
- lifecycle-manager: add persistPREnrichmentToMetadata() after poll,
write prReviewComments in maybeDispatchReviewBacklog()
- serialize: replace enrichSessionPR (6 API calls) with metadata read
- services: stop web lifecycle polling (keep for webhook checks)
- cache: remove prCache (no longer needed)
- routes: remove timeout wrappers and cacheOnly pattern
Eliminates ~237 calls / 15 min (54% of all AO-side traffic).
Dashboard data freshness improves from 5min to 30s.
* fix(web): remove unused beforeEach import in serialize test
* docs(experiments): add final trace report — 56% GraphQL reduction achieved
5-session, 24-min trace after all optimizations including shared
enrichment. GraphQL 905/hr (was 2,072), REST 5/hr (was 168).
Single lifecycle manager confirmed. Dashboard API calls eliminated.
Max sessions before budget exhaustion: ~27 (was ~12).
* docs(experiments): add REST budget breakdown to final report
* fix(core): use storageKey for getSessionsDir in persistPREnrichmentToMetadata
* fix(test): use OpenCodeSessionManager type in plugin-integration tests
* fix(test): update bugbot-comments and auto-cleanup tests for new review API
* fix(web): fix syntax error and missing import from rebase
* docs(experiments): add complete rate-limiting change log and update final report numbers
* fix(web): fix tmux session resolution for legacy wrapped storageKeys
* fix(web): pass tmuxName directly to terminal server instead of reverse-resolving
* perf(core): gate detectPR behind Guard 1 ETag — skip when PR list unchanged
* perf(core): always run Guard 1 for all repos, dedup issue views, include threadId in review messages
* feat(core): add Guard 3 (review ETag), enrich review data with summaries, dedup issue views, gate detectPR for all repos
* fix(web): reuse cached tmuxSessionId on re-open, add 15-session trace report and comparison docs
* perf(scm-github): reduce contexts to first:10, add -i to review GraphQL for rate limit tracing
* feat(core): merge CI details into transition, enrich merge conflict message, reduce batch contexts, add graphqlCost tracing
* chore(experiments): remove working artifacts, keep final reports and reference docs
* chore: remove experiments directory
* refactor: remove getAutomatedComments from SCM interface and all implementations
* fix: address all PR review comments
- gh-trace: make binary resolution async via fs.access (no event loop
blocking, no shell injection), cache mkdir for trace writes, async
fire-and-forget appendFile, document 10MB maxBuffer rationale
- lifecycle-manager: log detectPR failures via observer instead of
silent catch, add getPRState fallback for terminal states
(merged/closed) when batch enrichment cache misses
- scm-gitlab: implement getReviewThreads with bot+human threads and
isBot flag, fixing silent feature regression after
getAutomatedComments removal
- scm-github: clear reviewThreadsCache in invalidatePRCache, document
first:11 CI checks cost budget
- runtime-tmux: use printf+JSON.stringify for PATH export to prevent
shell injection from single quotes
- agent-workspace-hooks: add cache timestamp sanity check, include
--repo in cache keys to prevent cross-repo collisions
- services: document dashboard dependency on CLI polling
- tests: update gh binary path assertions for resolved paths
* fix: address all 17 PR review comments
- gh-trace: use path.delimiter, cache-only-on-success, last HTTP status
line, await writes with warn-once, redact secrets, gate JSON.parse
- agent-workspace-hooks: validate cache keys, redact wrapper trace args,
sha256 cache keys to prevent collisions, 120s TTL ceiling
- session-manager: skip PATH wrappers for claude-code (native hooks)
- types: add deprecation JSDoc for getReviewThreads
- graphql-batch: clear Guard 3 in clearETagCache, re-read ETag on 304,
switch Guard 2 to check-runs endpoint, drop per_page=1 from Guard 3
- Add gh-trace unit tests for extractOperation, redactArgs, parseHttp
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: adil <adil.business4064@gmail.com>
Co-authored-by: iamasx <adilshaikh4064@gmail.com>
Represent missing activity probes as first-class signal states so lifecycle inference only treats valid idle evidence as proof. This prevents false stuck transitions, keeps API/UI lifecycle truth aligned, and makes root monorepo verification deterministic by serializing recursive build and typecheck.
Renames all npm package scopes from @composio/* to @aoagents/* and
updates GitHub repo references from ComposioHQ/agent-orchestrator
to aoagents/ao throughout the codebase.
- All package.json names and dependencies
- README badges, links, and install instructions
- Documentation references
- Changeset config
- Source code imports and test files
* feat(core): add scm webhook contract
Defines a provider-agnostic SCM webhook contract in core types and
config so SCM plugins can verify and normalize inbound webhook events
without reshaping project config later.
* feat(scm): trigger lifecycle checks from github webhooks
Adds GitHub webhook verification and event parsing, exposes a web
webhook endpoint, and routes matching PR/branch events through the
existing lifecycle manager so CI and review reactions update immediately.
* fix(scm): verify github signatures with raw webhook bytes
Preserves the original webhook bytes alongside the decoded payload so
GitHub HMAC verification uses the exact request body while the route
continues to drive lifecycle checks through the existing manager.
* fix(web): wire scm webhook route into main branch services
Restores the main-branch service and route-test wiring while keeping the
new webhook route coverage and scoped lifecycle helper in place.
* fix(webhooks): use singleton lifecycle manager and fail closed on scm API errors
Reuses the existing services lifecycle manager for webhook-triggered checks
so reactions and state transitions don't replay from a fresh instance, and
restores fail-closed behavior for GitHub review comment fetch failures.
* fix(webhooks): tighten project matching and restore scm compatibility methods
Prevents repository-less webhook events from matching all projects, restores
GitHub SCM PR utility methods and CI status rollup fallback, and adds tests
covering the compatibility paths and safer project matching behavior.
* fix(webhooks): pre-check content length and continue on parse errors
Adds an early content-length guard against configured maxBodyBytes before
reading the body and changes candidate parse failures to fail-forward so
one malformed payload path does not abort other valid candidate handling.
* fix(scm-github): parse review-comment timestamps from comment payload
Use comment.updated_at/created_at for pull_request_review_comment webhook
timestamps so normalized events retain temporal data for comment events.
* fix(webhooks): apply early size guard only when all candidates are bounded
Uses the broadest candidate limit for pre-read content-length checks and
skips early rejection when any matching project has no configured limit,
while retaining per-candidate verification limits.
* fix(webhooks): normalize repo matching and skip terminal sessions
Match webhook repository names case-insensitively against configured project
repos and avoid lifecycle checks for terminal sessions when resolving
webhook-affected sessions.
* fix(webhooks): fail forward when lifecycle checks throw
* fix(scm-github): parse push webhook branch and sha
* refactor(scm-github): dedupe cli exec helper wrappers
* fix(scm-github): tighten exec helper type and comment timestamps
* fix(scm-github): prefer head_commit timestamp for push events
* fix(webhooks): tighten repository parsing and helper visibility
* fix(scm-github): ignore non-head refs for push branch
* chore: trigger bugbot rerun
* feat(scm-gitlab): add webhook verification and event parsing
* fix(webhooks): share parser utils and handle check_run branch
* fix(webhooks): ignore gitlab tag refs in ci branch mapping
* fix(scm-gitlab): harden token and tag ref handling
* chore(scm-gitlab): update webhook helpers around bugbot threads
Address two Bugbot review comments from PR #191:
1. Closed MRs are now correctly reported as non-mergeable in
getMergeability, returning a "MR is closed" blocker instead of
falling through to produce an empty blockers list.
2. Extract shared glab, parseJSON, extractHost, and stripHost helpers
into scm-gitlab/src/glab-utils.ts. The tracker-gitlab plugin now
imports these from @composio/ao-plugin-scm-gitlab/glab-utils instead
of maintaining duplicate copies.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>