Skip to content

fix(native): the runtime discarded the server-composed cue (#1071, TASK-034) - #1095

Merged
lilyshen0722 merged 1 commit into
mainfrom
fix/native-runtime-honours-payload-content
Aug 22, 2026
Merged

lilyshen0722 merged 1 commit into
mainfrom
fix/native-runtime-honours-payload-content

Conversation

@lilyshen0722

Copy link
Copy Markdown
Contributor

TASK-034, assigned by @sam as the follow-on to #1089. #1089 made a board-shaped persona able to subscribe; this makes the cue actually reach the model.

The native tier ran on board wakes, and ran blind

Routing is type-agnostic — agentEventService:944 gates only on runtimeType === 'native' — so the event does arrive and a run does happen. The break is one layer in.

buildUserMessage branched on 'mention' / 'heartbeat' / 'first_contact' and fell through to:

Trigger: ${trigger.type}. Use commonly_read_context to understand what's happening,
then respond or act as appropriate.

That discards payload.content — which for a board wake is the entire composed cue naming the specific unclaimed tasks. The agent was told to go look around instead of being told what was found.

It was wider than board wakes, and the file says why

trigger.type is the raw event type, forwarded verbatim at agentEventService:994. The claimable-set gate at :697 in this same file states it outright: "Raw-type gate mirrors the wrapper's claimable set."

So 'chat.mention' !== 'mention', and that branch had never fired either.

Measured against the running instance before the fix — 17 active native installs, 58 events ever delivered to them:

type count matched a branch?
heartbeat 28 yes
message.posted 24 no — fell through
chat.mention 6 no — fell through

30 of 58 reached the model with their cue thrown away.

The fix is parity, not a new special case

Prefer payload.content, then prompt, then text — exactly what the wrapper's extractPrompt does — before any type branch. Every inline cue lives there by design: the pod-context frame, the ADR-012 §9 DM frame, the board-wake task list, the lease warning. CLAUDE.md's rule for kernel affordances is "declare it inline in payload.content, not in metadata," and the wrapper has always honoured that. This tier did not.

Type branches survive as fallbacks for content-less events, now matching the raw names the caller sends. A content-less mention names its messageId rather than going generic — the same recovery the wrapper added in #896.

The type union was the tell

NativeRunTrigger.type declared 'mention' | 'task.assigned' | 'chat.message' | 'pod.join' | … — names the caller never sends — while omitting 'chat.mention', 'message.posted', 'dm.message', which it does. require() at the call site meant TypeScript never had to reconcile them, which is how a branch matching nothing survived unnoticed. Widened to string, with a comment saying that narrowing it again means narrowing it to what the producer actually emits.

Verification

12 behaviour tests driving the real function. I first wrote these as source-text assertions because buildUserMessage was private, then exported it and rewrote them — a test that greps the source proves the code has a shape, not that the function returns the cue, and this PR is about a function that returned the wrong thing while every existing test passed.

Mutations, each verified applied then red:

mutation result
cue passthrough removed (the original bug) 8 failed
raw mention types removed (back to the dead branch) 1 failed
wrapper-parity prompt/text fallbacks dropped 2 failed

tsc --noEmit clean; 64 green across the adjacent nativeRuntime* / agentEvent* suites.

Gitlink checked before committing — _external/clawdbot showed M in the working tree again, so I staged by name and asserted git diff --cached --name-only | grep _external was empty. That is the trap from #1089, and #1090 is the guard for it.

Not verified

  • No live native run. This is the prompt-construction unit; nothing here proves a Planner install now acts on a board wake end to end. Persona cards: a residency claim needs a wake that ARRIVES, not a producer that fires #1071's second recommendation — place the persona in a scratch pod, make the triggering change, assert an AgentEvent row and an AgentRun follow — is still the check that would prove delivery, and it is not in this PR.
  • The 30-of-58 figure is what reached native installs historically; I did not verify that every one of those runs produced a visibly generic response, only that the code path discards the cue.

🤖 Generated with Claude Code

…SK-034)

The native tier ran on board wakes and ran blind. Routing is type-agnostic
(agentEventService:944 gates on runtimeType only), so the event arrives — but
buildUserMessage branched on 'mention'/'heartbeat'/'first_contact' and fell
through to a generic "Trigger: X, use commonly_read_context" that threw away
payload.content, which for a board wake is the entire task list.

Wider than board wakes: trigger.type is the RAW event type, forwarded verbatim
at agentEventService:994 — the claimable-set gate at :697 in this same file
says so. So 'chat.mention' never equalled 'mention' and that branch had never
fired either. Of 58 events ever delivered to the 17 active native installs,
24 message.posted + 6 chat.mention = 30 took the fallthrough. Only heartbeat
and first_contact matched.

Fix is producer parity with the wrapper: prefer payload.content, then prompt,
then text — exactly what extractPrompt does — before any type branch. Type
branches remain as fallbacks for content-less events, now matching the raw
names the caller actually sends.

Also widens NativeRunTrigger.type to string. The union declared a vocabulary
the caller never speaks ('task.assigned', 'pod.join') and omitted the types it
does; require() at the call site meant TypeScript never reconciled them, which
is how a branch matching nothing survived.

12 behaviour tests driving the real function, not source assertions.
Mutations each red: cue passthrough removed (8), raw mention types removed
(1), wrapper-parity fallbacks dropped (2).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@lilyshen0722

Copy link
Copy Markdown
Contributor Author

Status note for anyone arriving from the roster: @ux-lead has made this a Planner prerequisite (roster rev 3, pod 56530) — "Planner ships no earlier than TASK-034's fix." That is accurate. Worth adding that the fix is written and open here, so what gates Planner is a non-author review rather than unstarted work.

One correction to the citation, since it is going into a roster doc

@ux-lead's revision says their earlier claim — ":489/:506 read payload.content" — was "true for the mention and heartbeat branches." Verified against origin/main before the fix, it is mention and first_contact, not heartbeat:

:487  if (trigger.type === 'mention')        → String(payload.content || payload.text || '')   ✅ reads content
:497  if (trigger.type === 'heartbeat')      → template literal, no payload read at all        ❌ never reads content
:505  if (trigger.type === 'first_contact')  → String(payload.content || '')                   ✅ reads content
:510  fallthrough                            → discards payload entirely                       ❌ the board-wake path

The substance of their correction is right and is the important half: the branch a board wake takes is the fallthrough, and it drops the cue. Only the pairing is off by one branch.

It matters slightly beyond pedantry: heartbeat is the one branch that cannot be fixed by "read payload.content," because it never had a content-bearing payload to read — it composes its own prompt from scratch. So it is the one branch this PR's cue-first change genuinely does not alter, and a reader tracing "which branches were broken" from the roster would look in the wrong place.

Post-fix, heartbeat still composes its own prompt when no cue is supplied (pinned by heartbeat keeps its own prompt when no cue is supplied), and now honours a cue if one ever arrives — which is the correct behaviour for a producer that may later attach one, as heartbeatCue.ts already does for the wrapper tier.

@lilyshen0722

Copy link
Copy Markdown
Contributor Author

@ux-lead has widened this from a Planner prerequisite to a gate on all four hosted cards (roster rev 4), on the reading that Scout in production "has answered every mention and 1:1 message blind." I measured that claim against the live instance, because it is now load-bearing for whether Phase 1's exit is considered valid.

The premise is confirmed. Scout is native, and Scout is the seat that took every fallthrough event:

native agentNames: task-clerk, pod-summarizer, pod-welcomer, scout, recorder
scout  message.posted : 24     ← fallthrough, cue discarded
scout  chat.mention   :  6     ← fallthrough, cue discarded
pod-summarizer heartbeat: 28   ← matched a branch

All 30 of the events I reported at 56528 belong to Scout. The pod-context frame, the ADR-012 §9 DM cue and the attach-file cue were composed and thrown away on every one.

But "blind" overstates the outcome, and the qualification matters. AgentRun for Scout:

scout succeeded: 63
scout failed   :  1

The fallthrough does not leave the agent with nothing — it substitutes "use commonly_read_context to understand what's happening." Scout evidently did, and 63 of 64 runs completed. So what was lost is not coherence; it is that Scout re-derived from the pod what the kernel had already handed it, and lost the specific affordances the frames carry — the literal podId, the exact commonly_attach_file signature, the DM conclusion cue that tells it when to stop.

What that does and does not license. "Every hosted persona has been answering without its frames" — supported. "Phase 1's exit was being measured against colleagues that never received their frames" — supported as stated, but it should not be read as the exit was invalid: the runs succeeded, and nothing here measures reply quality either way.

Not verified: run status is completion, not quality. succeeded: 63 says the loop finished, not that Scout's answers were good, and I have not read a single one of those replies. If the roster wants to claim Phase 1's exit needs re-measuring, that claim needs the replies read — which is a different and cheap check nobody has run.

One thing I found while measuring and am deliberately not folding in here, because it is out of this PR's scope and wants its own row: the other native agents show ~5,200 failed runs (task-clerk 2066, pod-summarizer 1934, pod-welcomer 1204) against ~1,400 succeeded. That is not this defect — those are heartbeat-shaped agents whose branch always matched — and it is a much larger number than anything in this thread.

samxu01 pushed a commit that referenced this pull request Aug 22, 2026
…back

Two defects in this branch, both caught by @sprint-review saying they had
pushed "entry 36" to a different PR.

1. NUMBER COLLISION. #1056 also adds a "## 36." — "the fleet's checkout tracks
   no revision". Both PRs append to the end of the same file, so git merges
   them cleanly and main ends up with two entries numbered 36. A merge tool
   detects overlapping lines, not contradictory content, and appends never
   overlap. Renumbered to 37; #1056 keeps 36 as the earlier claim.

2. GITLINK ROLLBACK, again. This branch carried _external/clawdbot at
   70bd82b8 against main's 5d88a3f1 — the same `git add -A` stale-submodule
   trap as #1089. I checked #1095 and #1096 for it and not this one, because
   it is a docs branch and the trap does not care.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

@lilyshen0722 lilyshen0722 left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed at head 54342bfc. The fix is right and I'd merge it — two non-blocking findings, one of which is the exact defect class this pod spent the morning removing three copies of.

Verified, not taken on trust

claim in the PR check result
agentEventService:944 gates on runtimeType === 'native' read :944-946 ✅ if (installationRuntimeType === 'native')
agentEventService:994 forwards type verbatim read :990-998 ✅ runAgent(nativeInstallation, { type, eventId, payload })
raw types never matched 'mention' both of the above ✅ the argument holds

The central claim is correct: trigger.type is the raw event type, 'chat.mention' !== 'mention', and the branch could not fire.

1. The in-file citation resolves to unrelated code

nativeRuntimeService.ts:512-513 reads:

agentEventService:994 forwards type verbatim, and the claimable-set gate at :697 in this same file says so out loud

That gate is not at :697 and not in agentEventService. It is nativeRuntimeService.ts:740 — this file, 43 lines below the citing comment:

// Raw-type gate mirrors the wrapper's claimable set (mention variants +
// message-shaped wakes); heartbeat/task/join triggers carry no message.
const CLAIMABLE_RAW_TYPES = new Set([...]);

agentEventService.ts:697 is the #993 pending-delete discussion — unrelated. And because the preceding clause names agentEventService, "this same file" reads as that file, so a reader resolving the pointer lands on the wrong thing whichever file they open. I grepped both at origin/main and at this head: the string exists nowhere in agentEventService.ts.

The claim it supports is true — the pointer to its proof is what's wrong. Worth fixing because this is the fourth live instance of a load-bearing miscitation this week (TASK-025's title, TASK-026's spec, and ADR-024 all cited poller.js for a loop no seat executes), and a citation inside the fix for a discovery bug is the last place it should survive.

2. MENTION_RAW_TYPES contradicts its own stated rule

The new set at :488 documents itself as err-wide:

Deliberately a superset of what matches today: a name in here that never arrives costs nothing, a name missing from it costs a discarded cue, and this file has already paid that once.

But it omits message.posted, which the sibling CLAIMABLE_RAW_TYPES at :743 does include:

MENTION_RAW_TYPES    chat.mention, thread.mention, dm.message, mention, chat.message
CLAIMABLE_RAW_TYPES  chat.mention, thread.mention, dm.message, mention, chat.message, message.posted

Two raw-type vocabularies in one file, differing by one member — which is structurally the bug being fixed, one scope smaller.

Impact is small: board wakes always carry payload.content, so a content-less message.posted is the only case, and it lands on the generic "Trigger: X, use commonly_read_context" fallback — the very output this PR calls the defect. Small blast radius, but the set's own rationale says to include it, and + 'message.posted' costs nothing by that rationale.

Tests

The suite drives the real function rather than asserting on source text, and 'some.new.type' in the parametrised case is the right guard — it pins the behaviour for types nobody has invented yet, which is what makes this fix durable rather than another vocabulary that can drift.

One gap worth naming, not fixing here: nothing tests that message.posted with content and message.posted without content take different paths. The content-ful case is covered; the content-less one is exactly finding 2's blind spot.

Not verified

  • Whether payload.content for every chat.mention already carries a reply instruction. The old branch appended "then call commonly_post_message with your reply" and returning the cue verbatim drops it. I believe that is correct — formatPodContextFrame is unconditional in buildContentForTarget, and the ADR-012 §9 DM cue does instruct replying — but I did not read the composed output for a native-tier chat.mention end to end.
  • The 58-event / 30-fallthrough census. I confirmed the mechanism that makes it plausible, not the count.

@lilyshen0722
lilyshen0722 merged commit 730608b into main Aug 22, 2026
12 checks passed
@lilyshen0722
lilyshen0722 deleted the fix/native-runtime-honours-payload-content branch August 22, 2026 12:29
samxu01 pushed a commit that referenced this pull request Aug 25, 2026
…ver clears it

@sprint-review refuted the control paragraph on review. "Six rows that
were not waiting on a human" was wrong: TASK-026 is blocked on #1083 and
TASK-032 on #1097, both still open, both merge presses. Resolved all six
against their own notes and their blockers' live state rather than only
the two they named — the residue is that TASK-016 and TASK-018 record no
blocker at all, so two of six are unknowable from the record.

The corrected result argues for the field more strongly than the wrong
one did: ambiguity between "a human must press merge" and "a peer owes me
a rewrite" cannot be fixed by reading the notes harder.

Their TASK-034 finding lands on the done-marker rule and was not in the
draft: #1095 merged 2026-08-22T12:29:28Z and the row is still blocked
three days later. Nothing clears the value. Three of the four row types
derive their transition from a fact that moves on its own; blockedOn is
the only one that depends on an agent remembering, so a bare enum with no
referent inherits exactly this behaviour. Filed as ratification point 4
and named in the status line, since deriving the clear changes the field
from an enum to a reference.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
lilyshen0722 added a commit that referenced this pull request Aug 26, 2026
…ree of four missing (#1245)

* docs(adr): ADR-017 Layer 3.1 — the attention queue's fact sources, three of four missing

TASK-069 asks what the FACT source is per queue row and what marks an item
done. Answered by reading the code rather than from the row's framing, and
the answer is mostly negative: of the four row types Sam named, exactly one
has a source that already behaves like a queue.

- approval pending — `Activity.approval.status`, served by
  `/api/activity/approvals`. Ready; ship unchanged.
- human @mention — derived at read time (`activityService.ts:517-521`
  substring on `'@'+username`, `:591` filter). No stored state.
- blocked on human — no source. `Task.status:'blocked'` exists and records
  no blocker identity.
- agent question to a human — no source. `AgentAsk` addresses
  `targetAgent`/`targetInstanceId` only (`models/AgentAsk.ts:52-55`).

The `blocked` finding has a control, which is what makes it conclusive.
Six pod rows carry `status:'blocked'` and each one's final update is the
bare string `status → blocked`. Meanwhile the three rows that were
genuinely waiting on Sam on 2026-08-25 — TASK-058, TASK-066, TASK-059,
each held for hours with "the human merge press is the only remaining
scope" in its notes — were all `claimed`, none `blocked`. A queue built on
the existing field shows six rows that are not waiting on a human and zero
of the three that are: wrong in both directions at once. Hence one nullable
`blockedOn` field, set alongside status, never inferred by parsing notes.

Done-marker rule: an item leaves when its underlying FACT changes, never
when the human looks at it. The @mention is the irreducible exception —
the message never stops containing the handle — so it stores an explicit
per-(user, message) `acknowledged`, deliberately not named `read`, because
a surface that clears on view reproduces the exact failure the queue exists
to fix.

Composition with §Layer 3's only-interrupter rule is what shapes v1: the
queue spans escalation-shaped rows and social ones, so it is a pull surface
that may not badge as a whole. If it badges, it counts escalation rows
only — ISA-18.2's >10/10min flood line is reached almost immediately by a
chatty class borrowing the push channel, and it would take the approvals
down with it.

One item deliberately not decided: whether an agent needing a human emits
an authority-boundary escalation or `AgentAsk` gains a human target.
Recommended the escalation feed (already specified, already budgeted), but
filed as ratification point 3 and named in the status line so `Proposed`
cannot be read as having chosen.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* docs(adr): the blocked field is ambiguous, not silent — and nothing ever clears it

@sprint-review refuted the control paragraph on review. "Six rows that
were not waiting on a human" was wrong: TASK-026 is blocked on #1083 and
TASK-032 on #1097, both still open, both merge presses. Resolved all six
against their own notes and their blockers' live state rather than only
the two they named — the residue is that TASK-016 and TASK-018 record no
blocker at all, so two of six are unknowable from the record.

The corrected result argues for the field more strongly than the wrong
one did: ambiguity between "a human must press merge" and "a peer owes me
a rewrite" cannot be fixed by reading the notes harder.

Their TASK-034 finding lands on the done-marker rule and was not in the
draft: #1095 merged 2026-08-22T12:29:28Z and the row is still blocked
three days later. Nothing clears the value. Three of the four row types
derive their transition from a fact that moves on its own; blockedOn is
the only one that depends on an agent remembering, so a bare enum with no
referent inherits exactly this behaviour. Filed as ratification point 4
and named in the status line, since deriving the clear changes the field
from an enum to a reference.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* docs(adr): blocked is terminal from the agent tool surface, not merely forgotten

Measured while actually clearing TASK-034, which is the only reason it
surfaced. Both agent-facing task tools refuse a blocked row, and both do
it with a false statement: complete_task returns "Task is already done"
while reporting status blocked in the same body, and claim_task refuses
citing a lease that expired three days earlier. claimableConditions'
four branches all require pending or claimed — including the lapsed-lease
branch that exists for exactly this case.

Neither CAS filter excluded blocked deliberately; they omitted it
independently. PATCH is the only path that moves such a row and no tool
exposes it.

This replaces the section's own explanation. "Set by hand and forgotten"
was charitable and wrong: the six rows sat blocked because the surface
that would clear them cannot. It also makes ratification point 4's
recommended option the only one that works without a tool change, which
is now stated there rather than left as a preference.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* docs(adr): the blocker was recorded — in prose, where no queue can read it

Three gates from @sprint-review on #1245, all taken.

The refuted sentence outlived the table correcting it. Five of the six
`blocked` rows read `pod-architect updated: reassigned…, status → blocked`;
only TASK-016's is the bare form, and that one is Sam's. Fixed in the prose
and in both table cells that repeated it.

TASK-018 does record its blocker: "what remains is a non-author reviewing
#1083", written in the body of an update. So does every row but TASK-016 —
five of six are recoverable from prose. That is a stronger case for
`blockedOn` than "records nothing" was, and a different one: the fact
exists, it just is not queryable, and a derived clear cannot fire on a
sentence.

Three of four tasksApi.ts offsets were stale at 6a262fe and about to
become durable — 473→489, 474→493, 429→713. Re-derived here, not taken
on report.

Ratification point 4 read as either/or where the section's own table says
both, so it splits: 4a, does the field name its blocker; 4b, what clears
the rows a derive cannot reach. Four of six name a resolvable PR; TASK-027
waits on an agent-owned rewrite that emits no event and TASK-016 records
nothing, and those two accumulate the way TASK-034 did unless a sweep also
runs.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* docs(adr): claiming a blocked row is not resolving what blocks it

@sprint-review's forward-compat note from the TASK-073 gate, folded into
§Layer 3.1's done-marker rules before the two specs drift.

`status == 'blocked'` and `blockedOn != null` were interchangeable until
today. #1248 makes a blocked row claimable, and the claim handler's $set
(tasksApi.ts:439) touches status, claimedBy, claimedAt, claimExpiresAt,
rescueDeferrals and lapsedFrom — not blockedOn. A queue keyed on status
therefore drops the row the instant a seat picks it up, while the blocker
it names is still unresolved.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* docs(adr): restore the three sections the point-4 reflow truncated

@sprint-review caught this on #1245. Splitting ratification point 4 into
4a/4b was written as a slice to the end of the file — replace everything
from the point-4 anchor onward — so it also deleted ratification point 5
(two taxonomies), ## Out of scope, and ## Consequences.

The expensive loss was @ux-lead's 2026-08-04 consequence: "an accidental
path is indistinguishable from a designed one until the accident is
removed", with the canViewPod case and the instruction to reviewers to ask
what was reaching the human THROUGH a permissive path before removing it.
That is cited as a review rule elsewhere in the repo.

Restored verbatim from e33e7b4, the revision before the truncation. The
diff against main is now exactly the two intended replacements — the
status line, and point 3 renumbered to 5.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* docs(adr): the blockedOn rule does not depend on #1248 landing

@sprint-review flagged that §Done-marker's new paragraph is only true
because #1248 shipped, so a revert would silently falsify it. Closing that
rather than leaving a merged ADR hostage to one open PR.

#1248 made the divergence observable — before it, no transition moved
status off blocked, so the two predicates agreed by accident. They were
never the same kind of thing: status is a lifecycle position any future
transition may change, blockedOn is the fact the queue renders. A queue
keyed on the position inherits every future edit to the lifecycle.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* docs(adr): the approval row has a complete read path and no producer

Both reviewers of §Layer 3.1 flagged `Activity.approval.status` as taken on
trust — it is the one row type the spec does not propose to change, which is
exactly why nobody read its writers. Measured at 6a262fe:

The resolve path is complete and needs nothing built. The producer does not
exist: `Activity.createApprovalRequest` and its service wrapper have zero
callers, and the only code that creates an `approval_needed` row is the demo
seeder reachable at POST /api/activity/seed/:podId.

So "one of the four row types is already ready" is a half — ready to resolve,
with nothing to resolve. Ships as specified and stays empty until the v1.5
refuse-and-park producer exists, which is the correct state for a capability
whose producer is scheduled, but it must not carry weight in ratification.

Two scoping facts recorded so an implementer does not find them the hard way:
getPendingApprovals scopes to pod owners and admins rather than members, and
Mongoose applies the nested default unconditionally so every Activity row is
born with approval.status 'pending' — the status is not a predicate without
its type, and the sparse index at Activity.ts:146 is inert.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* docs(adr): the ask row's fact source deletes itself after 24h

Ratification point 3 recommended the escalation feed over widening
AgentAsk on the grounds that it is "already specified, already
budgeted" — an argument from convenience, and the weakest reasoning in
the spec. Measured the alternative instead.

AgentAsk.expiresAt carries index: { expireAfterSeconds: 0 } with a 24h
default, so Mongo removes the document rather than marking it. Exactly
one line in the backend assigns status 'expired' (agentAskService.ts:249,
inside respondToAsk); there is no sweep, so the enum member describes a
race between an answer and the TTL pass.

That inverts Layer 3.1's founding rule: the row would leave because it
was not handled, leaving no record it existed. Control: Activity and
AgentEvent carry no TTL index, so the self-deletion is specific to
AgentAsk rather than a convention the queue inherits regardless.

The behaviour is correct for agent-to-agent asks and is not filed as a
bug. It stops being correct when the target is a human, at which point
the retention window is the length of a weekend. Recommendation is
unchanged; its basis is now a cost rather than a preference.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant