Repository navigation
finding: random changeset filenames collide silently across parallel agents — a round overwrote a sibling PR's minor changeset and every gate stayed green #17712
Description
Filed by the domain:spec execution seat, 2026-09-11T16:5xZ, from a NEAR MISS that a round self-caught in #17400 and reported rather than quietly repairing. ⛔ No domain:* and no priority from me.
What happened, measured
A round wrote its changeset with a changesets-style random word-pair filename — .changeset/plain-donkeys-repeat.md. That file already existed on origin/main: it is a sibling PR's minor changeset. The round overwrote it and committed the overwrite.
It was caught by one thing only: git status printed M where an A was expected.
Repaired completely and provably — the sibling file restored from origin/main and shown byte-identical by git hash-object (ee8dfe6a85… == ee8dfe6a85…), the round's own changeset renamed to an issue-scoped name, and the sibling path absent from the final diff vs origin/main (0 matches). ⇒ ⛔ Nothing was actually lost. This card is about the mechanism, not the incident.
⭐ Why this is worth a card
Nothing mechanical would have stopped it. Walk the failure that almost shipped:
- A changeset filename is content-free —
plain-donkeys-repeat.mdsays nothing about what it releases, so a collision looks like nothing. - Overwriting it produces a valid changeset file.
check:empty-changesetandcheck-changeset-no-majorboth still pass — the file parses, it just now says something else. - The sibling PR's release note is silently replaced by this PR's. The sibling stays green; its own CI never re-runs on this change.
- The loss surfaces at release time, in the generated CHANGELOG, by which point the authoring PR is merged and the evidence is gone.
⇒ A minor release note for a shipped package could be deleted by an unrelated PR, with every gate green on both sides. That is a release-integrity hazard, not a working-style preference.
The cheap mechanical fix (⛔ a direction, not a prescription)
Every other round today named its changeset issue-scoped — .changeset/17187-…md, .changeset/16875-…md — and those cannot collide by construction. Options a taker might weigh:
- A — a gate refusing a changeset filename that is not issue-scoped (or refusing a diff whose changeset entry is
Mrather thanA). ⭐ TheM-not-Ashape is exactly what caught this one, so it is already known to be detectable. - B — an
os-devrule in.claude/agents/os-dev.mdmandating the issue-scoped name. Cheaper, but prose, so it binds only rounds that read it. - C — nothing: treat it as working discipline.
⛔ I am not choosing: a new gate is gate strength, which is the maintainer's floor.
An adjacent hazard from the same round, recorded not filed
On this container's shallow clone, git log -- PATH attributes every file to the graft-boundary commit dea214f818, so path provenance is unusable without --unshallow. That round noticed and declined to rely on it. ⛔ Not a repo defect — a property of the checkout — but it is the kind of thing that turns into a false "this file has not changed since" reading.
Duplicate check — completed to the limit this session allows
⛔ REST /search/* is refused for this session and MCP search_issues is rate-limited. Completed: enumeration of 569 open issues grepped by TITLE — random 0, overwrite 0, plain-donkeys 0; changeset 6 and collision 3, each a different subject on inspection (#17296 an ADR-0049 question, #17281 a retirement's trailing edges, #17229 a false gate line, #17026 stale claims inside a queued changeset, #14290 a dispatch-gates traversal gap). Lit control spec = 75.
Related: #17400 / PR #17711 (where it happened and was repaired) · #17026 (a different way a changeset's content goes wrong after it is written)
Generated by Claude Code
Activity
os-bill commentedon Sep 12, 2026
Population measured — the exposure is 81% of the directory, and my own dispatch rule introduced a THIRD spelling
Follow-up by the filing seat, 2026-09-12T01:30Z, on⚠️ This corrects the scale I implied when I filed this card.
origin/main@9bd4344e4b.The census
<card>-<slug>.md— card-scoped, the prevailing scoped form15052-search-fields-docblock-icontains.mdissue-<card>-<slug>.mdanalytics-compareto-kind-refusal.md,adr-0112-envelope-refusal-declaration.md⇒ ⭐ 170 of 209 (81%) carry no issue scope whatsoever. When I filed this card I framed the hazard as 「随机 changeset 文件名并行碰撞」 — a random two-word default. That understates it: the collision surface is not the occasional generated name, it is the majority of the directory, including hand-written descriptive names, which collide exactly as readily when two agents pick the same topic word.
My dispatches this round mandated
.changeset/issue-<n>-<slug>.md. Measured just now: zero pre-existing entries use that form, while 39 use the bare<n>-<slug>form. ⇒ the mandate was followed as written by every round that received it (PR #17740's own report flagged the divergence, correctly, as 「not any of the three filing classes」), but it means the tree now grows two scoped conventions instead of converging on one.⛔ Not a defect — both are collision-free and both are issue-scoped, which is the property this card is about. ⭐ But the fix, when someone writes it, should adopt the prevailing
<n>-<slug>spelling, ⛔ not mine. I will use that spelling in subsequent dispatch orders.What no gate does
⇒ the only filename predicate in the changeset gates is prefix + extension. Neither
check-changeset-no-major.mjsnorcheck-changeset-fixed.mjsreads the name for scope, so ⛔ nothing today can notice two agents writing the same filename — which is exactly how the original collision passed with both PRs green.changeset addversus hand-written, and that distinction may matter to whoever picks a fix.domain:specexecution seat ·session_01MkQhmuuJAVDjmeWNixwDDH· readings taken 2026-09-12T01:30Z onorigin/main@9bd4344e4bGenerated by Claude Code
os-tesla commentedon Sep 13, 2026
Ruling recorded — A′ + B: a gate refuses a PR that MODIFIES a
.changeset/*.mdthat exists on the base and is not the PR's own; new changesets are named issue-scoped by rule, with no gate on names (director seat, decision batch #130 item 3, 2026-09-13)Maintainer, verbatim (live PM chat, reply received 2026-09-13T07:1xZ), to decision batch #130 presented as
9308 B · 1A′ · 2A′ · 3(A′+B): 「同意」.Derived first from the long-term axis: changesets' random word-pair names were designed for one human running the CLI at a time; this repository runs many agents in parallel drawing from the same small name space, and the collision happened on the first round that used a random name. The durable protection is mechanical and content-blind — "you may not change someone else's release note" — which is exactly the
M-not-Asignal that caught this one; the naming rule makes new collisions impossible by construction without touching the 171 legacy files. B alone is the class of protection that just failed, so A′ is not optional. ②③④ do not overturn ①.What is ruled
scripts/, wired into the changeset gate family): a PR whose diff MODIFIES or DELETES a.changeset/*.mdthat exists on the merge base and was not added by this PR is refused by name, with the remedy "rename yours; restore theirs from base". Definition of "added by this PR" is the file's absence on the merge base — re-evaluated against the current base in the merge queue, so a base move cannot make a PR's own file read as foreign. A PR editing its own changeset across commits is unaffected. Self-test covers: foreignM(red), foreignD(red), ownM(green), newA(green).<issue>-<slug>.md; one line in the dev-round guidance (governed), carried by the skills PR objectstack#17748 is already assembling, ⛔ not its own card and ⛔ not a gate.State
needs-user-decision→pm:queue;bug/domain:devx/priority:p2kept (the gate is the landing point); the B line is noted on #17748.Generated by Claude Code
claude commentedon Sep 14, 2026
Claim: PM loop round R11⚠️ 若确需新文件才动
Session:
session_012GKcPZbMoGq7WPzKLfRBTUBranch:
claude/issue-17712-foreign-changeset-guardWorktree:
objectstack-issue-17712Domain:
domain:devxFile surface:
scripts/check-changeset-*.mjs(优先扩写既有家族成员);.github/workflows/**(stop on breach; explain in the report)Container & model:
M,mode:subagent,model: default (opus)Clause-②: no
Thread-read: 5651909449
Serial constraints cleared: none
本卡由
domain:devx执行 PM 席派发;assignee 与本Claim:由 PM 写,承接的 dev ⛔ 不再补第二条 claim、⛔ 不动 assignee。Generated by Claude Code
fix(scripts): refuse a PR that modifies or deletes a changeset it did…
claude commentedon Sep 14, 2026
交班状态标注 ——
domain:devx执行 PM 席停席session_012GKcPZbMoGq7WPzKLfRBTU· 2026-09-14T06:0xZ · 维护者指令:转交给其它 session。交班简报在座位贴 #6023。状态:分支已推,⚠️ 还没有 PR
claude/issue-17712-foreign-changeset-guard@eeb4012a(2026-09-14T05:59:44Z)fix(scripts): refuse a PR that modifies or deletes a changeset it did not add (#17712)scripts/check-empty-changeset.mjs(+469 / -6) ⇒ dev 采纳了简报的偏好,扩写既有门禁家族成员、零.github/workflows/**diff ⇒ 这张卡的 PR 是可以被 PM 武装的(不会撞 HTTP 422)5651909449,批次 #130 项 3,维护者「同意」)点名的四个自测用例是否都在、且真的跑到:foreignM→ 红、foreignD→ 红、ownM→ 绿、newA→ 绿;以及 "added by this PR" 是否定义为在 merge base 上不存在、且对队列里的当前 base 重新求值。⛔ 范围提醒:只做 A′ 门禁。裁决的 B 项(新 changeset 命名规则)是治理面的一行,由 #17748 承载,⛔ 不属本卡;⛔ 也不许改任何历史 changeset 文件名(裁决第 3 项:legacy 不动,门禁只看 diff 形状)。
给接手席位的规矩
Claim:留在卡上作为历史,⛔ 不表示本席仍在占卡。接手席位可以接这张卡:按本仓规矩,发你自己的Claim:(含独立成行的Branch:与Clause-②:),⛔ 不要改本席那条,也 ⛔ 不要沿用本席的分支名以外的旧假设。Generated by Claude Code
claude commentedon Sep 14, 2026
状态收口:标签改回
pm:queue,assignee 已清 —— 接手席位可直接认领session_012GKcPZbMoGq7WPzKLfRBTU· 2026-09-14T06:1xZ承上一条(
5659821268)。本席停席,本卡没有在席 PM、也没有活着的 dev,继续挂pm:dispatched会把一个半态说成"正在做"。⇒ 剥回pm:queue并清掉 assignee。分支状态复测未变:
claude/issue-17712-foreign-changeset-guard@eeb4012a,一笔提交,只改scripts/check-empty-changeset.mjs(+469 / -6),无 PR。⛔ 接手人注意:上一条里的复核要点与范围边界(只做 A′;B 在 #17748;legacy 名不动)仍然成立,⛔ 不要因为标签变了就当作没做过的新卡。
Generated by Claude Code
8 remaining items
claude commentedon Sep 14, 2026
维护者裁决 —— D(本轮
AskUserQuestion在场通道,2026-09-14T08:06Z)domain:devx执行席session_017ef78bLdybu3AffehKkhfk录裁 · 承本卡评论5660507653呈报的 A/B/C/D维护者选定项,逐字:
裁定的作用范围
scripts/check-empty-changeset.mjs中scanForeign()的拒绝文案与::error注解文案。--diff-filter=MD、--no-renames、merge-base 语义、changeset-release/main豁免、skip-changeset的整 job 豁免,一律不动。Clause-②: no。裁决所依据的实测(本席取数,已在
5660507653与 #18146 评论5660165662留档)30 天窗口(
scripts/pm/git-history.mjs判定完整,exit 0;population 429,对照--diff-filter=A= 431):9 个 PR 会被新门禁拦,4 个带skip-changeset实际豁免,5 个硬拦(#18011 · #17845 · #17709 · #16750 · #16636)。承重实例ed7243d52:该 PR 落地 boolean 聚合支持,并同笔改对了描述该行为的待发布说明;按现文案 "rename yours; restore theirs from base" 执行,会把一条已被该 PR 改成假的发布说明恢复回去。⛔ 诚实边界:本席实测了这 5 个会被拒,并读了其中 2 个的 diff;未逐个裁定 5 个都正当。裁决 D 修的是补救语的适配,不是给这 5 个发通行证。
承接
门禁已在
main生效(0ffb4963e),⇒ 这条错误指引从现在起每命中一次就误导一次。本席据此立卡并派发实施,卡号随后追评在此。Generated by Claude Code
claude commentedon Sep 14, 2026
domain:devx执行席session_017ef78bLdybu3AffehKkhfk· 更正读数 2026-09-14T09:04Z本席早前在本线程报的「30 天窗口 429 个 changeset 提交 / 9 个 foreign / 5 个硬红」是错的。#18160 的 dev 证否了它,本席独立重测,确认 dev 对、本席错。
M/D提交skip-changeset豁免发火对照:同窗口
--diff-filter=A的 changeset 行 2070。结论方向不变,只是更强
裁决 D(只改补救语)不受影响 —— 被那句错误补救语误导的不是每月 5 个 PR,而是 14 个。承重实例
ed7243d52也不受影响,dev 用直接读取(git cat-file -e ed7243d52^:.changeset/aggregate-field-type-compatibility.md成功)替换了本席的推断,结论相同。根因
本席第一次跑的是⚠️ 征兆当场可见却没追:末行是
git-history.mjs log而非count,而那一次调用同时在给浅检出做 deepen。工具 exit 0,但本席收到的输出是截断的。afe1c4e0a1794a5681method: git log …—— 提交 hash 从中间断掉、method:行黏在其后。⇒ ⛔ exit 0 不保证你收到的行是全部,浅检出上触发 deepen 的那一次调用尤其如此。先
count定量级,再log取清单。⛔ 本更正不改本卡任何标签与状态。
Generated by Claude Code
fix(scripts): refuse a PR that modifies or deletes a changeset it did…
fix(scripts): the foreign-changeset refusal names both classes, not j…
fix(metadata): close out the adr-0030-notification-event retirement r…
fix(changeset, spec): cut the envelope spans the lowering refusal mak…
pm-dispatch: three landings the seat decides on its own record (rulin…
docs(changeset): the pending callerParamKeys note no longer lists the…
fix(plugin-security, objectql): a row-level check holds for every row…
fix(spec): the edge-condition upgrade entry and its pending changeset…
fix(formula, driver-mongodb): refuse == / != against a list literal a…
fix(cli)!: retire `os generate schema` — it refuses, and points at `o…
fix(service-analytics): an inferred cube lives only in its request — …
fix(plugin-security,driver-sql,driver-turso): lower type-blind at the…
fix(service-analytics)!: refuse a JSON-stored dimension or count_dist…