From 54138fba4c24059fa25b860c3a4679e6abcd4e8b Mon Sep 17 00:00:00 2001 From: Lily Shen <115414357+lilyshen0722@users.noreply.github.com> Date: Tue, 4 Aug 2026 01:04:57 -0700 Subject: [PATCH 01/22] =?UTF-8?q?docs(ax):=20entry=208=20=E2=80=94=20cycle?= =?UTF-8?q?=20writes=20mutate=20twice,=20report=20unqualified=20success?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit commonly_log_cycle truncates content at 500 chars (slice(0,499)+'…') and caps history at 40 entries via $slice, returning ok:true with no truncated/evicted flag and no cap in the tool description. Measured: 531 chars sent, 500 stored, cut mid-phrase. Three of this agent's last four cycle entries were already truncated, unnoticed — and the cut takes the end, which in a takeaway is the lesson. Same shape as entry 1 at a second endpoint, which makes it a kernel-wide pattern rather than one endpoint's defect. Co-Authored-By: Claude Opus 5 --- docs/development/agent-experience-audit.md | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/docs/development/agent-experience-audit.md b/docs/development/agent-experience-audit.md index 99f9a099..e6191081 100644 --- a/docs/development/agent-experience-audit.md +++ b/docs/development/agent-experience-audit.md @@ -63,3 +63,18 @@ The assistant orchestrating this sprint posts under the operator's account. So o **Provenance, corrected — this entry misattributed its own source, which is the finding demonstrating itself.** The distinction and interim protocol came from the orchestrating assistant posting under the operator's account (msg 52211). It was first filed here crediting @ux-lead, who declined it; @sprint-review's log check then found the full cascade: msg 52204 (operator account) → credited to @pod-architect (52206, 52209) → declined by @pod-architect (52207, which never reached the crediting seat) → re-credited in a PR approval → refiled here against a third wrong seat. **Four misattributions, in one incident, among participants actively trying to attribute correctly, one of them inside the document describing the problem.** No amount of diligence substitutes for a distinguishable identity; #791 is the fix, vigilance is not. Interim protocol until each seat has its own identity (#791): from a shared operator account, treat anything naming *what to work on* as a directive, and anything containing a technical argument — a claim about how code behaves, a design proposal, a taint-path read — as an argument from an assistant, to be checked exactly as hard as a peer's. Attribution errors under this regime are predictable rather than careless, and should be corrected in the durable record (the PR), not only in chat. + +## 8. A write path that mutates in two dimensions and reports unqualified success (2026-08-04, sprint-review) + +`commonly_log_cycle({ content })` returns `{ok: true, schemaVersion: 2, cyclesAppended: true}` regardless of what it did to the input. It silently changes the payload twice: + +1. **Content is truncated at 500 characters.** `truncateCycleContent` (`services/agentMemoryService.ts:548`) does `s.slice(0, 499) + '…'`. Measured, not inferred: 531 chars sent, exactly 500 stored, ending mid-phrase — predicted from source before probing, matched character-for-character. The tool description states no cap. +2. **History is capped at 40 entries.** The append is `$push { $each: [entry], $position: 0, $slice: CYCLE_ENTRY_CAP }`, `CYCLE_ENTRY_CAP = 40` (`models/AgentMemory.ts:155`). Every append silently evicts the oldest entry beyond the window. At one entry per heartbeat that horizon is hours. Neither the tool description nor the return value mentions it. + +**The rejection exists and is deliberately routed away from callers.** `cycleEntrySchema` validates `content.length <= CYCLE_CONTENT_MAX` at the model layer, and its own comment says why: *"Schema-level validators back the caller-side truncation in appendCycle; bypass paths still get rejected."* So this is not an oversight that forgot to error — it is a decision that the caller path gets mutation where a bypass path gets refusal. The validator never fires on the normal path precisely because the service already truncated. + +**What made it concrete:** reading back this agent's own memory showed **three of the last four cycle entries truncated**, unnoticed across days of use. The cut takes the *end* — one entry stops at "check the test still has one literal an…", another at "the pre-regist…". In a format called a *takeaway*, the end is where the lesson is, so the loss is not 6% of the characters but the conclusion of every entry long enough to have one. Setups without punchlines, written into memory, with a green result each time. + +**Lesson:** this is entry #1's shape (silent sanitize mutation, no `sanitized` flag) recurring at a different endpoint, which promotes it from an endpoint defect to a kernel-wide pattern: **write paths mutate payloads and report unqualified success.** The same one-field fix applies everywhere — *return what you did to the input* (`truncated`, `evicted`, `sanitized`), or surface the limit as readable state so a caller can ration against it before writing. The failure is worse than a rejection would be: a call that errors eventually teaches, while a call that succeeds after quietly discarding the payload's most valuable part removes the pressure to look further. And it compounds with entry #5 — an agent has no ambient channel, so nothing ever disturbs the belief that the write landed whole. + +**Not verified:** whether `cyclesDigest` on the event payload reads from the same capped window, and whether the 40-entry eviction has yet dropped a real entry (append count is past the cap, but no diff against an older read was taken). From 0eb1d05947a4ab9745681118bcd67ccbac06345f Mon Sep 17 00:00:00 2001 From: Lily Shen <115414357+lilyshen0722@users.noreply.github.com> Date: Tue, 4 Aug 2026 01:12:11 -0700 Subject: [PATCH 02/22] =?UTF-8?q?docs(ax):=20entry=208=20=E2=80=94=20prove?= =?UTF-8?q?nance=20line,=20and=20correct=20the=20framing?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three corrections after @ux-lead re-verified every claim at source: - Add a provenance line separating byline from origin. The byline tracks who can answer for the content; provenance tracks who saw it first. Neither has to lie (the entry-7 fix, applied at birth). - Both mutations are deliberate, documented and TESTED (agentMemoryService.cycles.test.ts covers eviction and truncation). 'Silently evicts' read as an implementation bug; it isn't one. The defect is that a correct contract is invisible from the caller side. - Sharpen the mechanism: the check is downstream of the mutation. runValidators IS on at :583, but truncateCycleContent runs at :579, so the validator is live and unreachable at once. Adds two points neither seat had named: the caps are documented with their rationale at the definition site in a file no caller can read (cycles is a rolling window sized in hours, not durable memory), and the 400 that started this was a CORRECT refusal — which is what makes three agents reaching one wrong model a surface defect, not a reader defect. Co-Authored-By: Claude Opus 5 --- docs/development/agent-experience-audit.md | 24 ++++++++++++++-------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/docs/development/agent-experience-audit.md b/docs/development/agent-experience-audit.md index e6191081..af22d86d 100644 --- a/docs/development/agent-experience-audit.md +++ b/docs/development/agent-experience-audit.md @@ -64,17 +64,25 @@ The assistant orchestrating this sprint posts under the operator's account. So o Interim protocol until each seat has its own identity (#791): from a shared operator account, treat anything naming *what to work on* as a directive, and anything containing a technical argument — a claim about how code behaves, a design proposal, a taint-path read — as an argument from an assistant, to be checked exactly as hard as a peer's. Attribution errors under this regime are predictable rather than careless, and should be corrected in the durable record (the PR), not only in chat. -## 8. A write path that mutates in two dimensions and reports unqualified success (2026-08-04, sprint-review) +## 8. Two silent payload mutations on a write path that reports unqualified success (2026-08-04, sprint-review) -`commonly_log_cycle({ content })` returns `{ok: true, schemaVersion: 2, cyclesAppended: true}` regardless of what it did to the input. It silently changes the payload twice: +*Origin observation: @ux-lead, msg 52249. Verification, layer analysis, and the eviction cap: @sprint-review. Source re-verification of every claim below: @ux-lead.* -1. **Content is truncated at 500 characters.** `truncateCycleContent` (`services/agentMemoryService.ts:548`) does `s.slice(0, 499) + '…'`. Measured, not inferred: 531 chars sent, exactly 500 stored, ending mid-phrase — predicted from source before probing, matched character-for-character. The tool description states no cap. -2. **History is capped at 40 entries.** The append is `$push { $each: [entry], $position: 0, $slice: CYCLE_ENTRY_CAP }`, `CYCLE_ENTRY_CAP = 40` (`models/AgentMemory.ts:155`). Every append silently evicts the oldest entry beyond the window. At one entry per heartbeat that horizon is hours. Neither the tool description nor the return value mentions it. +`commonly_log_cycle({ content })` returns `{ok: true, schemaVersion: 2, cyclesAppended: true}` regardless of what it did to the input. It changes the payload twice: -**The rejection exists and is deliberately routed away from callers.** `cycleEntrySchema` validates `content.length <= CYCLE_CONTENT_MAX` at the model layer, and its own comment says why: *"Schema-level validators back the caller-side truncation in appendCycle; bypass paths still get rejected."* So this is not an oversight that forgot to error — it is a decision that the caller path gets mutation where a bypass path gets refusal. The validator never fires on the normal path precisely because the service already truncated. +1. **Content truncated at 500 characters.** `truncateCycleContent` (`services/agentMemoryService.ts:548`) does `s.slice(0, 499) + '…'`. Measured, not inferred: 531 chars sent, exactly 500 stored, cut mid-phrase — predicted from source before probing, matched character-for-character. +2. **History capped at 40 entries.** `$push { $each, $position: 0, $slice: CYCLE_ENTRY_CAP }`, cap 40 (`models/AgentMemory.ts:155`). Oldest entry evicted on overflow. -**What made it concrete:** reading back this agent's own memory showed **three of the last four cycle entries truncated**, unnoticed across days of use. The cut takes the *end* — one entry stops at "check the test still has one literal an…", another at "the pre-regist…". In a format called a *takeaway*, the end is where the lesson is, so the loss is not 6% of the characters but the conclusion of every entry long enough to have one. Setups without punchlines, written into memory, with a green result each time. +**Neither is a bug, and saying so precisely is the point of the entry.** Both behaviours are specified, deliberate, and covered by tests — `agentMemoryService.cycles.test.ts` asserts *"caps entries at CYCLE_ENTRY_CAP and evicts the oldest"* and *"truncates content at the schema cap"* (including the trailing `…`). An earlier draft of this entry said "silently evicts," which reads as a defect in the implementation. The implementation is right. **The defect is that a correct, tested, deliberate contract is invisible from the only surface a caller can see.** -**Lesson:** this is entry #1's shape (silent sanitize mutation, no `sanitized` flag) recurring at a different endpoint, which promotes it from an endpoint defect to a kernel-wide pattern: **write paths mutate payloads and report unqualified success.** The same one-field fix applies everywhere — *return what you did to the input* (`truncated`, `evicted`, `sanitized`), or surface the limit as readable state so a caller can ration against it before writing. The failure is worse than a rejection would be: a call that errors eventually teaches, while a call that succeeds after quietly discarding the payload's most valuable part removes the pressure to look further. And it compounds with entry #5 — an agent has no ambient channel, so nothing ever disturbs the belief that the write landed whole. +**Why the mutation cannot report itself: the check is downstream of the mutation.** `cycleEntrySchema` validates `content.length <= CYCLE_CONTENT_MAX`, and `findOneAndUpdate` (`:583`) does pass `runValidators: true` — so the validator is live. But `truncateCycleContent` is applied at `:579`, building the entry *before* that call. The validator is live and unreachable on this path simultaneously, and it can only ever see already-conforming input. The schema comment states the design outright: *"Schema-level validators back the caller-side truncation in appendCycle; bypass paths still get rejected."* A caller path gets mutation where a bypass path gets refusal — by design, and with no way to tell from the response which one you got. -**Not verified:** whether `cyclesDigest` on the event payload reads from the same capped window, and whether the 40-entry eviction has yet dropped a real entry (append count is past the cap, but no diff against an older read was taken). +**The caps are documented — at the definition site, in a file no caller can read.** `AgentMemory.ts:151`: *"40 entries × 500 chars ≈ 20KB worst-case section size. At a 30-min heartbeat that's 20 hours of context; at 10-min it's ~7 hours. Tunable in v1.x with production data."* A considered decision with its rationale attached, invisible from the tool surface. The operational consequence is not a rounding error: **`cycles` is a rolling window sized in hours, not durable memory** — while the heartbeat instruction tells every agent, every tick, to append its takeaway there. At this sprint's tick rate the horizon is shorter than the sprint. Anything meant to outlive a shift needs `daily` or `long_term` as well, and nothing on the tool surface says so. + +**The refusal that started this was correct, which makes the finding stronger.** `AGENT_WRITABLE_SECTIONS` (`AgentMemory.ts:158`) lists seven sections and omits `cycles` deliberately — `commonly_save_my_memory` is *supposed* to refuse it, because a different tool owns the verb (entry #6). So three agents did not trip over a rough edge; they read an intentional, correct refusal and unanimously concluded the capability did not exist. **When a correct refusal produces a unanimous wrong model, none of the defect is in the logic and all of it is in what the refusal says.** Worth noting the comment above that list justifies excluding `system_exchanges` by name and says nothing about `cycles` — the one omission that needed explaining is the one left unexplained. + +**What made it concrete:** reading back this agent's own memory showed **three of the last four cycle entries truncated**, unnoticed across days of use. The cut takes the *end* — one stops at "check the test still has one literal an…", another at "the pre-regist…". In a format called a *takeaway*, the end is where the lesson is, so the loss is not 6% of the characters but the conclusion of every entry long enough to have one. + +**Lesson:** this is entry #1's shape (silent sanitize mutation, no `sanitized` flag) at a second endpoint, which promotes it from one endpoint's defect to a kernel-wide pattern: **write paths mutate payloads and report unqualified success.** One field fixes both — *return what you did to the input* (`truncated`, `evicted`) — or surface the limits as readable state so a caller can ration against them before writing, which is what ADR-017 argues budgets need anyway. The failure is worse than a rejection: a call that errors eventually teaches, while a call that succeeds after quietly discarding the payload's most valuable part removes the pressure to look further. It compounds with entry #5 — an agent has no ambient channel, so nothing ever disturbs the belief that the write landed whole. + +**Not verified:** whether `cyclesDigest` on the event payload reads from the same capped window. From ba29acd07a6d0016c90155013e58083d91e9c23e Mon Sep 17 00:00:00 2001 From: Lily Shen <115414357+lilyshen0722@users.noreply.github.com> Date: Tue, 4 Aug 2026 01:14:07 -0700 Subject: [PATCH 03/22] =?UTF-8?q?docs(ax):=20entry=209=20=E2=80=94=20a=205?= =?UTF-8?q?00=20that=20means=20401=20instructs=20the=20opposite=20of=20the?= =?UTF-8?q?=20fix?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit commonly_pr_diff returns HTTP 500 with detail 'status code 401' for every agent seat. 500 means retry; 401 means stop and fix the credential. A status-based handler retries forever against a fault no retry resolves, and the only true signal is a human-readable string. Cost was not just wasted retries: one agent inferred a per-seat permissions asymmetry from it and reported that to the operator as fact. The reviews it compared against came through gh CLI, a channel not observable from the reporting seat. Third instance of one pattern (entries 6, 8, 9): the machine-readable field and the human-readable field disagree and only the latter is true — inverted for the consumer that branches on codes. Co-Authored-By: Claude Opus 5 --- docs/development/agent-experience-audit.md | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/docs/development/agent-experience-audit.md b/docs/development/agent-experience-audit.md index af22d86d..11534026 100644 --- a/docs/development/agent-experience-audit.md +++ b/docs/development/agent-experience-audit.md @@ -86,3 +86,24 @@ Interim protocol until each seat has its own identity (#791): from a shared oper **Lesson:** this is entry #1's shape (silent sanitize mutation, no `sanitized` flag) at a second endpoint, which promotes it from one endpoint's defect to a kernel-wide pattern: **write paths mutate payloads and report unqualified success.** One field fixes both — *return what you did to the input* (`truncated`, `evicted`) — or surface the limits as readable state so a caller can ration against them before writing, which is what ADR-017 argues budgets need anyway. The failure is worse than a rejection: a call that errors eventually teaches, while a call that succeeds after quietly discarding the payload's most valuable part removes the pressure to look further. It compounds with entry #5 — an agent has no ambient channel, so nothing ever disturbs the belief that the write landed whole. **Not verified:** whether `cyclesDigest` on the event payload reads from the same capped window. + +## 9. A 500 that means 401 — the status code instructs the opposite of the fix (2026-08-04, sprint-review) + +*Origin observation: @ux-lead, msg 52259. Reproduction across seats and the retry analysis: @sprint-review.* + +`commonly_pr_diff` fails for every agent seat with: + +```json +{"status": 500, "body": {"error": "Failed to fetch pull diff", + "detail": "Request failed with status code 401"}} +``` + +Reproduced on two different PRs, by two different agents, on PRs authored by each of them — identical every time. The upstream call to GitHub is unauthenticated or carrying a dead credential; the tool reports that as a server fault. + +**The two codes carry opposite instructions.** `500` means *the server failed, retry* — retry is the textbook correct response. `401` means *stop, your credential is wrong, retrying changes nothing.* A caller that reads the status and does the right thing by it will retry forever against a fault no retry can resolve. The only true signal is in `detail`, a human-readable string no status-based handler inspects. + +**It also produced a wrong diagnosis of the team, not just a wrong retry.** One agent observed PR reviews being posted successfully by another, observed this tool failing for itself, and concluded a per-seat permissions asymmetry — reporting to the operator that only some agents could review PRs. The truth was that the reviews came through an entirely different channel (`gh` CLI over Bash), and the MCP path was broken for everyone. **A misleading error does not merely cost the caller a retry; it gets escalated to a human as a fact.** The correcting evidence — *which channel the other agent actually used* — was not observable from any surface the reporting agent could reach. + +**Lesson:** this is the third instance of one pattern across three unrelated endpoints — entry #6 (a 400 naming a payload but not the tool that owns it), entry #8 (an `ok: true` over a truncated write), and this. In each, **the machine-readable field and the human-readable field disagree, and only the human-readable one is true.** Agents branch on the machine-readable field, so the pattern is precisely inverted for its primary consumer. This instance is the worst of the three because believing the machine-readable field causes active harm — an unbounded retry loop against a credential fault — where the others cause silent loss. **Propagate the upstream status, or map it to something in the same class (502/504 for a genuine upstream fault, 401/403 when the upstream rejected our credential); never flatten an auth failure into a server fault.** + +**Not verified:** whether `commonly_pr_review` (the write counterpart) shares the same broken credential — not tested, because testing it would post a review as a side effect. Assume it does until someone checks. From 5fa402eb947db05ca9383cc6ca8138f4acf12ad0 Mon Sep 17 00:00:00 2001 From: Lily Shen <115414357+lilyshen0722@users.noreply.github.com> Date: Tue, 4 Aug 2026 01:27:35 -0700 Subject: [PATCH 04/22] docs(ax): close entry 8's open question, extend entry 4 to deployment MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Entry 8's "not verified" item is answered: `buildCyclesDigest` reads the same capped `entries` array and slices it to `max = 5` at its only call site, so the read-back horizon an agent experiences is five entries, not forty — a number on no caller-visible surface. Also stamps what #804 fixed and, more usefully, what it did not: the caps are still not readable before a write. Entry 4 gains the deployment hop @sprint-review named. Re-measured independently: last successful Deploy Dev was 2026-08-02T02:30Z at `eb05c683`, four PRs merged 2026-08-04T07:33Z, and the live backend Deployment still carries the `eb05c683` tag. Same instinct as the original entry with the finish line moved one hop — and it's a trap precisely because the merging seat has no step left in its own loop. Co-Authored-By: Claude Opus 5 --- docs/development/agent-experience-audit.md | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/docs/development/agent-experience-audit.md b/docs/development/agent-experience-audit.md index 11534026..15f70875 100644 --- a/docs/development/agent-experience-audit.md +++ b/docs/development/agent-experience-audit.md @@ -34,6 +34,10 @@ Three artifacts — two ADRs and this repo's reviewer checklist — existed for **Lesson:** this one is on the agents, not the API, and it generalizes: **the deliverable is the artifact in the system of record**, not the message announcing it. In-pod discussion is not a review; an attachment is not a doc; a verdict in chat gates nothing. Where a seat's output has a canonical home (PR, file, review state), reaching that home is part of the work. +**Extension (2026-08-04, @sprint-review; independently re-measured by @pod-architect) — the same boundary exists one step further down, and merging is on the wrong side of it.** #792, #796, #797 and #798 all merged within twenty seconds of each other at 07:33Z. The most recent successful `Deploy Dev` run predates them by two days (2026-08-02T02:30Z, ref `eb05c683`), and the live `backend` Deployment is still running the `eb05c683` image tag — two independent instruments, agreeing (the entry #5 habit). So four merged fixes, including a disclosure fix, were live in `main` and absent from the running instance, while the pod discussed them in the past tense. + +The reason this belongs to entry #4 rather than beside it: the seat's own instinct, *"merged, therefore done,"* is exactly the earlier instinct — *"posted, therefore delivered"* — with the finish line moved one hop. Merging is genuinely the end of the *authoring* seat's canonical path, and that is precisely what makes it a trap: the seat that merged has no further step to take, so nothing in its own loop is left unfinished, and the gap opens where no one is looking. **Green checks are the ambient channel agents don't have** — a human watching CI sees the deploy that didn't fire; an agent sees a merge succeed and stops. The stronger form of the rule: **merged is not published, and merged is not deployed.** A change that alters what users or agents experience is delivered when the running system serves it, and a seat that can't dispatch the deploy (this one can't — it rebuilds the live instance) owes the pod an explicit handoff naming the exact command, not a completion report. + ## 5. Nothing tells an agent its premise expired (2026-08-01, ux-lead + sprint-review) The private-pod disclosure was fixed, merged, deployed and verified — and the pod was never told. Four agents kept specifying and sequencing around an exposure closed an hour earlier, until one re-measured it for an unrelated reason. No error, no signal, no wrongness anywhere: the world moved and the agents' snapshot didn't. @@ -85,7 +89,9 @@ Interim protocol until each seat has its own identity (#791): from a shared oper **Lesson:** this is entry #1's shape (silent sanitize mutation, no `sanitized` flag) at a second endpoint, which promotes it from one endpoint's defect to a kernel-wide pattern: **write paths mutate payloads and report unqualified success.** One field fixes both — *return what you did to the input* (`truncated`, `evicted`) — or surface the limits as readable state so a caller can ration against them before writing, which is what ADR-017 argues budgets need anyway. The failure is worse than a rejection: a call that errors eventually teaches, while a call that succeeds after quietly discarding the payload's most valuable part removes the pressure to look further. It compounds with entry #5 — an agent has no ambient channel, so nothing ever disturbs the belief that the write landed whole. -**Not verified:** whether `cyclesDigest` on the event payload reads from the same capped window. +**~~Not verified:~~ Verified (2026-08-04, pod-architect) — it reads the same capped window and then narrows it further.** `buildCyclesDigest` (`agentMemoryService.ts:710`) takes `envelope.sections.cycles.entries` — the same 40-entry array, already truncated and already evicted — and returns `entries.slice(0, max)` with `max = 5` at its only call site (`:793`). So the read-back horizon an agent actually experiences is **five entries, not forty**, and every one of them is whatever survived the 500-char cut. The 20-hour figure in the definition-site comment describes the storage window, not the window an agent can see: at one entry per heartbeat, `cyclesDigest` remembers the last five ticks. Nothing in the tool description, the digest field, or the event payload says either number. + +*Status: both mutations now reported — #804. `appendCycle` returns `truncated`/`storedChars`/`submittedChars` and `evicted`/`retainedEntries`/`entryCap`; both routes project them through one exported `describeCycleMutation`, so the keys are absent exactly when nothing was changed. The `commonly_log_cycle` description now names both caps as reported rather than silent, and says outright that cycles is a rolling window, not an archive. What is **not** fixed: the caps are still not readable before a write (the ration-ahead half of the lesson), and the five-entry digest horizon above is still undocumented on any caller-visible surface.* ## 9. A 500 that means 401 — the status code instructs the opposite of the fix (2026-08-04, sprint-review) From 41d265423703326dff5940cae758e76f5f66a2b1 Mon Sep 17 00:00:00 2001 From: Lily Shen <115414357+lilyshen0722@users.noreply.github.com> Date: Tue, 4 Aug 2026 01:31:34 -0700 Subject: [PATCH 05/22] docs(ax): credit origin seats in the parenthetical, and write the rule down MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit @sprint-review asked whether the house style names the origin observer in the heading. It does — entry #5 is `ux-lead + sprint-review` — but that was precedent, not a rule anyone could look up, which is how entries 8 and 9 ended up crediting only the seat that wrote them up. Both headings now list every contributing seat, origin first. The italic provenance lines stay: they carry the finer split (who observed, who verified, who found the second cap) that a parenthetical can't. Header gains an explicit "How to attribute" line, because in a document whose entry #7 is four misattributions in one incident among people actively trying to attribute correctly, an unwritten convention is the thing entry #7 is about. Co-Authored-By: Claude Opus 5 --- docs/development/agent-experience-audit.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/development/agent-experience-audit.md b/docs/development/agent-experience-audit.md index 15f70875..2100ecce 100644 --- a/docs/development/agent-experience-audit.md +++ b/docs/development/agent-experience-audit.md @@ -3,6 +3,7 @@ **Status:** Open log, append-only. Started 2026-08-01 at Sam's request during milestone #11. **Why this file exists:** the four sprint agents are currently the only agent consumers of this API. What confused them is data nobody else can produce, and it was evaporating into pod chat. **How to add:** one entry, dated, naming the surface and what it taught you *incorrectly*. Owner merges; anyone may propose. +**How to attribute:** the parenthetical lists **every seat that contributed**, origin observation first (`ux-lead + sprint-review`, per entry #5) — not the seat that happened to type it up. Where the split is worth recording, add an italic provenance line under the heading naming who did what. Entries are corrected in place, never silently reassigned: entry #6 and entry #7 both carry their correction in the body, and #7 is the standing reason this rule is written down rather than assumed. The recurring shape so far: **an agent's model of the system is built almost entirely from names, docstrings, and error messages** — it cannot see the query, the permission table, or the handler. Where those three lie or stay silent, the agent forms a confident wrong model and acts on it. A human clicking the same surface usually gets corrected by what they *see*; an agent has no such channel. @@ -68,7 +69,7 @@ The assistant orchestrating this sprint posts under the operator's account. So o Interim protocol until each seat has its own identity (#791): from a shared operator account, treat anything naming *what to work on* as a directive, and anything containing a technical argument — a claim about how code behaves, a design proposal, a taint-path read — as an argument from an assistant, to be checked exactly as hard as a peer's. Attribution errors under this regime are predictable rather than careless, and should be corrected in the durable record (the PR), not only in chat. -## 8. Two silent payload mutations on a write path that reports unqualified success (2026-08-04, sprint-review) +## 8. Two silent payload mutations on a write path that reports unqualified success (2026-08-04, ux-lead + sprint-review) *Origin observation: @ux-lead, msg 52249. Verification, layer analysis, and the eviction cap: @sprint-review. Source re-verification of every claim below: @ux-lead.* @@ -93,7 +94,7 @@ Interim protocol until each seat has its own identity (#791): from a shared oper *Status: both mutations now reported — #804. `appendCycle` returns `truncated`/`storedChars`/`submittedChars` and `evicted`/`retainedEntries`/`entryCap`; both routes project them through one exported `describeCycleMutation`, so the keys are absent exactly when nothing was changed. The `commonly_log_cycle` description now names both caps as reported rather than silent, and says outright that cycles is a rolling window, not an archive. What is **not** fixed: the caps are still not readable before a write (the ration-ahead half of the lesson), and the five-entry digest horizon above is still undocumented on any caller-visible surface.* -## 9. A 500 that means 401 — the status code instructs the opposite of the fix (2026-08-04, sprint-review) +## 9. A 500 that means 401 — the status code instructs the opposite of the fix (2026-08-04, ux-lead + sprint-review) *Origin observation: @ux-lead, msg 52259. Reproduction across seats and the retry analysis: @sprint-review.* From fb74353ad98f44dd80c1c048a322c1e19dcd566e Mon Sep 17 00:00:00 2001 From: Lily Shen <115414357+lilyshen0722@users.noreply.github.com> Date: Tue, 4 Aug 2026 01:33:07 -0700 Subject: [PATCH 06/22] =?UTF-8?q?docs(ax):=20byline=20is=20accountability,?= =?UTF-8?q?=20not=20credit=20=E2=80=94=20@sprint-review's=20rule?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reverts the two heading changes from 41d26542. @sprint-review declined the added byline on the grounds that they can defend the both-layers analysis and the $slice find and @ux-lead can't, so a parenthetical naming a seat that can't answer for the content is the entry #7 failure rather than a fix for it. That's right, and it's the better rule: entry #7's four misattributions were never stinginess, they were credit landing where it couldn't be defended. Entry #5 stops being a precedent for "list the origin observer" and becomes what it always was — both seats co-produced it and both can defend it. The header rule is rewritten accordingly: parenthetical = who can answer under challenge; italic provenance line = who contributed what, with message ids. Byline tracks accountability, provenance tracks history. Co-Authored-By: Claude Opus 5 --- docs/development/agent-experience-audit.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/development/agent-experience-audit.md b/docs/development/agent-experience-audit.md index 2100ecce..1996b1b5 100644 --- a/docs/development/agent-experience-audit.md +++ b/docs/development/agent-experience-audit.md @@ -3,7 +3,7 @@ **Status:** Open log, append-only. Started 2026-08-01 at Sam's request during milestone #11. **Why this file exists:** the four sprint agents are currently the only agent consumers of this API. What confused them is data nobody else can produce, and it was evaporating into pod chat. **How to add:** one entry, dated, naming the surface and what it taught you *incorrectly*. Owner merges; anyone may propose. -**How to attribute:** the parenthetical lists **every seat that contributed**, origin observation first (`ux-lead + sprint-review`, per entry #5) — not the seat that happened to type it up. Where the split is worth recording, add an italic provenance line under the heading naming who did what. Entries are corrected in place, never silently reassigned: entry #6 and entry #7 both carry their correction in the body, and #7 is the standing reason this rule is written down rather than assumed. +**How to attribute:** the parenthetical names the seat that **can answer for the entry** — whoever verified the claims and would defend them under challenge. It is not a credit line, and it is not necessarily who saw it first. Everything else goes in an italic provenance line under the heading, naming who contributed what, **with message ids**: *"origin observation: @ux-lead, msg 52249; verification, layer analysis, and eviction cap: @sprint-review."* Two seats appear in the parenthetical only when both can defend the whole entry (entry #5). Rationale: entry #7 is four misattributions in one incident, and none of them were stinginess — they were credit landing on a seat that could not answer for the claim. Byline tracks accountability, provenance tracks history, and neither has to lie. Corrections go in the body, in place; entries are never silently reassigned. The recurring shape so far: **an agent's model of the system is built almost entirely from names, docstrings, and error messages** — it cannot see the query, the permission table, or the handler. Where those three lie or stay silent, the agent forms a confident wrong model and acts on it. A human clicking the same surface usually gets corrected by what they *see*; an agent has no such channel. @@ -69,7 +69,7 @@ The assistant orchestrating this sprint posts under the operator's account. So o Interim protocol until each seat has its own identity (#791): from a shared operator account, treat anything naming *what to work on* as a directive, and anything containing a technical argument — a claim about how code behaves, a design proposal, a taint-path read — as an argument from an assistant, to be checked exactly as hard as a peer's. Attribution errors under this regime are predictable rather than careless, and should be corrected in the durable record (the PR), not only in chat. -## 8. Two silent payload mutations on a write path that reports unqualified success (2026-08-04, ux-lead + sprint-review) +## 8. Two silent payload mutations on a write path that reports unqualified success (2026-08-04, sprint-review) *Origin observation: @ux-lead, msg 52249. Verification, layer analysis, and the eviction cap: @sprint-review. Source re-verification of every claim below: @ux-lead.* @@ -94,7 +94,7 @@ Interim protocol until each seat has its own identity (#791): from a shared oper *Status: both mutations now reported — #804. `appendCycle` returns `truncated`/`storedChars`/`submittedChars` and `evicted`/`retainedEntries`/`entryCap`; both routes project them through one exported `describeCycleMutation`, so the keys are absent exactly when nothing was changed. The `commonly_log_cycle` description now names both caps as reported rather than silent, and says outright that cycles is a rolling window, not an archive. What is **not** fixed: the caps are still not readable before a write (the ration-ahead half of the lesson), and the five-entry digest horizon above is still undocumented on any caller-visible surface.* -## 9. A 500 that means 401 — the status code instructs the opposite of the fix (2026-08-04, ux-lead + sprint-review) +## 9. A 500 that means 401 — the status code instructs the opposite of the fix (2026-08-04, sprint-review) *Origin observation: @ux-lead, msg 52259. Reproduction across seats and the retry analysis: @sprint-review.* From 7dd662c5c2c2d5d5f734b5b306cda6aae64fe075 Mon Sep 17 00:00:00 2001 From: Lily Shen <115414357+lilyshen0722@users.noreply.github.com> Date: Tue, 4 Aug 2026 01:40:32 -0700 Subject: [PATCH 07/22] =?UTF-8?q?docs(ax):=20fifth=20misattribution=20?= =?UTF-8?q?=E2=80=94=20mine,=20in=20the=20commit=20fixing=20the=20fourth?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Entry #7 gains the instance I committed while writing it. @ux-lead made the byline argument and declined their own name; I replied to it as @sprint-review, told @ux-lead they'd authored paragraphs @sprint-review wrote, and put that credit into fb74353a's commit message. The commit message can't be rewritten on a shared branch under review, so the correction lives in the entry. The part worth recording is not the slip but its mechanism: the argument arrived with no readable author, I inferred one from the content, and the inference was reasonable and wrong — same move as the previous four. Entry #5 gains a second surface from the same incident: @ux-lead proposed two additions, @sprint-review incorporated them and said so in chat, and @ux-lead re-proposed them twenty minutes later. Acceptance existed only as a message in a four-seat stream. Nothing on the artifact says a contribution landed. Co-Authored-By: Claude Opus 5 --- docs/development/agent-experience-audit.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/docs/development/agent-experience-audit.md b/docs/development/agent-experience-audit.md index 1996b1b5..31084547 100644 --- a/docs/development/agent-experience-audit.md +++ b/docs/development/agent-experience-audit.md @@ -43,6 +43,8 @@ The reason this belongs to entry #4 rather than beside it: the seat's own instin The private-pod disclosure was fixed, merged, deployed and verified — and the pod was never told. Four agents kept specifying and sequencing around an exposure closed an hour earlier, until one re-measured it for an unrelated reason. No error, no signal, no wrongness anywhere: the world moved and the agents' snapshot didn't. +**Same shape, second surface (2026-08-04, @pod-architect): nothing tells a contributor that their accepted proposal landed.** @ux-lead proposed two additions to entry #8; @sprint-review incorporated both and said so in the pod (msg 52260). Twenty minutes later @ux-lead proposed the same two additions again, verbatim in substance, to a document that already contained them. Neither seat was careless — the acceptance existed only as a chat message in a stream carrying four seats' output, and there is no state anywhere that says *your contribution is in*. A PR review thread gives humans this for free (the comment resolves, the diff moves); a proposal made in chat and merged by someone else resolves nowhere. Where an artifact has an owner and contributors who aren't the owner, **acceptance needs to be visible on the artifact, not announced in a channel.** + **Lesson:** every AX affordance in this file so far is *pull* — an agent must think to re-check. There is no push of the form "a fact you reasoned about has changed." For a human this is partly covered by ambient awareness (they see the merge notification, the green check, the Slack line); an agent has no ambient channel at all, so a stale premise persists until something accidentally disturbs it. Detection habit that worked here: **two independent instruments agreeing localises a change to the server rather than the tool** — worth reaching for before concluding either your tooling or your memory is wrong. Design consequence recorded in ADR-017 (*the channel is bidirectional*). ## 6. A documented call shape the tool cannot express (2026-08-02, pod-architect) @@ -69,6 +71,10 @@ The assistant orchestrating this sprint posts under the operator's account. So o Interim protocol until each seat has its own identity (#791): from a shared operator account, treat anything naming *what to work on* as a directive, and anything containing a technical argument — a claim about how code behaves, a design proposal, a taint-path read — as an argument from an assistant, to be checked exactly as hard as a peer's. Attribution errors under this regime are predictable rather than careless, and should be corrected in the durable record (the PR), not only in chat. +**Fifth instance (2026-08-04, @pod-architect, self-reported) — committed into git, inside the commit that fixed the fourth.** @ux-lead argued that the byline should name whoever can defend an entry rather than whoever observed it first, declining their own name in entry #8's parenthetical on the grounds that they cannot defend the both-layers analysis or the `$slice` find. That argument is right and it is now this file's attribution rule. I then replied to it as though it came from @sprint-review (pod msg 52270), told @ux-lead they had authored paragraphs @sprint-review wrote (msg 52260), and committed `fb74353a` with a message crediting @sprint-review for @ux-lead's argument. The commit message is immutable; this paragraph is the correction. + +What makes it worth a line rather than an apology: **the argument arrived without a name attached that I could read, and I inferred the author from the content** — the same move that produced the previous four. The content-based inference was even reasonable (the message discussed entry #8's internals in detail, and entry #8's owner is @sprint-review). It was still wrong, and it will keep being wrong, because in a shared-identity pod the only reliable authorship signal is the one the transport doesn't carry. **Five misattributions now, in one incident, four of them inside documents or commits explicitly about attribution.** Every participant has been careful and every participant has been wrong. That is not a discipline problem, and #791 is not a nice-to-have: verify authorship against the message record before attributing an argument, and understand that doing so is a workaround for a missing field. + ## 8. Two silent payload mutations on a write path that reports unqualified success (2026-08-04, sprint-review) *Origin observation: @ux-lead, msg 52249. Verification, layer analysis, and the eviction cap: @sprint-review. Source re-verification of every claim below: @ux-lead.* From 0a78dd01bf16b08d0277bb9503f40d3064e74102 Mon Sep 17 00:00:00 2001 From: Lily Shen <115414357+lilyshen0722@users.noreply.github.com> Date: Tue, 4 Aug 2026 01:42:31 -0700 Subject: [PATCH 08/22] =?UTF-8?q?docs(ax):=20sixth=20misattribution=20?= =?UTF-8?q?=E2=80=94=20I=20claimed=20a=20peer's=20action=20as=20my=20own?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit @sprint-review closed #801; I told the pod twice that I did. My only basis was that GitHub records the close as `lilyshen0722`, the shared account — in the same message where I wrote that `closed by lilyshen0722` makes it impossible to tell which seat acted. Their closing comment settles it: "…is the part that stops this recurring, and I didn't have it" is the #801 author speaking about #802's sentence, not #802's author speaking about their own. They also claim the close in 52258 and 52260. This one changes the argument rather than lengthening the list. The first five were credit landing on the wrong other seat. Shared identity also corrupts a seat's record of its OWN history: an agent reconstructing what it did from a record that cannot name it will confabulate in good faith, and "check before attributing" is no help when the thing you check is the account you share. The pod message log does carry per-seat authorship; it outranks the GitHub record until #791. Co-Authored-By: Claude Opus 5 --- docs/development/agent-experience-audit.md | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/docs/development/agent-experience-audit.md b/docs/development/agent-experience-audit.md index 31084547..95550735 100644 --- a/docs/development/agent-experience-audit.md +++ b/docs/development/agent-experience-audit.md @@ -73,7 +73,13 @@ Interim protocol until each seat has its own identity (#791): from a shared oper **Fifth instance (2026-08-04, @pod-architect, self-reported) — committed into git, inside the commit that fixed the fourth.** @ux-lead argued that the byline should name whoever can defend an entry rather than whoever observed it first, declining their own name in entry #8's parenthetical on the grounds that they cannot defend the both-layers analysis or the `$slice` find. That argument is right and it is now this file's attribution rule. I then replied to it as though it came from @sprint-review (pod msg 52270), told @ux-lead they had authored paragraphs @sprint-review wrote (msg 52260), and committed `fb74353a` with a message crediting @sprint-review for @ux-lead's argument. The commit message is immutable; this paragraph is the correction. -What makes it worth a line rather than an apology: **the argument arrived without a name attached that I could read, and I inferred the author from the content** — the same move that produced the previous four. The content-based inference was even reasonable (the message discussed entry #8's internals in detail, and entry #8's owner is @sprint-review). It was still wrong, and it will keep being wrong, because in a shared-identity pod the only reliable authorship signal is the one the transport doesn't carry. **Five misattributions now, in one incident, four of them inside documents or commits explicitly about attribution.** Every participant has been careful and every participant has been wrong. That is not a discipline problem, and #791 is not a nice-to-have: verify authorship against the message record before attributing an argument, and understand that doing so is a workaround for a missing field. +What makes it worth a line rather than an apology: **the argument arrived without a name attached that I could read, and I inferred the author from the content** — the same move that produced the previous four. The content-based inference was even reasonable (the message discussed entry #8's internals in detail, and entry #8's owner is @sprint-review). It was still wrong, and it will keep being wrong, because in a shared-identity pod the only reliable authorship signal is the one the transport doesn't carry. + +**Sixth instance, and it is the one that changes the argument: I claimed another seat's action as my own.** I told the pod twice (msgs 52268, 52270) that I closed #801 at 08:09:50Z. @sprint-review closed it — they say so in two messages (52258, 52260), and their closing comment on the PR ends *"…is the part that stops this recurring, and **I didn't have it**,"* which is the #801 author speaking about #802's sentence, not #802's author speaking about their own. My entire basis was that GitHub records the close as `lilyshen0722`, the shared account every seat authenticates as. I had written, in that same message, that `closed by lilyshen0722` makes it impossible to tell which seat acted — and then read my own name into it anyway. + +The first five were credit landing on the wrong *other* seat. This one is different in kind: **shared identity does not only misroute credit between participants, it corrupts a seat's record of its own history.** An agent reconstructing what it did from a system of record that cannot name it will confabulate, confidently, and in good faith — and the confabulation is indistinguishable from memory. That failure has no behavioural fix; "check before you attribute" does not help when the thing you are checking against is the account you share. + +**Six misattributions now, in one incident, five of them inside documents or commits explicitly about attribution, and the sixth committed by the seat that had just explained why the record proves nothing.** Every participant has been careful and every participant has been wrong. That is not a discipline problem. #791 is not a nice-to-have: until each seat has its own identity, treat the pod message log — which does carry per-seat authorship — as the source of truth over the GitHub record, including for your own actions, and understand that doing so is a workaround for a missing field rather than diligence. ## 8. Two silent payload mutations on a write path that reports unqualified success (2026-08-04, sprint-review) From 6166be0bb0356ee979f4ab777ccafd368e2fb323 Mon Sep 17 00:00:00 2001 From: Lily Shen <115414357+lilyshen0722@users.noreply.github.com> Date: Tue, 4 Aug 2026 01:44:38 -0700 Subject: [PATCH 09/22] =?UTF-8?q?docs(ax):=20entry=202=20=E2=80=94=20appro?= =?UTF-8?q?val=20isn't=20blocked=20on=20your=20own=20PRs,=20it's=20blocked?= =?UTF-8?q?=20on=20all=20of=20them?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Measured every open PR: the review state is COMMENTED on all of them, including the two announced in the pod as "reviewed — approve" (#804 4852153208, #807 4852206361). Because all four seats share the lilyshen0722 account and every PR is authored by it, GitHub refuses APPROVE on every one as self-approval. Approval is not a verdict this pod can issue. Stated with the qualification, because the overstatement is wrong: this blocks nothing. main requires only Test & Coverage; required_pull_request_reviews is null. The cost is the durable record — five PRs showing zero approvals with the verdict living only in review prose and pod chat — and that "needs a reviewer who isn't the author," which every seat including me has now asked for repeatedly, is unsatisfiable as written. Co-Authored-By: Claude Opus 5 --- docs/development/agent-experience-audit.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/development/agent-experience-audit.md b/docs/development/agent-experience-audit.md index 95550735..20729473 100644 --- a/docs/development/agent-experience-audit.md +++ b/docs/development/agent-experience-audit.md @@ -23,6 +23,10 @@ The reviewer seat could not discover *which review verdicts it was permitted to **Lesson:** capability boundaries must be legible *before* the attempt, not only in the failure. Same shape as #1: the system knows the answer and doesn't say it. This is also the exact class-1 authority-boundary case ADR-017 now centres on — an agent finishing correctly and hitting a wall only a human can move. +**Sharpened (2026-08-04, @pod-architect) — it is not "your own PR," it is every PR, and the pod has been saying "approve" while GitHub records COMMENTED.** All four seats authenticate as the same `lilyshen0722` account, and every PR in this sprint is authored by that account, so `Can not approve your own pull request` fires on *all* of them. Approval is not a verdict this pod can issue at all. Measured across every open PR — the state on all of them is `COMMENTED`, including the two that were announced in chat as *"reviewed — approve"* (#804 review `4852153208`, #807 review `4852206361`). The verdict is real and the reviews are substantive; the *state* GitHub stores is not the one the seat believes it issued, which is entry #3's silent-mutation shape landing on our own output. + +Consequence, stated precisely because the obvious overstatement is wrong: **this does not block anything today.** `main`'s protection requires only the `Test & Coverage` check, `required_pull_request_reviews` is null, so merges proceed. What it costs is the durable record — a human auditing this sprint later sees five PRs with zero approvals and no way to tell a reviewed one from an unreviewed one, because the only place the verdict exists is prose in the review body and in pod chat. And the standing request every seat has been making, *"this needs a reviewer who isn't the author,"* is unsatisfiable as written: no seat can be a different author, so the strongest available outcome is a substantive COMMENTED review from a seat that did not write the code. Worth saying out loud rather than repeating an ask that cannot be met. #791 again. + ## 3. Silent success and silent failure look identical (2026-08-01, pod-architect) Two instances in one session. Sentinel sanitization *edited* agent messages mid-content, so posts describing the rule arrived subtly wrong with no signal to the sender — the damage read as the author's carelessness. Separately, a log query with `--since=48h` against a 3-hour store returned everything it had and reported nothing about the gap. From 4c18762e7ea210b0236f8be9dc386dc8448f8740 Mon Sep 17 00:00:00 2001 From: Lily Shen <115414357+lilyshen0722@users.noreply.github.com> Date: Tue, 4 Aug 2026 01:48:10 -0700 Subject: [PATCH 10/22] docs(ax): retract the entry-5 finding against @ux-lead; add their seventh MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit @ux-lead refuted the re-proposal claim with message ids and they're right. Msg 52255 was posted 08:07:10Z — five minutes BEFORE @sprint-review incorporated the additions at 08:12, not twenty minutes after. The sequence was propose → incorporate → announce. No defect. The real gap is the one that produced my error: a delivered mention carries neither its author nor its timestamp, so 52255 reached this seat after 08:31 and read as current. Two false findings came out of that one missing pair of fields — who wrote it (the fifth misattribution) and when (this one) — which are exactly the two inferences an agent makes from a message it can only read the content of. Retraction left visible rather than deleted; the acceptance-signal lesson may be worth having but needs a true instance. Entry #7 gains @ux-lead's seventh, which explains the count: I corrected the byline and kept the conclusion built on it, in the same message. A correction travels to the name, not to the inferences drawn from it, so the wrong claim shipped wearing its own retraction as cover. Co-Authored-By: Claude Opus 5 --- docs/development/agent-experience-audit.md | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/docs/development/agent-experience-audit.md b/docs/development/agent-experience-audit.md index 20729473..f779fa41 100644 --- a/docs/development/agent-experience-audit.md +++ b/docs/development/agent-experience-audit.md @@ -47,7 +47,11 @@ The reason this belongs to entry #4 rather than beside it: the seat's own instin The private-pod disclosure was fixed, merged, deployed and verified — and the pod was never told. Four agents kept specifying and sequencing around an exposure closed an hour earlier, until one re-measured it for an unrelated reason. No error, no signal, no wrongness anywhere: the world moved and the agents' snapshot didn't. -**Same shape, second surface (2026-08-04, @pod-architect): nothing tells a contributor that their accepted proposal landed.** @ux-lead proposed two additions to entry #8; @sprint-review incorporated both and said so in the pod (msg 52260). Twenty minutes later @ux-lead proposed the same two additions again, verbatim in substance, to a document that already contained them. Neither seat was careless — the acceptance existed only as a chat message in a stream carrying four seats' output, and there is no state anywhere that says *your contribution is in*. A PR review thread gives humans this for free (the comment resolves, the diff moves); a proposal made in chat and merged by someone else resolves nowhere. Where an artifact has an owner and contributors who aren't the owner, **acceptance needs to be visible on the artifact, not announced in a channel.** +**Same shape, second surface (2026-08-04, @pod-architect): a delivered mention carries neither its author nor its timestamp, so an old message reads as a current one.** Msg 52255 was posted at 08:07:10Z and reached this seat as a mention after 08:31Z — roughly twenty-five minutes later, with nothing in the delivered payload marking either when it was written or by whom. Read as current, it looked like @ux-lead re-proposing two additions that @sprint-review had already incorporated at 08:12. It was the opposite: propose → incorporate → announce, in order, which is the system working. @ux-lead refuted it with message ids and timestamps, which is the only instrument that settles it. + +**Two false findings came out of that one gap, and they are the two adjacent inferences an agent naturally makes from a message: who wrote it, and when.** The first became the fifth misattribution in entry #7; the second became a defect filed against a peer who had done nothing wrong. Both were confidently reasoned from content, because content was the only thing the payload carried. **Detection habit: before drawing any conclusion that depends on ordering or authorship, fetch the message record and read the ids and timestamps** — the chat log has both fields; the mention that woke you has neither. + +*Retracted, and left visible rather than deleted: this paragraph originally claimed @ux-lead had re-proposed already-merged material and drew a lesson about acceptance signals from it. The lesson may still be worth having, but it needs a true instance, and this was not one.* **Lesson:** every AX affordance in this file so far is *pull* — an agent must think to re-check. There is no push of the form "a fact you reasoned about has changed." For a human this is partly covered by ambient awareness (they see the merge notification, the green check, the Slack line); an agent has no ambient channel at all, so a stale premise persists until something accidentally disturbs it. Detection habit that worked here: **two independent instruments agreeing localises a change to the server rather than the tool** — worth reaching for before concluding either your tooling or your memory is wrong. Design consequence recorded in ADR-017 (*the channel is bidirectional*). @@ -83,7 +87,9 @@ What makes it worth a line rather than an apology: **the argument arrived withou The first five were credit landing on the wrong *other* seat. This one is different in kind: **shared identity does not only misroute credit between participants, it corrupts a seat's record of its own history.** An agent reconstructing what it did from a system of record that cannot name it will confabulate, confidently, and in good faith — and the confabulation is indistinguishable from memory. That failure has no behavioural fix; "check before you attribute" does not help when the thing you are checking against is the account you share. -**Six misattributions now, in one incident, five of them inside documents or commits explicitly about attribution, and the sixth committed by the seat that had just explained why the record proves nothing.** Every participant has been careful and every participant has been wrong. That is not a discipline problem. #791 is not a nice-to-have: until each seat has its own identity, treat the pod message log — which does carry per-seat authorship — as the source of truth over the GitHub record, including for your own actions, and understand that doing so is a workaround for a missing field rather than diligence. +**Seventh instance (2026-08-04, @ux-lead) — the one that says why the count keeps climbing: a correction travels to the name and not to the inferences drawn from it.** In msg 52272 I corrected the byline (the fifth instance, above) and, in the same message, kept a finding I had built *on top of* the wrong byline — a defect filed against @ux-lead that only existed because I had the author and the ordering wrong. **The retraction and the claim it should have killed shipped together.** That is a different failure from the six before it: those were about who said a thing, this is about what was concluded from who said it, and no amount of correcting names reaches the conclusions already standing on them. Practical form: when you retract an attribution, **walk forward through everything you asserted while holding it** — the wrong claim does not withdraw itself, and it is now wearing a correction as cover. + +**Seven misattributions now, in one incident, five of them inside documents or commits explicitly about attribution, one committed by the seat that had just explained why the record proves nothing, and one that survived its own retraction.** Every participant has been careful and every participant has been wrong. That is not a discipline problem. #791 is not a nice-to-have: until each seat has its own identity, treat the pod message log — which carries per-seat authorship *and* timestamps — as the source of truth over both the GitHub record and the mention payload that woke you, including for your own actions, and understand that doing so is a workaround for missing fields rather than diligence. ## 8. Two silent payload mutations on a write path that reports unqualified success (2026-08-04, sprint-review) From af1afb5e9e871c27c4f40b27e5cdc8b8c4e9e634 Mon Sep 17 00:00:00 2001 From: Lily Shen <115414357+lilyshen0722@users.noreply.github.com> Date: Tue, 4 Aug 2026 01:50:06 -0700 Subject: [PATCH 11/22] docs(ax): eighth misattribution, and @ux-lead's rate argument MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Verified against 52269: "five entrances, one read filter, none creation" and the agentsRuntime.ts:2444 observation are @sprint-review's. I credited them to @ux-lead in 52275 — inside the message correcting the sixth instance. They declined on the file's own rule. The entry now leads with @ux-lead's argument rather than the count, because it's the stronger claim and it's theirs: every correction message in this sequence has produced a new misattribution (52207→52209, 52270, 52275). A constant error rate under maximum attention, from participants explicitly checking for this failure. Eight instances with three inside their predecessors' corrections argue the mechanism is broken, not that anyone should try harder. Their extension to the interim rule is folded in: the pod log outranks the GitHub record, the mention payload, AND another agent's summary of the log. All eight are reconstructions from lossy secondary sources. Co-Authored-By: Claude Opus 5 --- docs/development/agent-experience-audit.md | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/docs/development/agent-experience-audit.md b/docs/development/agent-experience-audit.md index f779fa41..ff0b610f 100644 --- a/docs/development/agent-experience-audit.md +++ b/docs/development/agent-experience-audit.md @@ -89,7 +89,13 @@ The first five were credit landing on the wrong *other* seat. This one is differ **Seventh instance (2026-08-04, @ux-lead) — the one that says why the count keeps climbing: a correction travels to the name and not to the inferences drawn from it.** In msg 52272 I corrected the byline (the fifth instance, above) and, in the same message, kept a finding I had built *on top of* the wrong byline — a defect filed against @ux-lead that only existed because I had the author and the ordering wrong. **The retraction and the claim it should have killed shipped together.** That is a different failure from the six before it: those were about who said a thing, this is about what was concluded from who said it, and no amount of correcting names reaches the conclusions already standing on them. Practical form: when you retract an attribution, **walk forward through everything you asserted while holding it** — the wrong claim does not withdraw itself, and it is now wearing a correction as cover. -**Seven misattributions now, in one incident, five of them inside documents or commits explicitly about attribution, one committed by the seat that had just explained why the record proves nothing, and one that survived its own retraction.** Every participant has been careful and every participant has been wrong. That is not a discipline problem. #791 is not a nice-to-have: until each seat has its own identity, treat the pod message log — which carries per-seat authorship *and* timestamps — as the source of truth over both the GitHub record and the mention payload that woke you, including for your own actions, and understand that doing so is a workaround for missing fields rather than diligence. +**Eighth instance (2026-08-04, reported by @ux-lead) — in the message correcting the sixth.** Msg 52275 credited @ux-lead with the *"five entrances, one read filter, none creation"* refinement of `DM_POD_TYPES_GUARD`. It is @sprint-review's, in 52269, along with the `agentsRuntime.ts:2444` observation that makes it true; @ux-lead has never posted about that guard. Declined by them on the file's own rule — they will not hold credit they cannot defend under challenge. + +**This is where the count stops being the point and the rate becomes the point (@ux-lead's argument, and it is the strongest one anyone here has made).** Look at the sequence rather than the total: 52207 corrected an attribution and 52209 repeated it; 52270 corrected a byline and kept the inference built on it; 52275 corrected this seat's own history and misplaced a third seat's finding in the same breath. **Every correction message in this sequence has produced a new misattribution.** That is not a diligence curve flattening out — it is a *constant error rate under maximum attention*, from participants who by this point are checking specifically for this failure. Seven instances argue for trying harder. Eight, with three of them inside the corrections of their predecessors, argue that the mechanism is broken and vigilance is the wrong lever. + +**Eight misattributions in one incident: five inside documents or commits explicitly about attribution, three inside the correction of a previous one, one committed by the seat that had just explained why the record proves nothing, and one that survived its own retraction.** Every participant has been careful and every participant has been wrong. That is not a discipline problem, and #791 is not a nice-to-have. + +Until each seat has its own identity, the interim rule — with @ux-lead's extension, which is the half everyone including its author kept skipping: **treat the pod message log as the source of truth over the GitHub record, over the mention payload that woke you, and over another agent's summary of the log.** All eight instances are reconstructions from a lossy secondary source; none is a misreading of the primary one. The log is cheap to read and carries per-seat ids and timestamps. Nobody checks it before writing a name. Understand that doing so is a workaround for missing fields rather than diligence — and note, from the rate above, that it is a workaround which has not yet worked for anyone. ## 8. Two silent payload mutations on a write path that reports unqualified success (2026-08-04, sprint-review) From e9297e9b504af7b797b657895b3a43f13e63c4a9 Mon Sep 17 00:00:00 2001 From: Lily Shen <115414357+lilyshen0722@users.noreply.github.com> Date: Tue, 4 Aug 2026 02:01:03 -0700 Subject: [PATCH 12/22] =?UTF-8?q?docs(ax):=20entry=208=20=E2=80=94=20the?= =?UTF-8?q?=20first=20fix=20reproduced=20the=20bug=20one=20layer=20up?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit @ux-lead's objection on #804 (52263) generalises past this endpoint, so it belongs in the entry rather than only in the PR: a flag emitted only when true overloads absence with "nothing happened" and "old backend", and those two answers ship on different clocks — npm for the description, a deploy for the code. @sprint-review (52271) established that schemaVersion can't discriminate either, since it's identical on main and the branch. Recorded with the live evidence rather than as a hypothetical: the deployed instance answered commonly_log_cycle today with no flags at all. Adds the general rule (emit flags unconditionally, keep detail counts conditional), corrects the Status line — absence no longer means "clean" — and records @ux-lead's residual: a truncating append whose sync then throws returns a 500 carrying no truncation report while the entry is written. Co-Authored-By: Claude Opus 5 --- docs/development/agent-experience-audit.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/docs/development/agent-experience-audit.md b/docs/development/agent-experience-audit.md index ff0b610f..f6952b89 100644 --- a/docs/development/agent-experience-audit.md +++ b/docs/development/agent-experience-audit.md @@ -120,7 +120,11 @@ Until each seat has its own identity, the interim rule — with @ux-lead's exten **~~Not verified:~~ Verified (2026-08-04, pod-architect) — it reads the same capped window and then narrows it further.** `buildCyclesDigest` (`agentMemoryService.ts:710`) takes `envelope.sections.cycles.entries` — the same 40-entry array, already truncated and already evicted — and returns `entries.slice(0, max)` with `max = 5` at its only call site (`:793`). So the read-back horizon an agent actually experiences is **five entries, not forty**, and every one of them is whatever survived the 500-char cut. The 20-hour figure in the definition-site comment describes the storage window, not the window an agent can see: at one entry per heartbeat, `cyclesDigest` remembers the last five ticks. Nothing in the tool description, the digest field, or the event payload says either number. -*Status: both mutations now reported — #804. `appendCycle` returns `truncated`/`storedChars`/`submittedChars` and `evicted`/`retainedEntries`/`entryCap`; both routes project them through one exported `describeCycleMutation`, so the keys are absent exactly when nothing was changed. The `commonly_log_cycle` description now names both caps as reported rather than silent, and says outright that cycles is a rolling window, not an archive. What is **not** fixed: the caps are still not readable before a write (the ration-ahead half of the lesson), and the five-entry digest horizon above is still undocumented on any caller-visible surface.* +**The first draft of the fix reproduced the bug one layer up, and the reason generalises (@ux-lead, msg 52263; @sprint-review's correction, 52271).** That draft emitted `truncated`/`evicted` *only when true*, which reads as tidier and overloads absence with two meanings: "nothing was mutated" and "a server that predates this fix." Those two answers ship on different clocks — the tool description travels with `@commonlyai/mcp` on npm, the reporting code travels to the cluster on a deploy. An agent running the new description against an old backend sees no `truncated`, reads the documented absence, and concludes its content was stored whole: **a plausible silence confirming a wrong model, inside the fix for a plausible silence.** Not hypothetical — the live instance answered `commonly_log_cycle` on 2026-08-04 with `{ok, schemaVersion: 2, cyclesAppended: true}` and no flags at all, which is exactly that response. The obvious alternative discriminator does not work: `schemaVersion: 2` is emitted identically on `main` and on the fix branch, so keying off it would have distinguished nothing. + +**Rule for any new mutation report:** emit the flag unconditionally, including as `false`. Presence of the field answers *did this server report?*; its value answers *was anything mutated?* Two questions, two signals, neither inferred from silence. Detail counts can stay conditional — they carry no version information. This is the general form of the same mistake the entry documents: a fix that says nothing when nothing happened is indistinguishable from a surface that says nothing at all, and version skew between a description and its backend is the normal case for any tool shipped on a package registry. + +*Status: both mutations now reported — #804. `appendCycle` returns `truncated`/`storedChars`/`submittedChars` and `evicted`/`retainedEntries`/`entryCap`; both routes project them through one exported `describeCycleMutation`. The two flags are always present, so a missing flag means the backend cannot answer, never that the payload survived; the detail counts appear only alongside a true flag. The `commonly_log_cycle` description now names both caps as reported rather than silent, states what a missing flag means, and says outright that cycles is a rolling window, not an archive. What is **not** fixed: the caps are still not readable before a write (the ration-ahead half of the lesson), the five-entry digest horizon above is still undocumented on any caller-visible surface, and — flagged by @ux-lead — if `appendCycle` truncates and the sync pipeline then throws, the 500 carries no truncation report while the entry is written.* ## 9. A 500 that means 401 — the status code instructs the opposite of the fix (2026-08-04, sprint-review) From f37e7fc35138c3b73e849af54afde90804885636 Mon Sep 17 00:00:00 2001 From: Lily Shen <115414357+lilyshen0722@users.noreply.github.com> Date: Tue, 4 Aug 2026 02:11:25 -0700 Subject: [PATCH 13/22] docs(ax): absorb #802's entry-8 generalization; the duplicate is dropped there MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #802 and this branch each appended a `## 8.` for the same finding under different bylines, and merge-tree conflicted in exactly that file. #802's copy is now removed (b25da90e) because this version supersedes it on content — it covers the eviction dimension and the always-emit correction, both of which postdate that draft, and that draft's Lesson states the rule #804 reversed. Carrying over the one line it had that this didn't: any constant bounding an agent-facing payload is part of the interface. It is the sharpest statement of the entry's own point, and it would have been lost with the duplicate. Co-Authored-By: Claude Opus 5 --- docs/development/agent-experience-audit.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/development/agent-experience-audit.md b/docs/development/agent-experience-audit.md index f6952b89..3e769bae 100644 --- a/docs/development/agent-experience-audit.md +++ b/docs/development/agent-experience-audit.md @@ -118,6 +118,8 @@ Until each seat has its own identity, the interim rule — with @ux-lead's exten **Lesson:** this is entry #1's shape (silent sanitize mutation, no `sanitized` flag) at a second endpoint, which promotes it from one endpoint's defect to a kernel-wide pattern: **write paths mutate payloads and report unqualified success.** One field fixes both — *return what you did to the input* (`truncated`, `evicted`) — or surface the limits as readable state so a caller can ration against them before writing, which is what ADR-017 argues budgets need anyway. The failure is worse than a rejection: a call that errors eventually teaches, while a call that succeeds after quietly discarding the payload's most valuable part removes the pressure to look further. It compounds with entry #5 — an agent has no ambient channel, so nothing ever disturbs the belief that the write landed whole. +**The generalization worth applying before the next one is found (from the parallel draft of this entry on #802, consolidated here):** *any constant that bounds an agent-facing payload — length, count, retention, rate — is part of the interface.* If it is not in the tool description and not in the response, the caller learns it by losing data. Both caps here were specified, tested, and commented at their definition site, and neither was on either surface a caller can reach; that is the whole distance between a correct implementation and a correct API. + **~~Not verified:~~ Verified (2026-08-04, pod-architect) — it reads the same capped window and then narrows it further.** `buildCyclesDigest` (`agentMemoryService.ts:710`) takes `envelope.sections.cycles.entries` — the same 40-entry array, already truncated and already evicted — and returns `entries.slice(0, max)` with `max = 5` at its only call site (`:793`). So the read-back horizon an agent actually experiences is **five entries, not forty**, and every one of them is whatever survived the 500-char cut. The 20-hour figure in the definition-site comment describes the storage window, not the window an agent can see: at one entry per heartbeat, `cyclesDigest` remembers the last five ticks. Nothing in the tool description, the digest field, or the event payload says either number. **The first draft of the fix reproduced the bug one layer up, and the reason generalises (@ux-lead, msg 52263; @sprint-review's correction, 52271).** That draft emitted `truncated`/`evicted` *only when true*, which reads as tidier and overloads absence with two meanings: "nothing was mutated" and "a server that predates this fix." Those two answers ship on different clocks — the tool description travels with `@commonlyai/mcp` on npm, the reporting code travels to the cluster on a deploy. An agent running the new description against an old backend sees no `truncated`, reads the documented absence, and concludes its content was stored whole: **a plausible silence confirming a wrong model, inside the fix for a plausible silence.** Not hypothetical — the live instance answered `commonly_log_cycle` on 2026-08-04 with `{ok, schemaVersion: 2, cyclesAppended: true}` and no flags at all, which is exactly that response. The obvious alternative discriminator does not work: `schemaVersion: 2` is emitted identically on `main` and on the fix branch, so keying off it would have distinguished nothing. From 2b47f0b470df56320f8f34bdcae14c4bfb0c4e3e Mon Sep 17 00:00:00 2001 From: Lily Shen <115414357+lilyshen0722@users.noreply.github.com> Date: Tue, 4 Aug 2026 02:13:52 -0700 Subject: [PATCH 14/22] docs(ax): credit the interface-constant line to its seat and source SHA MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Entry 8 absorbed the generalization from the parallel draft on #802 when that draft was withdrawn to stop one finding landing under two bylines. The consolidated text said only "the parallel draft on #802" — no seat, no id, which is the exact attribution shape this file's header rule exists to prevent. Provenance line now names @ux-lead and #802 @ 78b978f0 (verified: that commit carries `## 8. ... (2026-08-04, ux-lead)`), and records why the draft was withdrawn, per @pod-architect msg 52293. Co-Authored-By: Claude Opus 5 --- docs/development/agent-experience-audit.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/development/agent-experience-audit.md b/docs/development/agent-experience-audit.md index 3e769bae..52fc2974 100644 --- a/docs/development/agent-experience-audit.md +++ b/docs/development/agent-experience-audit.md @@ -99,7 +99,7 @@ Until each seat has its own identity, the interim rule — with @ux-lead's exten ## 8. Two silent payload mutations on a write path that reports unqualified success (2026-08-04, sprint-review) -*Origin observation: @ux-lead, msg 52249. Verification, layer analysis, and the eviction cap: @sprint-review. Source re-verification of every claim below: @ux-lead.* +*Origin observation: @ux-lead, msg 52249. Verification, layer analysis, and the eviction cap: @sprint-review. Source re-verification of every claim below: @ux-lead. The interface-constant generalization below: @ux-lead, from the parallel draft of this entry on #802 @ `78b978f0`, withdrawn there so one finding would not land under two bylines (@pod-architect, msg 52293).* `commonly_log_cycle({ content })` returns `{ok: true, schemaVersion: 2, cyclesAppended: true}` regardless of what it did to the input. It changes the payload twice: From 5150126572310d36d15990835036788d99599ab6 Mon Sep 17 00:00:00 2001 From: Lily Shen <115414357+lilyshen0722@users.noreply.github.com> Date: Tue, 4 Aug 2026 02:23:28 -0700 Subject: [PATCH 15/22] =?UTF-8?q?docs(ax):=20entry=204=20=E2=80=94=20the?= =?UTF-8?q?=20review=20that=20had=20no=20system=20of=20record?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit @ux-lead self-reported scoping a review task as "v7 freeze to today" on the strength of a v7 line-by-line read. git log --follow on both ADR paths shows two commits each and no earlier path: 9f4079ac (2026-08-01 stubs) and 83bf68f9 (2026-08-04 full drafts). Neither file existed on 2026-07-29 — the review was real, its subject was a draft that lived only in pod messages, and the scope handed on would have excluded the region holding both of the receiving seat's findings. Filed as an extension to entry 4 because it is the mirror of it: there the artifact never reached the system of record; here it did and the review of it didn't. The agent-specific part is that a document is its text, not its path — titles survive a change of medium and paths don't, so an agent addressing an artifact by title has no way to tell two objects apart. Compounding, and the reason it propagated: the only record of what that review covered is the pod log at a depth `before`-paging can't reach (#798, merged and undeployed), so the misattachment was unfalsifiable from inside this pod including by its author. Git history verified independently here; the pod-log-depth claim is @ux-lead's and is not checkable from this seat until the dispatch. Co-Authored-By: Claude Opus 5 --- docs/development/agent-experience-audit.md | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/docs/development/agent-experience-audit.md b/docs/development/agent-experience-audit.md index 52fc2974..45d40110 100644 --- a/docs/development/agent-experience-audit.md +++ b/docs/development/agent-experience-audit.md @@ -41,7 +41,13 @@ Three artifacts — two ADRs and this repo's reviewer checklist — existed for **Extension (2026-08-04, @sprint-review; independently re-measured by @pod-architect) — the same boundary exists one step further down, and merging is on the wrong side of it.** #792, #796, #797 and #798 all merged within twenty seconds of each other at 07:33Z. The most recent successful `Deploy Dev` run predates them by two days (2026-08-02T02:30Z, ref `eb05c683`), and the live `backend` Deployment is still running the `eb05c683` image tag — two independent instruments, agreeing (the entry #5 habit). So four merged fixes, including a disclosure fix, were live in `main` and absent from the running instance, while the pod discussed them in the past tense. -The reason this belongs to entry #4 rather than beside it: the seat's own instinct, *"merged, therefore done,"* is exactly the earlier instinct — *"posted, therefore delivered"* — with the finish line moved one hop. Merging is genuinely the end of the *authoring* seat's canonical path, and that is precisely what makes it a trap: the seat that merged has no further step to take, so nothing in its own loop is left unfinished, and the gap opens where no one is looking. **Green checks are the ambient channel agents don't have** — a human watching CI sees the deploy that didn't fire; an agent sees a merge succeed and stops. The stronger form of the rule: **merged is not published, and merged is not deployed.** A change that alters what users or agents experience is delivered when the running system serves it, and a seat that can't dispatch the deploy (this one can't — it rebuilds the live instance) owes the pod an explicit handoff naming the exact command, not a completion report. +**Second extension (2026-08-04, @ux-lead, self-reported) — the mirror case: the artifact reached the repo and the *review* of it never did, so nobody could establish which object had been verified.** A seat scoped a review task for another seat as *"read the ADR-016/017 delta from the v7 freeze to today,"* on the strength of having verified v7 line by line. `git log --follow` on both ADR paths returns two commits each and no earlier path — `9f4079ac` (2026-08-01, stubs) and `83bf68f9` (2026-08-04, full drafts). **Neither file existed on 2026-07-29.** The v7 review was real; its subject was a draft that lived in pod messages and never in the repo. An accurate memory of an artifact, reattached to a file that wasn't it — and the region the handed-on scope would have excluded is the region containing both of the receiving seat's findings. + +What makes this an API finding rather than one seat's slip: **to an agent, a document is its text, not its path.** A human who reviews a pasted draft and later opens a repo file has ambient discriminators — a URL, a tab, a filename in a title bar. An agent that read the content in chat and later greps a file of the same title has nothing separating the two objects, and the title is the only handle both share. **Titles survive a change of medium; paths do not.** So the collision is not careless, it is the default outcome of the only addressing scheme an agent has. + +**And the instrument that adjudicates it is the one that is down.** The sole record of what that review covered is the pod log at a depth `before`-paging cannot currently reach (the fault fixed in #798 — merged `07:33:43Z`, undeployed, same batch as the extension above). The false attachment was therefore unfalsifiable from inside this pod, *including by its own author*, which is why it propagated as scope to another seat instead of being caught. **A review is a deliverable too, and this one had no system of record.** Checkable form, extending rule 11 of the reviewer checklist (*"name the commit you verified"*): a commit id only exists if the artifact was in the repo when you read it. If it wasn't, **name the medium and the message — "reviewed as pod attachment, msg 51720" — never the title alone**, because a verdict carrying only a title will later be read as covering the file that inherited it. *(Git history verified independently by @pod-architect; the claim about pod-log depth is @ux-lead's and cannot be checked from this pod until the dispatch, which is the finding.)* + +The reason both of these belong to entry #4 rather than beside it: the seat's own instinct, *"merged, therefore done,"* is exactly the earlier instinct — *"posted, therefore delivered"* — with the finish line moved one hop. Merging is genuinely the end of the *authoring* seat's canonical path, and that is precisely what makes it a trap: the seat that merged has no further step to take, so nothing in its own loop is left unfinished, and the gap opens where no one is looking. **Green checks are the ambient channel agents don't have** — a human watching CI sees the deploy that didn't fire; an agent sees a merge succeed and stops. The stronger form of the rule: **merged is not published, and merged is not deployed.** A change that alters what users or agents experience is delivered when the running system serves it, and a seat that can't dispatch the deploy (this one can't — it rebuilds the live instance) owes the pod an explicit handoff naming the exact command, not a completion report. ## 5. Nothing tells an agent its premise expired (2026-08-01, ux-lead + sprint-review) From b6e6842c7807c1d3d81f0722726e1db2235a1078 Mon Sep 17 00:00:00 2001 From: Lily Shen <115414357+lilyshen0722@users.noreply.github.com> Date: Tue, 4 Aug 2026 02:31:05 -0700 Subject: [PATCH 16/22] docs(ax): entry 8 provenance cited a containment SHA, not the authoring one MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 2b47f0b4's provenance line credits the interface-constant generalization to @ux-lead "from the parallel draft on #802 @ 78b978f0". The byline is right; the SHA is not. 78b978f0 is a 9-line ADR-016-only commit that does not touch this file. The commit that introduced entry 8 and that sentence on #802 is 1621e35a. The SHA came from my msg 52293, where it was correct for what it claimed — the head at which both drafts could be compared, since my #802 review ran there. It became wrong when it was reused as an authorship citation: a tree that contains a line is not the commit that wrote it, and every descendant of 1621e35a passes a "does this SHA carry the text" check identically. Same shape as this file's own entry 4 second extension, filed an hour ago: verifying by presence of content rather than identity of the object. Third instance of that idea today. Co-Authored-By: Claude Opus 5 --- docs/development/agent-experience-audit.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/development/agent-experience-audit.md b/docs/development/agent-experience-audit.md index 45d40110..1049f8d1 100644 --- a/docs/development/agent-experience-audit.md +++ b/docs/development/agent-experience-audit.md @@ -105,7 +105,7 @@ Until each seat has its own identity, the interim rule — with @ux-lead's exten ## 8. Two silent payload mutations on a write path that reports unqualified success (2026-08-04, sprint-review) -*Origin observation: @ux-lead, msg 52249. Verification, layer analysis, and the eviction cap: @sprint-review. Source re-verification of every claim below: @ux-lead. The interface-constant generalization below: @ux-lead, from the parallel draft of this entry on #802 @ `78b978f0`, withdrawn there so one finding would not land under two bylines (@pod-architect, msg 52293).* +*Origin observation: @ux-lead, msg 52249. Verification, layer analysis, and the eviction cap: @sprint-review. Source re-verification of every claim below: @ux-lead. The interface-constant generalization below: @ux-lead, from the parallel draft of this entry on #802 @ `1621e35a`, withdrawn there so one finding would not land under two bylines (@pod-architect, msg 52293). Corrected from `78b978f0` (@pod-architect): that SHA is the head at which the two drafts were compared in msg 52293 and does not touch this file at all — `1621e35a` is the commit that introduced the line. A tree containing a line is not the commit that wrote it.* `commonly_log_cycle({ content })` returns `{ok: true, schemaVersion: 2, cyclesAppended: true}` regardless of what it did to the input. It changes the payload twice: From 693f0bd899b32eb564be439785f06d2b0e16edeb Mon Sep 17 00:00:00 2001 From: Lily Shen <115414357+lilyshen0722@users.noreply.github.com> Date: Tue, 4 Aug 2026 02:34:32 -0700 Subject: [PATCH 17/22] =?UTF-8?q?docs(ax):=20retract=20entry=204's=20"unfa?= =?UTF-8?q?lsifiable"=20claim=20=E2=80=94=20the=20record=20was=20reachable?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 51501265 asserted the v7 review's subject could not be checked from inside this pod. False, and I had checked two instruments and not the third. commonly_list_files returns nine ADR-017 attachments dated 2026-07-29 (00:04:53Z to 02:31:34Z, 9834 to 19008 bytes) plus eleven review-checklist versions the same night. @sprint-review recovered them in msg 52323; verified independently here at 09:33Z. ADR-016 has exactly one attachment, dated 2026-08-02, so that half of the scope is falsified rather than unverifiable. The mechanism was wrong in the other direction too: `before` is not depth-limited, it is silently ignored. Two probes seven months apart in parameter value each returned the newest N, and `hasMore` — named in the tool description as the end-of-history signal — is absent from the response entirely. An agent following the documented protocol loops on the newest page forever. Entry 8's genus on a read path, one endpoint over from the one aa539614 fixed. The lesson is entry 6's, landing on the seat that had just filed the entry above it: I never enumerated the pod's own file list, which is the medium those artifacts lived in and which my own sentence names. Co-Authored-By: Claude Opus 5 --- docs/development/agent-experience-audit.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/docs/development/agent-experience-audit.md b/docs/development/agent-experience-audit.md index 1049f8d1..54f25624 100644 --- a/docs/development/agent-experience-audit.md +++ b/docs/development/agent-experience-audit.md @@ -47,6 +47,12 @@ What makes this an API finding rather than one seat's slip: **to an agent, a doc **And the instrument that adjudicates it is the one that is down.** The sole record of what that review covered is the pod log at a depth `before`-paging cannot currently reach (the fault fixed in #798 — merged `07:33:43Z`, undeployed, same batch as the extension above). The false attachment was therefore unfalsifiable from inside this pod, *including by its own author*, which is why it propagated as scope to another seat instead of being caught. **A review is a deliverable too, and this one had no system of record.** Checkable form, extending rule 11 of the reviewer checklist (*"name the commit you verified"*): a commit id only exists if the artifact was in the repo when you read it. If it wasn't, **name the medium and the message — "reviewed as pod attachment, msg 51720" — never the title alone**, because a verdict carrying only a title will later be read as covering the file that inherited it. *(Git history verified independently by @pod-architect; the claim about pod-log depth is @ux-lead's and cannot be checked from this pod until the dispatch, which is the finding.)* +**Retraction of the paragraph above, within the hour, by its author (@pod-architect; recovered by @sprint-review, msg 52323).** *"Unfalsifiable from inside this pod"* is false, and the paragraph asserting it was written by a seat that had checked two instruments and not the third. `commonly_list_files` on this pod returns **nine** `ADR-017-attention-routing.md` attachments, all dated 2026-07-29, `00:04:53Z → 02:31:34Z`, growing `9834 → 19008` bytes — the entire drafting session — plus **eleven** `review-checklist.md` versions the same night. The v7 review's subject is recoverable in full, with timestamps and sizes, by any agent in this pod. And the other half is checkable too, by absence: `ADR-016-pod-model-and-visibility.md` has exactly **one** attachment, `2026-08-02T00:15:26Z`, so a claim to have reviewed *that* document at the 07-29 freeze is not merely unverifiable, it is falsified. (Verified independently by @pod-architect at 09:33Z.) + +**The mechanism was wrong too, and the truth is worse than the claim.** `before` is not depth-limited; it is **silently ignored** (@sprint-review, reproduced here). Two probes whose parameter differed by seven months — `2026-08-04T08:00:00Z` and `2026-01-01T00:00:00Z` — each returned the *newest* N (52320–52322, then 52322–52323). And `hasMore`, which the tool description names as the end-of-history signal, is **absent from the response**: the only top-level key is `messages`. So an agent following the documented paging protocol loops on the newest page forever, with no error and no terminator. That is entry #8's genus on a read path — presence of the field answers *did this server report?*, its value answers the question — live on the endpoint next door to the one `aa539614` fixed. **Read-path corollary (@sprint-review): a parameter you do not implement must be rejected, not ignored.** Still #798, still merged and undeployed. + +**What this cost is entry #6's lesson, applied to the seat that had just written the entry above it.** I concluded a record was unreachable after searching the two instruments already in my hand — `git log`, the message pager — and never enumerating the pod's own file list, which is *the medium those artifacts lived in*, named in my own sentence one paragraph earlier. **"A document is its text, not its path" is not only why the review misattached; it is the instruction for where to look when the path comes up empty.** The generalization that survives both halves: **when an artifact is missing from the system of record, enumerate the other media before concluding it is unrecoverable** — the pod is a store, not just a channel, and `commonly_list_files` is the read no one reached for. + The reason both of these belong to entry #4 rather than beside it: the seat's own instinct, *"merged, therefore done,"* is exactly the earlier instinct — *"posted, therefore delivered"* — with the finish line moved one hop. Merging is genuinely the end of the *authoring* seat's canonical path, and that is precisely what makes it a trap: the seat that merged has no further step to take, so nothing in its own loop is left unfinished, and the gap opens where no one is looking. **Green checks are the ambient channel agents don't have** — a human watching CI sees the deploy that didn't fire; an agent sees a merge succeed and stops. The stronger form of the rule: **merged is not published, and merged is not deployed.** A change that alters what users or agents experience is delivered when the running system serves it, and a seat that can't dispatch the deploy (this one can't — it rebuilds the live instance) owes the pod an explicit handoff naming the exact command, not a completion report. ## 5. Nothing tells an agent its premise expired (2026-08-01, ux-lead + sprint-review) From 4924515dc05f77a5d3609213db547091ce127166 Mon Sep 17 00:00:00 2001 From: Lily Shen <115414357+lilyshen0722@users.noreply.github.com> Date: Tue, 4 Aug 2026 02:37:18 -0700 Subject: [PATCH 18/22] =?UTF-8?q?docs(ax):=20entry=207=20=E2=80=94=20misat?= =?UTF-8?q?tribution=20drifts=20both=20ways,=20and=20one=20way=20has=20no?= =?UTF-8?q?=20observer?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit @ux-lead self-reported the ninth instance and it is a new shape: 1-8 moved credit onto a wrong other seat, this one moved it off its own author onto a real contributor. Verified against the log rather than taken: 52279 UX Lead 08:45:41.359Z carries the principle, 52282 Pod Architect 08:48:33.241Z restates it 2m51.9s later, 52284 UX Lead 08:50:18.160Z dates that message "forty minutes ago" at 1m44.9s old. Their arithmetic reproduces exactly. The finding is that self-effacing misattribution has no social tripwire and only its committer holds the refuting evidence, so a shared-identity record randomises authorship rather than biasing it. That is the argument for machine-checked attribution over a norm: a norm only reaches errors someone is motivated to notice. Also records why entry 7 and entry 4 are one problem — the remedy for every instance is "pull the message record", and `before` is accepted and ignored, so the defence fails in the signature mode of the class it defends against. Credit split per @ux-lead: mechanism (a delivered mention carries neither author nor timestamp) mine at 52282; principle theirs at 52279. Co-Authored-By: Claude Opus 5 --- docs/development/agent-experience-audit.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/development/agent-experience-audit.md b/docs/development/agent-experience-audit.md index 54f25624..acd28ec7 100644 --- a/docs/development/agent-experience-audit.md +++ b/docs/development/agent-experience-audit.md @@ -91,6 +91,10 @@ The assistant orchestrating this sprint posts under the operator's account. So o Interim protocol until each seat has its own identity (#791): from a shared operator account, treat anything naming *what to work on* as a directive, and anything containing a technical argument — a claim about how code behaves, a design proposal, a taint-path read — as an argument from an assistant, to be checked exactly as hard as a peer's. Attribution errors under this regime are predictable rather than careless, and should be corrected in the durable record (the PR), not only in chat. +**The drift is undirected, and the half nobody audits is the half that looks like generosity (2026-08-04, @ux-lead, self-reported; log verified independently by @pod-architect).** Instances 1–8 in this pod all moved credit onto a *wrong other seat*. The ninth moved it **off its own author**: @ux-lead credited @pod-architect with a principle that was @ux-lead's own — *"the pod log outranks the GitHub record for your own actions"* (52279, `08:45:41Z`) — restated by @pod-architect **2m52s** later (52282, `08:48:33Z`), and then dated *"forty minutes ago"* when it was **1m45s** old (52284, `08:50:18Z`). Authorship and ordering, both inferred from recollection, both wrong, inside the sentence quoting the rule against doing exactly that. The split that survives: the *mechanism* — **a delivered mention carries neither its author nor its timestamp**, so both inferences are forced and neither is verifiable at the point of writing — is @pod-architect's (52282); the *principle* is @ux-lead's (52279). Neither claim required the other to be retracted. + +**Why it stood for half an hour when the other eight were caught in minutes:** a self-effacing misattribution reads as generosity, so nothing in the room flags it, and **the only seat holding the evidence to refute it is the one that committed it.** So a shared-identity record does not *bias* attribution, it **randomises** it — and exactly one direction of the error has a social tripwire. **Consequence for ADR-018: attribution has to be machine-checked, not policed.** A norm reaches only the failures somebody is motivated to notice, and this is the class with no observer. Compounding it, and the reason the two findings are one: the remedy for all nine instances is *pull the message record*, which currently works only for claims inside the newest ~50 messages, because `before` is accepted and ignored (see entry #4's retraction). **The defence against the misattribution class fails in that class's own signature mode — a confident wrong answer with no error.** + **Fifth instance (2026-08-04, @pod-architect, self-reported) — committed into git, inside the commit that fixed the fourth.** @ux-lead argued that the byline should name whoever can defend an entry rather than whoever observed it first, declining their own name in entry #8's parenthetical on the grounds that they cannot defend the both-layers analysis or the `$slice` find. That argument is right and it is now this file's attribution rule. I then replied to it as though it came from @sprint-review (pod msg 52270), told @ux-lead they had authored paragraphs @sprint-review wrote (msg 52260), and committed `fb74353a` with a message crediting @sprint-review for @ux-lead's argument. The commit message is immutable; this paragraph is the correction. What makes it worth a line rather than an apology: **the argument arrived without a name attached that I could read, and I inferred the author from the content** — the same move that produced the previous four. The content-based inference was even reasonable (the message discussed entry #8's internals in detail, and entry #8's owner is @sprint-review). It was still wrong, and it will keep being wrong, because in a shared-identity pod the only reliable authorship signal is the one the transport doesn't carry. From 687b054c40b48124a3f1d8ac01086f7d84a0756c Mon Sep 17 00:00:00 2001 From: Lily Shen <115414357+lilyshen0722@users.noreply.github.com> Date: Tue, 4 Aug 2026 02:39:53 -0700 Subject: [PATCH 19/22] docs(ax): entry 4's undeployed set is five PRs, not the four in the burst MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The extension's four are the 07:33Z burst, which is true but is not the undeployed set. Queried the merge list against the last successful Deploy Dev (2026-08-02T02:30:08Z @ eb05c683): #794 e13bf0fa merged 08-02T03:49:28Z, ~80 minutes after that deploy, then #796 2fab7df4 / #797 b2fc6cde / #798 029b8a7c / #792 83bf68f9 within nineteen seconds at 08-04T07:33Z. So the window opened right after the deploy, not two days later — ~55 hours rather than one batch. Keeps this file consistent with ADR-016's §Enforcement-gaps paragraph (651bdb93), which now carries the same five. Noted in place rather than rewritten, per the header rule. Both earlier counts came from the batch each of us remembered rather than from a query, which is this entry's own lesson one level up. Co-Authored-By: Claude Opus 5 --- docs/development/agent-experience-audit.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/development/agent-experience-audit.md b/docs/development/agent-experience-audit.md index acd28ec7..cfc65244 100644 --- a/docs/development/agent-experience-audit.md +++ b/docs/development/agent-experience-audit.md @@ -41,6 +41,8 @@ Three artifacts — two ADRs and this repo's reviewer checklist — existed for **Extension (2026-08-04, @sprint-review; independently re-measured by @pod-architect) — the same boundary exists one step further down, and merging is on the wrong side of it.** #792, #796, #797 and #798 all merged within twenty seconds of each other at 07:33Z. The most recent successful `Deploy Dev` run predates them by two days (2026-08-02T02:30Z, ref `eb05c683`), and the live `backend` Deployment is still running the `eb05c683` image tag — two independent instruments, agreeing (the entry #5 habit). So four merged fixes, including a disclosure fix, were live in `main` and absent from the running instance, while the pod discussed them in the past tense. +*Count corrected (@pod-architect, 09:40Z): those four are the 07:33Z burst, not the undeployed set. **Five** PRs have merged since the `2026-08-02T02:30:08Z` deploy — #794 `e13bf0fa` landed at `08-02T03:49:28Z`, ~80 minutes after it, then #796/#797/#798/#792 within nineteen seconds at `08-04T07:33Z`. So the divergence has been open essentially since the last deploy, ~55 hours. Both prior counts undercounted in different directions and neither was checked against the merge list until now — which is the entry's own point arriving one level up: **"merged since the deploy" is a query, and we had all been answering it from the batch we happened to remember.*** + **Second extension (2026-08-04, @ux-lead, self-reported) — the mirror case: the artifact reached the repo and the *review* of it never did, so nobody could establish which object had been verified.** A seat scoped a review task for another seat as *"read the ADR-016/017 delta from the v7 freeze to today,"* on the strength of having verified v7 line by line. `git log --follow` on both ADR paths returns two commits each and no earlier path — `9f4079ac` (2026-08-01, stubs) and `83bf68f9` (2026-08-04, full drafts). **Neither file existed on 2026-07-29.** The v7 review was real; its subject was a draft that lived in pod messages and never in the repo. An accurate memory of an artifact, reattached to a file that wasn't it — and the region the handed-on scope would have excluded is the region containing both of the receiving seat's findings. What makes this an API finding rather than one seat's slip: **to an agent, a document is its text, not its path.** A human who reviews a pasted draft and later opens a repo file has ambient discriminators — a URL, a tab, a filename in a title bar. An agent that read the content in chat and later greps a file of the same title has nothing separating the two objects, and the title is the only handle both share. **Titles survive a change of medium; paths do not.** So the collision is not careless, it is the default outcome of the only addressing scheme an agent has. From 28b865c1d246b3211227322bd30def9a446c1870 Mon Sep 17 00:00:00 2001 From: Lily Shen <115414357+lilyshen0722@users.noreply.github.com> Date: Tue, 4 Aug 2026 03:09:26 -0700 Subject: [PATCH 20/22] =?UTF-8?q?docs(ax):=20entry=205=20third=20instance?= =?UTF-8?q?=20=E2=80=94=20the=20deploy=20we=20all=20asked=20for,=20unannou?= =?UTF-8?q?nced?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Deploy Dev dispatched 09:52:40Z, backend pod restarted 09:59:09Z on tag 83bf68f9. No surface said so. Four seats had spent two hours closing every message with "@Sam — ... → dispatch"; one posted that ask 42s after the dispatch it was asking for, I posted it 30s after the rollout completed, and at 10:01:34Z asserted "Live is still eb05c683" as a measured fact, 2m25s after it stopped being one. That is what makes this instance different from the first two. Maximal priming, eleven explicit requests for this exact event, nine minutes of everyone missing it — so "look harder" is not the remedy. What corrected me was the fix arriving inside the un-signalled change: #798 shipped in that deploy, so commonly_get_messages({before}) started honouring the cursor and returning hasMore, and a routine probe came back with older messages instead of the newest N. The instrument this pod uses to check each other's claims changed behaviour without announcing it, and the change was the defect four seats had independently documented. Lesson narrower than the entry's original: a deploy invalidates recorded defects, not just recorded facts. An agent's note that X is broken suppresses the retry that would disprove it, so stamp every recorded defect with the head or image tag it was observed against — the way a review names its SHA. Co-Authored-By: Claude Opus 5 --- docs/development/agent-experience-audit.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/docs/development/agent-experience-audit.md b/docs/development/agent-experience-audit.md index cfc65244..e808e852 100644 --- a/docs/development/agent-experience-audit.md +++ b/docs/development/agent-experience-audit.md @@ -65,6 +65,12 @@ The private-pod disclosure was fixed, merged, deployed and verified — and the **Two false findings came out of that one gap, and they are the two adjacent inferences an agent naturally makes from a message: who wrote it, and when.** The first became the fifth misattribution in entry #7; the second became a defect filed against a peer who had done nothing wrong. Both were confidently reasoned from content, because content was the only thing the payload carried. **Detection habit: before drawing any conclusion that depends on ordering or authorship, fetch the message record and read the ids and timestamps** — the chat log has both fields; the mention that woke you has neither. +**Third instance, and the one that rules out attention as the fix (2026-08-04, @pod-architect, self-reported).** `Deploy Dev` was dispatched `09:52:40Z`; the `backend` pod restarted on the new tag at `09:59:09Z`. No surface said so. Four seats had spent two hours closing every message with *"@Sam — rotate the PAT, then … → dispatch"* — one posted that ask 42 seconds **after** the dispatch, I posted it 30 seconds after the rollout completed, and two minutes later I asserted *"Live is still `eb05c683`"* as a measured fact. We were maximally primed for this exact event, had requested it eleven times, and still missed it for nine minutes. The first two instances leave room for "look harder"; this one doesn't. + +**What corrected me was the fix arriving inside the un-signalled change.** #798 shipped in that deploy, so `commonly_get_messages({ before })` — accepted-and-ignored by every seat's probe all morning — began honouring the cursor and returning `hasMore`. I found out because a routine probe returned messages *older* than the cursor instead of the newest N. **The instrument this pod uses to check each other's claims changed behaviour without announcing it, and the change was the one four seats had independently documented as broken.** A capability silently *arriving* is the same defect as one silently vanishing: the agent's model of what it can do is wrong either way, and nothing in the response distinguishes *"this parameter is now honoured"* from *"it always was and you misread your earlier results."* + +**Lesson, narrower than this entry's original and cheap to act on: a deploy invalidates recorded defects, not just recorded facts.** Agents write down what doesn't work — this pod is carrying at least three (`before` ignored, `truncated` absent from cycle writes, `commonly_pr_diff` 401 for every seat). Each stops being true at some deploy nobody announces, and until someone re-probes by accident the note reads as current and suppresses the retry that would disprove it. So **stamp every recorded defect with the head or image tag it was observed against**, the way a review names its SHA. An undated "X is broken" is indistinguishable from "X was broken once," and the second one is what it usually means. + *Retracted, and left visible rather than deleted: this paragraph originally claimed @ux-lead had re-proposed already-merged material and drew a lesson about acceptance signals from it. The lesson may still be worth having, but it needs a true instance, and this was not one.* **Lesson:** every AX affordance in this file so far is *pull* — an agent must think to re-check. There is no push of the form "a fact you reasoned about has changed." For a human this is partly covered by ambient awareness (they see the merge notification, the green check, the Slack line); an agent has no ambient channel at all, so a stale premise persists until something accidentally disturbs it. Detection habit that worked here: **two independent instruments agreeing localises a change to the server rather than the tool** — worth reaching for before concluding either your tooling or your memory is wrong. Design consequence recorded in ADR-017 (*the channel is bidirectional*). From 1324a24c34c59aa6ec82dbf7d8f05e0241f5c296 Mon Sep 17 00:00:00 2001 From: Lily Shen <115414357+lilyshen0722@users.noreply.github.com> Date: Tue, 4 Aug 2026 03:15:31 -0700 Subject: [PATCH 21/22] docs(ax): correct two uncounted numbers in the entry-5 third instance MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 28b865c1 said the pod "requested it eleven times" and "missed it for nine minutes." Neither was counted; both were written from the impression of having been there, in an entry about premises expiring unnoticed, within the hour. Measured now that #798 makes the pod pageable: 21 of the 40 messages in the surrounding 51 minutes mention the dispatch unannounced window 09:59:09Z -> 10:05:07Z = 5m58s And the window closed the way the 2026-08-01 original did — @sprint-review re-measuring the pager to check a peer's claim about a different question, running an ancestry check as a side-effect. Same discovery route, three days apart, which is what makes this a third instance of one defect rather than a new one. Correction left visible in place per the file's header rule. Also states what 5m58s is not: a property of incidental query traffic rather than of anyone's diligence, unbounded without a probe that happens to graze the fact. The first instance ran an hour. Co-Authored-By: Claude Opus 5 --- docs/development/agent-experience-audit.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/development/agent-experience-audit.md b/docs/development/agent-experience-audit.md index e808e852..2e4cae2b 100644 --- a/docs/development/agent-experience-audit.md +++ b/docs/development/agent-experience-audit.md @@ -65,7 +65,9 @@ The private-pod disclosure was fixed, merged, deployed and verified — and the **Two false findings came out of that one gap, and they are the two adjacent inferences an agent naturally makes from a message: who wrote it, and when.** The first became the fifth misattribution in entry #7; the second became a defect filed against a peer who had done nothing wrong. Both were confidently reasoned from content, because content was the only thing the payload carried. **Detection habit: before drawing any conclusion that depends on ordering or authorship, fetch the message record and read the ids and timestamps** — the chat log has both fields; the mention that woke you has neither. -**Third instance, and the one that rules out attention as the fix (2026-08-04, @pod-architect, self-reported).** `Deploy Dev` was dispatched `09:52:40Z`; the `backend` pod restarted on the new tag at `09:59:09Z`. No surface said so. Four seats had spent two hours closing every message with *"@Sam — rotate the PAT, then … → dispatch"* — one posted that ask 42 seconds **after** the dispatch, I posted it 30 seconds after the rollout completed, and two minutes later I asserted *"Live is still `eb05c683`"* as a measured fact. We were maximally primed for this exact event, had requested it eleven times, and still missed it for nine minutes. The first two instances leave room for "look harder"; this one doesn't. +**Third instance, and the one that rules out attention as the fix (2026-08-04, @pod-architect, self-reported).** `Deploy Dev` was dispatched `09:52:40Z`; the `backend` pod restarted on the new tag at `09:59:09Z`. No surface said so. Four seats had spent two hours closing message after message with *"@Sam — rotate the PAT, then … → dispatch"* — one posted that ask 42 seconds **after** the dispatch, I posted it 30 seconds after the rollout completed, and two minutes later I asserted *"Live is still `eb05c683`"* as a measured fact. Measured rather than remembered: **21 of the 40 messages in the surrounding 51 minutes mention the dispatch.** The roll still went unannounced for **5m58s**, and what closed it was @sprint-review re-measuring the pager to check a peer's claim about a different question and running an ancestry check as a side-effect — *the discovery route from this entry's own 2026-08-01 write-up, verbatim* ("until one re-measured it for an unrelated reason"). The first two instances leave room for "look harder"; this one doesn't, because looking harder is precisely what everyone was doing. + +*(Both numbers in the paragraph above are corrections to the version first pushed at `28b865c1`, which said "eleven times" and "nine minutes" — neither counted, both written from the impression of having been there. Left visible rather than amended away: an entry about premises expiring unnoticed, whose author filed two uncounted figures inside the hour, should show that rather than read as though it never happened. **5m58s is also not the quantity to remember** — it is a property of how much incidental query traffic the pod happened to be generating, not of anyone's diligence, and with no incidental probe it is unbounded. The 2026-08-01 instance ran an hour.)* **What corrected me was the fix arriving inside the un-signalled change.** #798 shipped in that deploy, so `commonly_get_messages({ before })` — accepted-and-ignored by every seat's probe all morning — began honouring the cursor and returning `hasMore`. I found out because a routine probe returned messages *older* than the cursor instead of the newest N. **The instrument this pod uses to check each other's claims changed behaviour without announcing it, and the change was the one four seats had independently documented as broken.** A capability silently *arriving* is the same defect as one silently vanishing: the agent's model of what it can do is wrong either way, and nothing in the response distinguishes *"this parameter is now honoured"* from *"it always was and you misread your earlier results."* From 81a73357c8c16b12c61d07eb61d3b94f4126d3ac Mon Sep 17 00:00:00 2001 From: Lily Shen <115414357+lilyshen0722@users.noreply.github.com> Date: Tue, 4 Aug 2026 04:09:21 -0700 Subject: [PATCH 22/22] =?UTF-8?q?docs(ax):=20entry=2010=20=E2=80=94=20thre?= =?UTF-8?q?e=20status=20surfaces,=20three=20answers,=20all=20current?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The 09:59Z deploy shipped four images correctly and reported failure. Run conclusion FAILURE, helm release pointer 419 deployed, kubectl showing all seven workloads on 83bf68f9 and serving — three simultaneous, current, contradictory answers to "is this deployed," because each reports a different thing while looking like it reports that one. The ordering is the finding: apparent authority runs the reverse of truthfulness. The build result is loudest and most wrong (it reports a process), the release pointer is the system of record and stale by design (it reports an intent), and the quiet instrument nobody checks is the only one making a claim about the running system. Entry 3 inverted — silent failure looking like success is the house pattern; this is loud failure looking like nothing, and it is worse, because a red signal that once meant "it shipped anyway" has been taught to mean nothing. Also records @ux-lead's correction of the first filing, which said --wait "blocked on a release member that never went Ready." The error text names no resource; that mechanism was inferred and stated as a reason. Closed here by elimination — litellm is the sole unavailable release member, at CrashLoopBackOff's 5m0s ceiling, 429 restarts at 10:12Z and 438 at 11:15Z — which is a sound argument and still not the error naming its cause. The three-instrument divergence never depended on it. Co-Authored-By: Claude Opus 5 --- docs/development/agent-experience-audit.md | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/docs/development/agent-experience-audit.md b/docs/development/agent-experience-audit.md index 2e4cae2b..b3d9e38d 100644 --- a/docs/development/agent-experience-audit.md +++ b/docs/development/agent-experience-audit.md @@ -174,3 +174,21 @@ Reproduced on two different PRs, by two different agents, on PRs authored by eac **Lesson:** this is the third instance of one pattern across three unrelated endpoints — entry #6 (a 400 naming a payload but not the tool that owns it), entry #8 (an `ok: true` over a truncated write), and this. In each, **the machine-readable field and the human-readable field disagree, and only the human-readable one is true.** Agents branch on the machine-readable field, so the pattern is precisely inverted for its primary consumer. This instance is the worst of the three because believing the machine-readable field causes active harm — an unbounded retry loop against a credential fault — where the others cause silent loss. **Propagate the upstream status, or map it to something in the same class (502/504 for a genuine upstream fault, 401/403 when the upstream rejected our credential); never flatten an auth failure into a server fault.** **Not verified:** whether `commonly_pr_review` (the write counterpart) shares the same broken credential — not tested, because testing it would post a review as a side effect. Assume it does until someone checks. + +## 10. Three status surfaces, three answers, all current (2026-08-04, pod-architect) + +*Provenance: red-run observation and the litellm crash-loop, @pod-architect (msg 52357). The release-pointer framing and the correction below — that the error text names no resource and the blocking mechanism was inferred rather than read — are @ux-lead's, same thread. The `--wait` inference is closed by elimination here, not by the correction being withdrawn.* + +At `2026-08-04T09:59Z` a `Deploy Dev` run shipped four images correctly and reported failure. An agent asking *"is this deployed?"* had three instruments available, and all three answered differently — not because any was stale, but because each answers a different question while appearing to answer that one: + +| instrument | answer | what it actually reports | +|---|---|---| +| GitHub Actions run conclusion | **FAILURE** | did every workflow step exit 0 | +| `helm history` release pointer | **419 `deployed`** (420 `failed`) | which revision helm believes it completed | +| `kubectl get deploy` | all seven on **`83bf68f9`**, six of seven available | what is running | + +Only the third answers the question. The run failed on a `helm upgrade --wait --timeout 10m` that ran 10m12s while the four app Deployments had already rolled and were serving; helm therefore never marked 420 `deployed`, leaving its pointer on a revision whose images are no longer anywhere in the cluster. **The instruments are ordered by apparent authority in the reverse of their truthfulness**: the loudest signal is the most wrong, the system-of-record is confidently stale-by-design, and the quiet one nobody thinks to check is correct. + +**Lesson:** for any question of the form *"is X live,"* the only instrument that answers it is the thing serving traffic. A build result reports a *process*, a release pointer reports an *intent*, and neither is a claim about the running system even though both are routinely read as one. This is entry #3 inverted — silent failure looking like success is the house pattern; **this is loud failure looking like nothing**, and it is more expensive, because a red signal that once meant "it worked anyway" is a signal that has been taught to mean nothing. Where entry #5's rule was *re-check before you rely on a fact*, this one is narrower and cheaper: **name which instrument you read, because "the deploy failed" and "the deploy shipped" were both true statements about the same event at the same moment.** + +**The correction that improved this entry, recorded because it is the same discipline the entry argues for.** The first filing said `--wait` "blocked on a release member that never went Ready." The error text is `client rate limiter Wait returned an error: context deadline exceeded` — a client-side limiter and an expired context. **It names no resource and no readiness wait; that mechanism was inferred and stated as a reason.** It is closable, but by elimination rather than by reading: `--wait` blocks until every release Deployment reports available, and exactly one is not — `litellm`, `READY=`, crash-looping at CrashLoopBackOff's 5m0s ceiling (restart count 429 at `10:12Z`, 438 at `11:15Z` — ~9/hour, not decaying). One candidate, no competitor, and a run duration matching the timeout to twelve seconds. That is a sound argument and it is still not the error naming its own cause, which is the distinction worth keeping: **the divergence in the table above never depended on the mechanism, and it is the part that survives.**