From dea0c3a0c011ee93454a8b85d7f00490de10806f Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 2 Sep 2026 06:13:31 +0000 Subject: [PATCH] docs(agents): record how GitHub mangles agent-written bodies, and the per-query search control Two clauses in the instrument-discipline area of AGENTS.md: - a channel-level control rule: the control belongs on the CHANNEL, not only on the query, and it is run per query. A non-empty result is self-validating; an empty one always needs a known-must-hit control. The zero-quota web payload channel is recorded as the measured fallback under the same rule. - a new section enumerating the six measured ways GitHub rewrites an issue/PR body after it is written, the four mitigations measured working, and a pointer to the authoritative objectstack wording (carried verbatim only for the read-back-before-repair caveat, which is load-bearing). Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01BGMDbrVa8JjZcCQ7DWYH1b --- AGENTS.md | 46 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/AGENTS.md b/AGENTS.md index 2574ebcf24..61117a5d3d 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -398,6 +398,52 @@ ls /* | wc -l # 两个数不等 ⇒ 工 于钩子。有意例外:`OS_ALLOW_TREE_ENUM=1`。改这个钩子?重跑 `.claude/hooks/guard-tree-enum.selftest.sh`。 +#### ⭐ 对照挂在**通道**上,不只挂在查询上 —— 而且**每次查询**都要跑一遍 + +上面两条写的都是**查询**级的对照(总体 vs 逐项、对照必须有能力失败)。还差一层:**通道本身也会静默 +失灵,而且是间歇性的 —— 间歇比全坏更危险,因为第一次拿到命中的席位会学会信任它。** + +实测(objectui#7185,同一容器、同一小时、两条 lane):MCP `search_issues` 对一个 `issue_read` 能直接 +读出来的 issue 返回 `total_count: 0`;而另一条 lane 用一个**短关键词**查询在同一个工具上拿到 **3 条 +命中**。⇒ ⛔ **不是通道不可用** —— 死通道不会返回 3 条。失灵的是那一次查询(失败的那条是**长的、近 +乎逐字的标题**,可用的那条是短关键词)。所以规则不是「别用 search」,而是: + +- **非空结果自我验证,不需要对照;空结果永远需要一个「已知必中」的对照** —— 例如某个你能用 `issue_read` 直接读到的 issue 的近逐字标题。⛔ **没跑对照的空结果不是一次读数**:它不携带任何信息,而它渲染出来恰好就是去重步骤想要的那个答案(「没有重复」),与真负例**不可区分**,不做对照就**不可证伪**。失败形态不是「报错然后重试」,是「立了一张重复卡、没人纠正、下一个席位再立一次」。 +- **每次查询都跑,不是每个会话跑一次。**「这个通道十分钟前还好好的」不是关于你眼前这次查询的证据。 +- **兜底通道**:零配额的 GitHub 网页 payload 通道实测可用(同一次去重里返回 8 个 issue 号,含 `search_issues` 看不见的那个),**它要跑同一条对照** —— 它只是「某一天、某一个容器里被测过可用」,不是永久答案。 +- ⚠️ **未解,别当已答问题用**:`search_issues` 为何对一个直读得到的 issue 返回 0 —— 索引延迟,还是读路径与搜索路径的 scope / 查询形状差异?两者的补救完全不同(前者自愈,后者会永久地、静默地收窄每一次去重),本仓尚未诊断。 + +### ⚠️ GitHub 会改写你写进 issue/PR 正文的字节 —— 每次发布后回读 + +**六种已实测的改写,共享同一个失败模式:正文在写入之后被静默改变,而且不回读就看不见** +(objectui#6970、#6452;多个 agent 在同一个工作会话里各自独立撞到,重复发现率本身就是把它写下来的 +理由)。 + +- **① tag 形状的片段在保存时被删掉** —— 反引号和围栏代码块**都不保护**。实测:一张 `.d.ts` 的 before/after 对照表,两列的泛型参数被吃掉后**双双塌成同一个字符串**,于是一张专为展示类型变化而写的表,渲染出来正好读作「什么都没变」。⚠️ 单独占据第一行的 HTML 注释标记同样被吃掉 —— 首行位置不提供任何保护,靠标记扫描找报告的机制会因此完全看不见那条评论。 +- **② `PATCH` 把 session-URL 形式的 attribution footer 降级为 bare 形式**,丢掉 session 引用。 +- **③ `PATCH` 无条件追加第二个 footer** —— 哪怕提交的正文已经以一个 footer 结尾。逐字节回读实测:存储的正文比发出的多**恰好 58 字节**,unified diff 只有那几行追加的 footer、没有内容被吃 ⇒ 与 ①② 都不同,这一种是**加**,不是**减**。 +- **④ `issue_write` 创建 issue 时,attribution footer 被整块删除**(不是降级,是消失)。⭐ 定位方式值得单独记住,因为它把「删除」和「截断」分开了:发出的是 body + footer + 尾部 sentinel,回读结果是 **sentinel 在、footer 不在** ⇒ 被删的是**中间**那一块,是针对 attribution 的定向剥离。⚠️ 没有这个 sentinel 对照项,唯一能得出的结论是「正文被截断了」,那会把人指向完全错误的方向(比如去查长度上限)。 +- **⑤ `create_pull_request` 把 bare footer 归一化为 session-URL 形式** —— 与 ② 恰好**反向**。⇒ ⭐ 两条合起来才是完整后果:**一个 PR 正文只要被创建之后再编辑一次,就会丢掉 session 引用**,单看任一条都看不出来。⚠️ 而且那个 session id 是**席位**的,不是具体那次实现的 —— 任何想靠它回溯「是哪个 agent 做的这次改动」的机制,只会拿到席位粒度的答案,比看上去弱一档。 +- **⑥ ⛔ 关单关键词解析器无视否定 —— 这一种最危险。** 解析器扫的是 `close|closes|closed|fix|fixes|fixed|resolve|resolves|resolved` 紧跟一个 issue 引用,**它不解析句子**:一句「本 PR 并不关闭某卡」若按英文否定式写成 does-NOT-close 加卡号,合并时照样把那张卡关掉。⚠️ 前五种在检视时**看得见**,这一种看不见:损害是**延迟**的(合并那一刻才发生,可能是几天后的另一个会话),而且**反转作者的明确意图** —— 那句话之所以存在,正因为作者在小心行事;卡被静默关掉后不再出现在任何队列普查里,于是它在跟踪的后续不是「可见地被阻塞」,而是直接丢失。安全写法:`Part of #`、`Refs: #`、`Related: #`、裸 `objectui#`(不带关键词)。 + +**实测有效的缓解,四条 —— 合起来才够,单独任何一条都不够:** + +- **泛型和占位符写成大写单词**(例如 "OPT of `z.ZodNever`"),⛔ 永不字面写成尖括号形状,并在正文里说明为什么这么写。 +- **session URL 写进正文散文、并作为反引号代码跨度** —— 实测原样穿过 `PATCH`;⛔ 别指望 markdown 链接形式的 footer 活下来。 +- **issue 正文里的 attribution 写成散文**,⛔ 别依赖 footer 块(见 ④)。 +- **每次发布之后回读到正文末尾,并数一次尖括号命中**;开 PR 之前额外扫一遍自己的正文 —— `grep -nEi '(clos|fix|resolv)' `,确认每一个靠近 `#` 的命中都是你**有意**要关的那张卡。 + +**占位符不是特例:普通散文和围栏代码块里的占位符同样被吃掉** —— 实测一行命令配方里的两个占位符双 +双塌成一个裸 `-`,而那一行存在的意义正是给出那两个占位符。权威措辞在 `../objectstack` 的 `AGENTS.md` +(「GitHub mutates body BYTES — spell poison-shaped tokens out in words, never literally」那一条), +⛔ 本仓不复制它的正文,免得两处漂移;这里只留指针,并**逐字**保留其中最要紧、也最容易被漏掉的那半 +句: + +> A body reading short only through the API is probably intact — check the rendered page before "repairing" it; a rewrite destroys a correct card. + +⇒ ⭐ **回读发现正文「变短」时,先看渲染后的页面,再决定要不要修。** 一次不必要的重写会毁掉一张本 +来正确的卡 —— 而在受管面上,那是不可恢复的。 + ### ⛔ 受管面(governed surface):agent 起草,人类合并 维护者裁决(2026-08-18),**原文照录、不翻译** —— 提问明确点名了本仓: