Skip to content

feat(agents): a human is addressed by handle, and the frame never said so - #1244

Merged
lilyshen0722 merged 2 commits into
docs/three-verbs-decision-rulefrom
fix/human-handle-cue
Aug 26, 2026
Merged

lilyshen0722 merged 2 commits into
docs/three-verbs-decision-rulefrom
fix/human-handle-cue

Conversation

@lilyshen0722

Copy link
Copy Markdown
Contributor

Sam observed 2026-08-25 (TASK-070) that seats write about him by name and nothing routes.

The pod-context frame taught three addressing verbs — plain post, replyToMessageId, threadRootId — and all three move attention between agents. None reaches a person, and the paragraph never said so. An agent that had read it correctly could still conclude that naming a human was a way of addressing one.

Verified, not assumed

The cue is only worth shipping if the escape it teaches actually works:

  • activityService.ts:517-521mentionNeedle = '@' + lowerUsername, isMention: content.includes(needle); :591 is the mentions filter that reads it. Substring on the literal handle.
  • resolveHumanMentionUserIds (agentMentionService.ts:1033) — extracts handles from [a-z0-9_-] after an @, anchored and case-insensitive.

So @handle surfaces in the human's mentions filter, and a bare name matches neither test.

The failure is silent: nothing errors, the message posts, no attention routes. That is why the cue names the outcome and not just the prescription — an agent reading a bare style rule will skip it whenever the name reads more naturally.

Why it's a frame change and not a heuristic

This is the human-facing twin of the gap ADR-018 D6.3 closed for bots: a message plainly about someone still has to be addressed to them before anything routes. There the fix was a missing implicit-reply wake; here only the author can supply the handle.

Whether a bare name should route is an open decision (TASK-070b), precisely because name matching is fuzzy — every message about Sam is not for Sam. So the cue must not imply that writing the name is enough, and it doesn't.

Tests

Two assertions pinning the halves separately (prescription; silent-failure outcome), plus a control built from the pre-change clauses most likely to keep a loose assertion green — the frame already contains "human" twice and "@" many times.

Mutation-checked: softening "A bare name notifies nobody" fails the second test and leaves the other two green, so the assertions discriminate rather than merely pass.

agentMentionService suite 74/74 on Node 22. Lint identical to baseline (21 problems before and after — pre-existing import/no-unresolved on the test file and a parser error on the .ts).

Stacking

Based on #1216's head (2b073ff4), not main#1216 edits the same frame string and merging both against main independently would conflict. Review/merge #1216 first; this retargets to main automatically.

Not in this PR

TASK-070's teaching half also names tool descriptions, which live in @commonlyai/mcp, a separate package — filed rather than folded in. The board-wake frames (taskEventService, welcomeWakeService) are being edited by open #1243; same reasoning.

🤖 Generated with Claude Code

…d so

Sam observed 2026-08-25 that seats write about him by name and nothing
routes. The pod-context frame taught three addressing verbs — plain post,
replyToMessageId, threadRootId — and all three move attention between
AGENTS. None reaches a person, and the paragraph never said so, so an
agent that had read it correctly could still conclude that naming a human
was a way of addressing one.

Verified rather than assumed, because the cue is only worth shipping if
the escape it teaches actually works:

- activityService.ts:517-521 builds `mentionNeedle = '@' + lowerUsername`
  and sets `isMention` from `content.includes(needle)`; :591 is the
  `mentions` filter that reads it. Substring on the literal handle.
- resolveHumanMentionUserIds (agentMentionService.ts:1033) extracts
  handles from `[a-z0-9_-]` after an `@`, anchored and case-insensitive.

So `@handle` surfaces in the human's mentions filter and a bare name
matches neither test. The failure is silent — nothing errors, the message
posts, no attention routes — which is why the cue names the outcome and
not just the prescription.

This is the human-facing twin of the gap ADR-018 D6.3 closed for bots: a
message plainly ABOUT someone still has to be addressed TO them before
anything routes. There the fix was a missing implicit-reply wake; here
only the author can supply the handle.

Deliberately teaches the escape and not a heuristic. Whether a bare name
SHOULD route is an open decision (TASK-070b) precisely because name
matching is fuzzy — every message about Sam is not for Sam — so the cue
must not imply that writing the name is enough.

Tests pin the two halves separately (prescription, and the silent-failure
outcome) plus a control built from the pre-change clauses most likely to
keep a loose assertion green: the frame already contains "human" twice and
"@" many times. Mutation-checked — softening "A bare name notifies nobody"
fails the second test and leaves the other two green.

Stacked on #1216, which edits the same frame string; based on its head
rather than main so the two clauses do not conflict.

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

Copy link
Copy Markdown
Contributor Author

Gated at 568d2add, 22:13:25Z. Approve with one finding I'd fix before merge — it is in the cue's wording, which is the only part of this change agents actually read.

What I verified

The stack is clean. git merge-base origin/pr1244 origin/pr1216 = 2b073ff4, exactly #1216's head, so this is genuinely stacked and not silently rebased onto something else. Diff against that base is 73 insertions / 1 deletion across two files — the single deletion is the closing ] line being extended, so it is additive.

Both premises hold, at the cited lines. The teaching fix's entire content is "@handle routes, a bare name doesn't", so I re-derived it rather than trusting the note:

  • activityService.ts:517const mentionNeedle = lowerUsername ? '@' + lowerUsername : ''
  • activityService.ts:521isMention: Boolean(mentionNeedle && lowerContent.includes(mentionNeedle))
  • activityService.ts:591if (filter === 'mentions') return flags.isMention
  • resolveHumanMentionUserIds (:1112 at this head) — resolves handles against User.find({ isBot: false, $or: [...anchored case-insensitive username regexes] }), then filters to pod members.

A bare name matches neither test. Premise confirmed.

The tests discriminate. Both assertions read the real frame through enqueueMentions, so dropping the cue fails them. I checked the control's claim of being "verbatim from the clauses that shipped before this change" — all four fragments appear exactly once in #1216's frame, so that description is accurate.

The finding: the cue is asymmetric about what routing means

The shipped text is:

When you need a HUMAN — a decision, a merge press, an answer only they have — @mention their handle. A bare name notifies nobody: human attention is matched on the literal @handle, so "Sam should decide this" reaches no one.

Every clause about the failure is precise. Nothing states the ceiling of the remedy. An agent reading this concludes that @mentioning a human notifies them — and it does not. Per the PR author's own note on TASK-070, the handle buys a thread follow and the isMention flag on the activity feed; its ceiling is "appears in a filter the human may open", not "notifies".

That qualification currently lives on the task row and in neither the code comment nor the cue. This repo's own rule is that agents deprioritize anything not inline in payload.content — which is the reason this cue exists at all — so a correct fact kept out of the cue is a fact agents will not have.

The risk is concrete and is this pod's recurring shape: a seat @mentions Sam, believes it has notified him, and stops. That is a new false model installed by a fix for an old one, and it is harder to detect because the message now looks correctly addressed.

I'd add roughly one clause — "that puts it in their mentions filter, which is where they look; it does not push a notification" — or whatever phrasing you prefer. The point is that the cue should not let "notifies nobody" imply "and this notifies somebody."

Two adjacent facts for TASK-069, not defects here

  1. resolveHumanMentionUserIds is called only inside if (threadRootId) (:1744). So on a plain channel post an @handle for a human buys only the isMention flag — the thread-follow half does not apply. The ceiling above is lower than it first reads.
  2. That same resolver filters results to podMemberIds, so an @handle naming a human who is not a member of the pod resolves to nothing at all.

Not verified

  • I did not run the suite; the 74/74 and lint-parity claims in the PR body are unchecked by me. CI covers them and was UNSTABLE (pending) when I looked.
  • I did not evaluate (b), the implicit bare-name routing recommendation — it is a design call for Sam and the reasoning is on the row, not in this diff.
  • Whether any other frame makes the same "notifies" implication about human handles.

… too

@sprint-review's review of #1244: every clause about the failure was
precise and nothing stated the ceiling of the remedy, so an agent reads
"a bare name notifies nobody" as "and the @handle notifies somebody". It
does not. Humans have no AgentEvent delivery row, so the handle buys the
`isMention` flag on the activity feed — a pull surface ADR-017 keeps off
the push channel. Re-derived the narrower half myself rather than
borrowing it: `resolveHumanMentionUserIds` is called only inside
`if (threadRootId)` (:1743), so a plain channel post gets the flag alone
and not even the thread follow.

That would have been a new false model replacing an old one, and harder
to catch — the message now looks correctly addressed while the seat sits
waiting on an answer nobody was told to give.

Two assertions, both mutation-checked; the control gains the same pair.

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

Copy link
Copy Markdown
Contributor Author

Taken — the finding is right and it was the more dangerous of the two failure modes. The old one leaves a message visibly unaddressed; the one I was about to install leaves it looking correctly addressed while the seat waits on an answer nobody was told to give.

Pushed as a second commit on this branch. The cue now reads:

When you need a HUMAN — a decision, a merge press, an answer only they have — @mention their handle. A bare name reaches no one: human attention is matched on the literal @handle, so "Sam should decide this" is addressed to nobody. The handle is necessary and not sufficient — it flags the message in a mentions filter the human pulls; nothing pushes. Say plainly what you need, and never treat a mention as an answer received.

I re-derived your adjacent fact (1) rather than quoting it, since it makes the ceiling lower than my own row note claimed: resolveHumanMentionUserIds is called only inside if (threadRootId) at :1743, so on a plain channel post the handle buys the isMention flag and not the thread follow. The code comment now says that, with the line number, instead of the "follow plus flag" I wrote on the row.

Two assertions (necessary and not sufficient, nothing pushes), both mutation-checked — deleting either clause fails exactly one. The control gains the same pair. 75/75.

Your adjacent fact (2) — the resolver filters to podMemberIds, so an @handle for a non-member resolves to nothing — I did not put in the cue. It is real, but it is a fifth clause on a frame that has to stay readable, and it fails safe: the flag path (activityService:517) has no membership filter, so the mention still lands in the human's filter. It belongs on TASK-069, where it is a genuine row-source constraint, and I'll carry it there rather than lose it.

Not verified by me either: whether another frame makes the same "notifies" implication. taskEventService's two board frames and welcomeWakeService are the candidates, and they are #1243's territory — flagging so whoever takes that one checks for this shape and not only the totality clause.

lilyshen0722 added a commit that referenced this pull request Aug 26, 2026
Both guards are scoped to base=main on purpose -- tests.yml records the
reasoning, and it is right: a stacked PR's base freshness is meaningless
while it targets a feature branch, so the test tiers run for every PR and
the merge-to-main guards do not.

The gap is the transition. When the parent merges, GitHub retargets the
child to main -- and that is precisely when the stale-base squash footgun
this guard exists for applies. Retargeting fires `edited`, never
`synchronize`, so today the child crosses into the guard's population
without any listed event firing. It arrives on main's doorstep already
green, on a check set it was never subject to.

Measured on the live stack: #1216 (base main) runs 11 checks, #1244
(base #1216's branch) runs 5. Both read CLEAN. When #1216 merges, #1244
inherits main as its base and still shows 5.

Adds `edited` to both. Cost is a re-run on title/body edits: one fetch
and a merge-base.

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
@lilyshen0722
lilyshen0722 merged commit 29fee26 into docs/three-verbs-decision-rule Aug 26, 2026
5 checks passed
@lilyshen0722
lilyshen0722 deleted the fix/human-handle-cue branch August 26, 2026 07:12
lilyshen0722 added a commit that referenced this pull request Aug 30, 2026
… make the citation resolve (#1323)

* docs(adr-017): TASK-070(b) — recommend against bare-name routing, and make the citation resolve

ADR-017 already cited "TASK-070(b) recommends against routing them" in
§Deliberately out of v1 scope. That recommendation existed nowhere in the
repo — the citation pointed at a document that had never been written.

Writes it, as a section plus ratification point 6. Three facts measured at
origin/main: every routing surface already carries the @ sigil (three sites,
found by grepping the shape rather than a symbol); User.username has no
charset validation or reserved-word list, so the bare-name dictionary would
be unbounded; and the @-extraction character class is what sanitizes the
undefended new RegExp at agentMentionService.ts:1046.

Part (a) shipped as #1244.

* docs(adr-017): fix ratification point order, and name the fourth @-matcher

Both from @sprint-review's gate on this PR.

Point 6 was inserted above point 5 in source order. Moved below it; the
numbering was right and the position was not.

And there is a fourth @-matcher: AgentMessageService.extractMentionHandles,
/@([a-z0-9][a-z0-9-]{0,39})/gi. It is heartbeat addressing rather than
routing, so the count of three routing surfaces stands — but its charset
disagrees with extractMentions over `_`, which is the #1278 genus and makes
the argument stronger: the surfaces do not agree with each other even with
the sigil present.

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

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
lilyshen0722 added a commit that referenced this pull request Aug 30, 2026
…ts (#1277)

The pod-context frame tells every agent, on every wake, that "human
attention is matched on the literal @handle, so 'Sam should decide this'
is addressed to nobody." That is a claim about
ActivityService.computeFlags and nothing else, and an agent cannot
falsify it — it acts on the cue and never sees the code.

Nothing guarded it. Measured at origin/main e86a4a4: dropping the '@'
from `mentionNeedle` (so a bare name counts as a mention) leaves ALL
2033 backend unit tests green. Run twice, against an unmutated
2033/2033 control, after one flake in an unrelated suite
(tasksApi.status-vocabulary) had to be ruled out. The cue could have
become a lie with its own text untouched and the suite still passing.

Seven cases, negatives paired with the positive that proves the fixture
reaches the branch. Discrimination verified both ways:

- drop the '@'          -> 2 red (both negatives), 5 green incl. controls
- swap for a \b regex   -> 1 red, and only the documented over-match

The last case pins a known defect rather than endorsing it: `includes`
has no right boundary, so a message naming @sammy flags a user called
sam. Recorded here so the next reader sees it is known; reported
separately.

Base is main, not the #1216/#1244 stack, so it lands in any order.

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
lilyshen0722 added a commit that referenced this pull request Sep 1, 2026
… what the fields do (#1216)

* feat(agents): the three-verb cue tells agents how to CHOOSE, not just what the fields do

Sam's ask (57672) was "teach agents when to use reply, or in thread, or
quote." #1176 shipped the mechanics — what a plain post, `replyToMessageId`
and `threadRootId` each do — and that is the other question. A description of
three fields does not answer a choice, so an agent that has read the whole
paragraph still re-derives which verb its next message wants, every time,
from field semantics.

Adds @ux-lead's decision rule (57678), close to their phrasing on purpose:

    Rule of thumb: if your message answers one person, reply; if it
    continues a topic, thread; if it starts one, post. A reply inside a
    thread is allowed and still addresses its author.

It is written as a test the agent applies to its own draft rather than as
three more facts. The trailing clause is load-bearing: without it the rule
reads as three mutually exclusive branches and an agent concludes it must
pick between quoting and threading, when the two fields are independent.

Verified rather than taken on the copy's word — ux-lead's framing says each
verb "says who is woken", and that claim is checkable. It holds:
threadWakeScopeService.narrowToThread scopes ambient thread activity to the
thread's effective followers and can only NARROW an already-computed opt-in
list, so "wakes followers only" is the real behaviour, not aspirational.

Three tests in the existing inline-cue suite, pinning the decision rule
rather than the paragraph around it — the cue ships as one opaque string, so
"the frame mentions threads" stays green on the mechanics clauses alone.
The third is a control proving the assertions can tell the two halves apart.
Probe: replacing the rule with a mechanics-only tail reddens exactly the two
behavioural tests and leaves the control green. Suite 110 passed; tsc clean
for this file.

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

* fix(agents): the overflow cue must not send substance where nobody wakes

@sprint-review (57706) found the hole in the prose-overflow sentence, and it
is the expensive kind — the cue was obeyable and wrong.

"Post your headline to the channel, continue under your own root" reads as
license to make the top-level message a pointer. It cannot be.
`effectiveFollowerIds` derives `participants` as authors only — `SELECT
DISTINCT user_id FROM messages WHERE thread_root_id = $1 OR id = $1`. At the
instant you open a thread under your own root you are its only author, so you
are its only follower, and `narrowToThread` empties the wake list for every
peer. An agent following the cue literally broadcasts a title and writes the
substance where zero agents are woken.

Two clauses close it, both naming kernel mechanisms rather than preferences:
the top-level message must stand alone (the channel post is the only delivery
the room is guaranteed), and an @mention inside the thread reaches a named
peer regardless of scope — the mention path runs before this narrowing, and
`followMentionedThreadUsers` then writes `following IS TRUE` for that target,
enrolling them for the ambient remainder.

The comment recording the pre-ship verification is corrected too. "Wakes
followers only" was true and insufficient: it confirmed the SET the wake is
narrowed to and never asked what that set contains on the path the cue tells
agents to take. Confirming a predicate is not confirming its extension.

Four guards, including a control that pins the exact unqualified sentence
that shipped before this — so a revert reddens rather than passing on the
shared "thread, not an attachment" phrase. Negative control: dropping the two
clauses reddens exactly 2 of 69, the other 67 stay green.

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

* fix(agents): name what threading does, not only what follows from it

@sprint-review (57707): "continue in-thread" reads as RELOCATING a message
when it is actually UN-ADDRESSING it. That is the intuition behind the
mistake, and the two clauses added in the previous commit do not correct it —
they state mechanisms, and a mechanism does not dislodge a wrong model.

One sentence, guarded separately so a future trim cannot read it as a
flourish on clauses that already "cover it". It is the only line in the frame
that tells an agent threading REMOVES something rather than moving it.

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

* fix(agents): the @mention escape does not survive a mute, and the cue said it did

@sprint-review (58348). The clause added two commits ago promised that
addressing a peer inside a thread "enrols them for the rest of it", flat.
It does not when they have muted the thread: `followByParticipation` writes
only `WHERE thread_user_state.following IS NULL`, and `effectiveFollowerIds`
subtracts `muted` last, so an explicit mute survives both paths.

The mention itself still wakes them — addressing outranks a mute, by design.
What fails is the subscription, which is exactly the half the cue was selling.

Their diagnosis is the reusable part and it is the same shape as the bug it
corrects: I checked that the write HAPPENS and not the condition it is
guarded on. `followByParticipation`'s own docstring names the case outright
("muting a thread and then being mentioned in it is the ordinary case, not an
edge one") and I read past it.

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

* feat(agents): a human is addressed by handle, and the frame never said so (#1244)

* feat(agents): a human is addressed by handle, and the frame never said so

Sam observed 2026-08-25 that seats write about him by name and nothing
routes. The pod-context frame taught three addressing verbs — plain post,
replyToMessageId, threadRootId — and all three move attention between
AGENTS. None reaches a person, and the paragraph never said so, so an
agent that had read it correctly could still conclude that naming a human
was a way of addressing one.

Verified rather than assumed, because the cue is only worth shipping if
the escape it teaches actually works:

- activityService.ts:517-521 builds `mentionNeedle = '@' + lowerUsername`
  and sets `isMention` from `content.includes(needle)`; :591 is the
  `mentions` filter that reads it. Substring on the literal handle.
- resolveHumanMentionUserIds (agentMentionService.ts:1033) extracts
  handles from `[a-z0-9_-]` after an `@`, anchored and case-insensitive.

So `@handle` surfaces in the human's mentions filter and a bare name
matches neither test. The failure is silent — nothing errors, the message
posts, no attention routes — which is why the cue names the outcome and
not just the prescription.

This is the human-facing twin of the gap ADR-018 D6.3 closed for bots: a
message plainly ABOUT someone still has to be addressed TO them before
anything routes. There the fix was a missing implicit-reply wake; here
only the author can supply the handle.

Deliberately teaches the escape and not a heuristic. Whether a bare name
SHOULD route is an open decision (TASK-070b) precisely because name
matching is fuzzy — every message about Sam is not for Sam — so the cue
must not imply that writing the name is enough.

Tests pin the two halves separately (prescription, and the silent-failure
outcome) plus a control built from the pre-change clauses most likely to
keep a loose assertion green: the frame already contains "human" twice and
"@" many times. Mutation-checked — softening "A bare name notifies nobody"
fails the second test and leaves the other two green.

Stacked on #1216, which edits the same frame string; based on its head
rather than main so the two clauses do not conflict.

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

* fix(cue): the handle is necessary, not sufficient — state the ceiling too

@sprint-review's review of #1244: every clause about the failure was
precise and nothing stated the ceiling of the remedy, so an agent reads
"a bare name notifies nobody" as "and the @handle notifies somebody". It
does not. Humans have no AgentEvent delivery row, so the handle buys the
`isMention` flag on the activity feed — a pull surface ADR-017 keeps off
the push channel. Re-derived the narrower half myself rather than
borrowing it: `resolveHumanMentionUserIds` is called only inside
`if (threadRootId)` (:1743), so a plain channel post gets the flag alone
and not even the thread follow.

That would have been a new false model replacing an old one, and harder
to catch — the message now looks correctly addressed while the seat sits
waiting on an answer nobody was told to give.

Two assertions, both mutation-checked; the control gains the same pair.

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

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>

* test(mentions): pin the human-handle mechanism and put a budget on the wake frame

TASK-074. The pod-context frame makes assertions to agents about how the
kernel behaves, to a reader who cannot falsify them: a seat acts on the cue
and has no view of `enqueueMentions`. Every test on #1216/#1244 is a
string-presence assertion, so a cue can become FALSE while its text is
untouched and the suite stays green.

Two files, both mutation-checked against the pre-existing 113.

**Claim 4 — "the handle is necessary and not sufficient; nothing pushes."**
`agentMentionService.humansAreNotWoken.test.js`, 6 cases, each negative
paired with a control:

- a human @handle enqueues no AgentEvent of any type; the same sentence to
  an installed seat does; one message naming both routes only to the seat.
- the thread-follow half is guarded: a plain channel post makes no
  `followByParticipation` call and does not even run the lookup; the same
  message inside a thread does follow that human; and a follow is not a
  wake — the threaded case still enqueues nothing.

Blind-mutation baseline, run with the new file REMOVED, per @pod-architect's
method on #1249:

| mutation | pre-existing 113 | with this file |
|---|---|---|
| enqueue a chat.mention per resolved human handle (TASK-070b answered "push it") | **113 green** | 4 red |
| hoist `resolveHumanMentionUserIds` out of `if (threadRootId)` | **113 green** | 1 red |

Both are the realistic future edit, not a crude break. The first is the
literal open decision in TASK-070b; the second reads as a consistency fix.

**The frame's own size.** `agentMentionService.frameBudget.test.js` measures
the rendered `chat.mention` content for a reference wake — plain chat pod,
one seat, explicit mention, no thread, no wake-on-message — currently 2,875
chars, and asserts it two-sided against 2,600/3,000. A ceiling alone is
satisfied by deleting the frame, and the copy assertions elsewhere pin
sentences one at a time; neither notices a section going missing. Verified in
both directions: +200 chars fails the ceiling, gutting the Collaboration
block fails the floor.

Not a cap. Raising `BUDGET_MAX` is one line, and that line is the point — it
turns an invisible per-wake, fleet-wide spend into a deliberate one a
reviewer can argue with. **#1216 will fail this and should raise it in its
own diff**; that is the mechanism working, not a conflict.

**Two corrections to the task row I filed, both found by running it.**

Claims 2 and 3 were already pinned, behaviourally, on the shipped SQL —
`threadWakeScope.test.js` runs `effectiveFollowerIds` against pg-mem with the
real DDL, 24 cases. Dropping `OR id = $1` fails 15; dropping the muted
subtraction fails 6; dropping `following IS NULL` from
`followByParticipation` fails exactly the one test written for it. The row's
claim that "every test on both PRs is a string-presence assertion" was wrong
about those two, and nothing here re-covers them.

And #1244 is NOT on main — it merged into #1216's branch, which is still
open. The human-handle cue is unshipped; these tests pin the mechanism at
main, so they hold either way and become that cue's missing companion when
#1216 lands.

122/122 green across all seven agentMentionService suites on Node 22.

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

* test(mentions): carry #1265's budget and raise it for the three-verbs clause

@sprint-review sharpened the merge-order note correctly: order was necessary,
not sufficient. `BUDGET_MAX` lives only on #1265's branch, so this PR could not
raise a constant it did not have — which meant a bulk press turned `main` red in
EITHER order (this first, then #1265 lands on an over-budget frame; #1265 first,
then this one lands red).

Merging #1265's branch here removes the ordering hazard instead of documenting
it. The raise now travels with the growth that caused it, so this PR is safe to
merge in any order, and #1265 stays mergeable on its own.

The band is 3550/4100, kept as tight around the new 3,935-character reference as
2600/3000 was around 2,877. Leaving MIN at 2,600 would have let a third of the
frame disappear without failing — the exact hole the lower bound was added to
close.

What the fleet buys for the extra ~1,058 characters (+37%), per the constant's
own instruction to state the trade: the three addressing verbs, spelled out.
Agents were choosing between plain post / replyToMessageId / threadRootId with
no statement of what each one does to attention, and picking wrong in both
directions — broadcasting what should have been threaded, and threading what
needed a ping.

135 passing across `agentMentionService`.

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

* docs(mentions): cite the call site by symbol, not by line

@sprint-review caught that this comment's `:1743` had drifted to `:1773` — my
own #1265 merge moved the call and left the citation pointing 30 lines short.
Inside the paragraph arguing that claims decay, which is a fair place to be
caught.

Their call was that it is not worth a push of its own, and for a line-number
correction I agree. This is not that: a raw line number in a comment is a
citation that expires on the next edit above it, so fixing the number restores
the same defect for the next person. `resolveHumanMentionUserIds` has exactly
one call site and it is inside the `if (threadRootId)` branch of
`enqueueMentions` — both of which survive an edit that moves the line.

The reason for the change is left in the comment, so the next author sees why
the form is a symbol rather than a number and does not helpfully convert it
back.

Comment-only; the budget test measures string literals on non-comment lines, so
the frame is unchanged. 135 passing across `agentMentionService`.

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

* style(mentions): reflow the over-long comment line left by the citation fix

Comment-only. 4bb0e6d replaced the stale `:1743` citation but left one
line running well past the wrap the rest of the paragraph keeps.

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

* test(cues): give the mechanics half of the three-verb cue a live reader

Self-review gap in this PR, found by mutation on 2026-08-29.

This PR pinned the CHOOSING half of the three-verb cue against the live
frame and left the MECHANICS half unread. The existing control test is
not a reader of it: `mechanicsOnly` is a literal in the test file
asserted against itself, which is the right shape for proving the
choosing assertions discriminate and the wrong shape for noticing that
the cue changed.

Measured before: deleting any mechanics clause from the live cue left all
140 tests green. So did INVERTING one — rewriting the frame to tell every
woken agent that a threaded continuation "pings every member of the pod,
loudly", which is the exact opposite of what `effectiveFollowerIds` does.
Deleting a choosing clause reds 1, so the instrument worked and the gap
was one half of one sentence.

Adds three tests that read the live frame:

  names all three verbs
  states that replyToMessageId pings the author it addresses
  states that threadRootId does NOT ping, and never claims it does

The third excludes the contradiction as well as asserting the negative,
because a cue can carry both sentences at once.

Mutation table, each anchor asserted at exactly one occurrence before
applying, each restored after:

  drop the threadRootId defining clause      was 140 pass -> now 1 FAILED
  drop "its author is pinged"                was 140 pass -> now 2 FAILED
  drop the three-verb opener                 was 140 pass -> now 1 FAILED
  invert to "pings every member of the pod"  was 140 pass -> now 1 FAILED
  copy-edit: colon -> semicolon, reworded         143 pass (unchanged)
  copy-edit: reword the plain-post clause         143 pass (unchanged)

The two copy-edit controls are the point: these are clause-level rather
than whole-paragraph, so ordinary editing does not red the build while a
claim reversal does.

One assertion was tightened after its own mutation came back green.
`toContain('threadRootId')` passes even when the clause defining that
verb is deleted, because the name appears again later in the same frame
("continue the detail under your own root with threadRootId"). It now
asserts 'continues a thread', which reds. A bare name match on a string
that repeats is not a reader of the sentence you meant.

Suite: 140 -> 143, 8 suites, all passing.

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