Repository navigation
feat(gate): issue-guard — a PR must name the CLOUD issue it serves - #100
Conversation
Refs: CLOUD-178 The board rule was prose, and this repo's own rule 2 predicts what happens to prose: feedforward only. The evidence is a session that landed three PRs while following every gated discipline and skipping every ungated one. It never skipped verify — ready-guard denies gh pr ready without the receipt. It never wrote a memory directly — memory-guard denies it. It never malformed a commit subject — commit-msg rejects it. And it never once consulted the board: no issue pulled, no issue moved, no issue created, and CLOUD-178 — which already described the defect being worked on, with measurements that contradicted the fix — sat unread in Todo the whole time. Follow-ups went into chat messages that die with the session. That is not a memory failure, it is an enforcement-surface failure: behaviour tracked exactly which rules had a mechanism. So give the board rule one, on the path that cannot be routed around. issue-guard denies gh pr create and gh pr ready unless a CLOUD-<n> appears in the branch, in a commit on the branch, or in the command. You cannot name an issue you have not looked up, so the gate that blocks landing is also what forces the search — and it forces it before the code is written, not after. Deliberately NOT gated, stated rather than left as an unmet obligation: whether outstanding items actually reach the issue. No computable predicate exists over anything the repo can see. The compensating control is that a durable home now always exists, so "nowhere to put it" stops being available. Wrapper-aware (mise exec -- gh), quote-scrubbed so a commit message naming the command is not the command, fails open on unparseable input, and honours BATTEN_ISSUE_GUARD_BYPASS=1 for a PR that genuinely precedes its issue. 14 bats cases.
Refs: CLOUD-178 The first draft ran in the real checkout, and passed. Then the commit adding the guard put `Refs: CLOUD-178` in its own message — the guard correctly allowed, and every deny case flipped red. A guard whose verdict reads live git state cannot be tested against live git state: the suite was asserting a property of this branch, not of the guard. Each case now builds a throwaway repo with no issue reference and works from there. Adds two cases the isolated fixture makes expressible: a reference on main but not on this branch does not count, and outside a git repo the guard fails open rather than blocking every PR.
CLOUD-178 claude.ai connector tools flip between readable and UUID names, silently breaking the permission allowlist
Split out of CLOUD-177, which is Done on its own scope. Evidence thread: the comments on CLOUD-177, in particular the final correction establishing verdict (a). ReadyA claude.ai connector MCP server is exposed to a session under two different names over its lifetime, and a permission allow rule can only name one of them. At session start the Linear connector appears as This hits the board-in-lockstep rule directly: moving a Measured, across two containers
Verdict: the UUID is stable and equal across containers. It is a durable identity for the connector; what varies is which of the two names is live at a given moment. This was initially mis-called as "unstable per boot" from a session that had not yet reconnected — absence of a reconnect is not evidence of naming stability, and any future measurement here needs to span at least one reconnect to say anything. Confirmed load-bearing, not cosmetic: a Note this is a second, independent defect from the one that originally masked it: an org-level Ready predicateA session survives a connector reconnect with Linear calls still auto-approved — no prompt on a Done (proposed)
"mcp__Linear",
"mcp__4db58e41-cd4e-4818-8922-46cf616593f4"
Open questions
Why the repo cannot fix this itselfIn Claude Code on the web, connectors are "provisioned by the remote host and arrive as explicit Generated by Claude Code |
|
/fast-forward |
Outstanding items from this session, recorded where they persistPosting here because Linear writes are blocked by the permission classifier in this session. This PR is linked to CLOUD-178 via linkback, so these reach the issue. They belong on CLOUD-178 proper — please move or let me know when Linear writes are permitted. 1. #95's fix targets a different mechanism than CLOUD-178 measured#95 added CLOUD-178's measured finding is the readable ↔ UUID flip, chosen per registration episode by the host. That flip occurred live during this session: Linear disconnected and returned as 2. #95 put the entry in the wrong scopeCLOUD-178's Done specifies user-level
3.
|
…he transition Refs: CLOUD-178 issue-guard shipped with a correct predicate and a wrong premise. It said the agent must be forced to remember the board. Measured since: a commit carrying `Refs: CLOUD-178` moved that issue Todo -> In Progress, set its assignee and attached the PR, with no write call from the session — the tracker's GitHub integration keys on the identifier and performs the transitions itself. So the identifier is not traceability, it is the automation's input, and the agent's job is to supply it rather than to mirror the board by hand. Hand-moving is also the fragile path: a state change is a tracker write, and a write can be denied mid-session when the connector re-registers under a name no allow rule matches — which is precisely how this session lost the board for three landed PRs. An identifier in a commit travels in git, where nothing can deny it. Records two limits observed rather than assumed: the merge-side transition did not fire (#100 merged, the issue stayed In Progress), and the tracker's own per-issue branch name carries the key from the first push, earlier than any commit message does. No behaviour change — the gate's predicate was already right.
Refs CLOUD-178.
The board rule was prose, and rule 2 predicts what happens to prose: feedforward only. This is its mechanism.
The diagnosis
A session landed three PRs (#95, #97, #99) while following every gated discipline and skipping every ungated one.
It never skipped
verify—ready-guarddeniesgh pr readywithout the receipt. It never wrote a memory directly —memory-guarddenies it. It never malformed a commit subject —commit-msgrejects it.And it never once consulted the board: no issue pulled, no issue moved, no issue created. CLOUD-178 — which already described the defect being worked on, carrying measurements that contradicted the fix — sat unread in Todo the whole time. Follow-ups went into chat messages that die with the session.
That is not a memory failure. Behaviour tracked the enforcement surface exactly.
graph-check(CLOUD-175) gates the board's internal coherence, but it is a pure function of stdin someone must choose to pipe — nothing makes a session consult the board at all.The gate
issue-guarddeniesgh pr createandgh pr readyunless aCLOUD-<n>appears in the branch, in a commit the branch adds, or in the command. You cannot name an issue you have not looked up, so the gate that blocks landing is also what forces the search — at the one point in the lifecycle that cannot be routed around, and before the code is written rather than after.Wired as a third
PreToolUsehook onBashalongsidegh-guardandready-guard. Wrapper-aware (mise exec -- gh …is the sandbox's only working form), quote-scrubbed so a commit message naming the command is not the command, fails open on unparseable input and outside a git repo, and honoursBATTEN_ISSUE_GUARD_BYPASS=1for a PR that genuinely precedes its issue.It would have blocked all three PRs that motivated it.
What this deliberately does not gate
Whether outstanding items actually reach the issue. There is no computable predicate for that over any artifact the repo can see, and stating it beats leaving an unmet obligation implied. The compensating control is that every PR now names an issue, so a durable home always exists and "nowhere to put it" stops being available.
A test bug worth recording
The first suite ran in the real checkout and passed. Then the commit adding the guard put
Refs: CLOUD-178in its own message — the guard correctly allowed, and every deny case flipped red. A guard whose verdict reads live git state cannot be tested against live git state; the suite was asserting a property of this branch, not of the guard.Every case now builds a throwaway repo it controls. That also made two cases expressible that weren't before: a reference on
mainbut not on the branch does not count, and outside a git repo the guard fails open rather than blocking every PR.Verification
mise run verifygreen on69c57ff. 15 bats cases for this guard; 181 in the suite.Generated by Claude Code