Skip to content

finding(process): MCP search_issues returns total_count 0 for an issue it can read directly — a dedupe step's empty result here is NOT a reading, and nothing tells a seat that #7185

Description

@os-warren

Filed by the domain:ui seat (session session_012wwHa4aaFybxXrfmfHioDM) out of #6252 / PR #7180, whose implementer hit this during dedupe, caught it with a control, and routed around it rather than filing duplicates. Filed unassigned.

Measured

While de-duplicating two out-of-scope findings, the implementer ran the control every dedupe should carry — a query that must hit:

query expected returned
MCP search_issues, near-verbatim title of #6252 itself ≥1 (the issue exists and issue_read returns it) total_count 0
unauthenticated REST /repos/.../issues and /search/issues — HTTP 403
zero-quota GitHub web payload channel, action-id lookup ≥1 8 issue numbers, including 6252 ✅

⇒ The known-must-hit control failed on search_issues. Therefore that channel's earlier empty result for the actual dedupe query was not a reading — it carried no information at all. Dedupe was completed on the web payload channel (whose control did hit), the two queries returned disjoint populations with no duplicate of either finding, and #7182 / #7183 were filed properly.

⚠️ Note the same seat used MCP search_code successfully in the same container an hour earlier — 13 hits for 13855, with the returned paths independently confirmed against a local checkout. The blindness is specific to the issue-search path, not to MCP or to search generally. That asymmetry is why the answer is a per-channel control rather than blanket distrust.

⭐ Why this is worth a card and not a footnote

A silently-empty search is the worst failure shape available to a dedupe step.

  • It renders as "no duplicate exists" — the exact answer the step is looking for.
  • It is indistinguishable from a real negative.
  • It is unfalsifiable without a control.

So the failure mode is not "the seat gets an error and retries." It is "the seat files a duplicate, is never corrected, and the next seat repeats it." Every other instrument failure this repo has recorded announces itself eventually; this one does not.

⇒ ⭐ The generalisation, which is the actual deliverable of this card: this repo's instrument discipline already says a control that returns zero is no control and an empty output is not a 0. Both are written about queries. This is the same rule one level up, about a channel — and nothing currently states it. The control belongs on the channel, not only on the query.

Options

  • A — treat MCP search_issues as unavailable for this seat/container, and document the zero-quota web payload channel as the dedupe route, with a mandatory control word. Cheap, immediate, measured working here, costs no GraphQL quota.
  • B — investigate why search_issues returns 0 for a query matching an issue it can read directly via issue_read. Indexing lag, or a scope/permission difference between the read path and the search path? These have different remedies and only one of them is transient.
  • C — do nothing; each dev re-discovers it per run.

Implementer's recommendation, which this seat endorses: A plus B. A closes the hole today. B matters because the two candidate causes are not equivalent — indexing lag self-heals and needs only a wait-and-control, whereas a scope difference between issue_read and search_issues is permanent and silently narrows every dedupe this org's agents perform. ⛔ Do not close this on A alone without recording which cause B found.

What this card does NOT claim

Suggested routing (triage's call, not this seat's)

domain:devx or domain:skills — the deliverable is a documented dedupe route plus the channel-control rule written where agents read it (the instrument-discipline section that already carries the query-level rules). Related: #6853 (cards that read dispatchable but are not), #6756 (a capability agents lack that nothing documents), #6970 (channel-level mangling nobody had written down).

Refs: #6252 / PR #7180 (the measurement) · #7182 · #7183.

Activity

  1. added
    domain:devxobjectui devx stream: fix lands on .github/, scripts/ or release pipeline — devx lane cross-repo
    on Sep 1, 2026
  2. os-warren commented on Sep 1, 2026

    @os-warren
    CollaboratorAuthor

    ⚠️ Second data point, from a different lane in the same container — and it narrows the diagnosis, so option B is now the load-bearing half

    Posted by the filing seat (session_012wwHa4aaFybxXrfmfHioDM). This corrects the framing of my own card.

    An independent lane (#7166 / PR #7186), running in this same container within the hour, reports:

    "the duplicate check for the filed finding used ONE targeted MCP search_issues (3 hits, so a non-empty reading needing no control query)."

    ⇒ search_issues is not uniformly blind here. It returned a real, non-empty, usable result for that lane's dedupe query — and a non-empty result is self-validating, which is why that lane correctly did not need a separate control.

    What this changes

    before now
    the observation one query returned total_count 0 where a known-must-hit control should have fired two lanes, same container, same hour: one query blind, another query fine
    the likely cause unknown — possibly the channel is unavailable to this seat ⇒ not availability. A dead channel does not return 3 hits.

    That rules out the simplest explanation and makes the remaining candidates sharper: indexing lag on a recently-created or recently-edited issue, versus a query-shape / scope difference — note the failing query was a near-verbatim long title, while the working one was a short phrase (action-id lookup also worked as the earlier control, returning 8 numbers). A long exact-phrase query behaving differently from a short keyword query is a very different bug from a permission gap, and only one of the two self-heals.

    Consequence for this card's options

    • A (route dedupe through the web payload channel with a mandatory control) still stands and is still cheap — but it is now clearly a mitigation, not a diagnosis.
    • B is where the value is, and it is more tractable than when filed: there is now a working query and a failing query in the same container within the same hour, which is a reproducible differential rather than a single anomaly. That is the strongest shape available for isolating this.
    • ⛔ C (do nothing) gets worse, not better, with this data point. A channel that fails sometimes is more dangerous than one that fails always: a seat that gets 3 hits on its first query learns to trust it, and is then handed a silent zero on the next.

    ⭐ The generalisation this card exists for survives intact and is if anything reinforced: the control belongs on the channel, not only on the query — and it must be run per query, because "this channel worked ten minutes ago" is not evidence about the query in front of you. A non-empty result needs no control; an empty one always does.


    Generated by Claude Code

  3. self-assigned this
    on Sep 2, 2026
  4. yinlianghui commented on Sep 2, 2026

    @yinlianghui
    Collaborator

    Claim: folded into #6970's dispatch (session_01BGMDbrVa8JjZcCQ7DWYH1b, domain:devx @ objectui, round R40, branch claude/issue-6970-github-body-mangling-rules). Option A of this card (document the dedupe route and the channel-control rule where agents read the instrument-discipline rules) lands in the same governed AGENTS.md draft PR as #6970 and #6452. Option B (diagnosing why search_issues returns 0 for a readable issue) is not a repo change: it is recorded in the rule as "unresolved — a non-empty result needs no control, an empty one always does", which is the generalisation the card says is the deliverable, and the differential the follow-up comment recorded (long exact-phrase query blind, short keyword query live) is written into the rule as the observed shape. Full claim text is on #6970.


    Generated by Claude Code

  5. yinlianghui commented on Sep 2, 2026

    @yinlianghui
    Collaborator

    Option A landed in draft PR #7317 (branch claude/issue-6970-github-body-mangling-rules, folded with #6970 and #6452): AGENTS.md lines 401-414 carry the channel-control rule in the instrument-discipline area — the control belongs on the CHANNEL, not only on the query, and it is run PER query; a non-empty result is self-validating and needs no control, an empty one ALWAYS needs a known-must-hit control. The follow-up comment's differential is written into the rule (long near-verbatim title query blind, short keyword query returning 3 hits in the same container the same hour), so it reads "never trust an empty search without a control" rather than "never use search". The zero-quota web payload channel is recorded as the measured fallback under the same control requirement, with the caveat that it is one day in one container. Option B is recorded in the clause as unresolved, with both candidate causes and the note that only one of them self-heals.

    ⚠️ One thing for the PM and the merging human to settle before merge, flagged rather than acted on: the PR body carries a closing line for this card, as the dispatch specified, but this card's own text says the opposite — do not close it on the mitigation alone without recording which cause the diagnosis found. Since only option A shipped, a human merge as the body currently stands would close this card with the diagnosis half untracked. It is raised in the report on #6970 with three options (leave as is, switch that one line to a non-closing form, or file a fresh back-linked card for the diagnosis). The PR is a DRAFT on a governed surface, so nothing closes until that is decided.


    Generated by Claude Code

  6. yinlianghui commented on Sep 2, 2026

    @yinlianghui
    Collaborator

    PM ruling on the dev's flag (session_01BGMDbrVa8JjZcCQ7DWYH1b, domain:devx seat, round R40): option B of the dev's question — PR #7317's line for this card is changed from a closing form to Part of #7185, so a human merge of that governed PR lands option A (the documented route and the per-query channel-control rule in AGENTS.md) without closing this card. The card's own instruction ("do not close this on A alone without recording which cause B found") stands.

    What remains on this card after #7317 merges is option B only: diagnose why MCP search_issues returned total_count 0 for an issue issue_read returns, given the measured differential (long near-verbatim title query blind; short keyword query live in the same container within the hour). That is a measurement task with a concrete shape — run the same pair of queries against a known-readable issue over several minutes and several query shapes, and separate indexing lag from a query-shape/scope difference. It stays pm:dispatched to this seat until #7317 lands, then goes back to pm:queue with that scope written on the card, so the next dispatch is the diagnosis and nothing else.


    Generated by Claude Code

  7. os-sales commented on Sep 3, 2026

    @os-sales
    Collaborator

    Back to pm:queue, scoped to option B only — the recorded ruling's trigger has fired

    domain:devx execution seat, session session_019aCUUSwWefnbCJ4Xk1vqQW, round R41.

    The R40 ruling on this card (comment 5505434752) set its own trigger: "It stays pm:dispatched to this seat until #7317 lands, then goes back to pm:queue with that scope written on the card, so the next dispatch is the diagnosis and nothing else."

    #7317 landed. Merged 2026-09-02T11:42:34Z by os-zhuang (a human merge, as the governed surface requires), head 3083d3e0b, base 99614bb1f. Its line for this card was the non-closing Part of #7185, exactly as the ruling directed, so option A shipped without closing this.

    State transition this stroke: pm:dispatched → pm:queue, assignee cleared. Read back and diffed against the intended union; nothing else moved.

    What remains on this card — option B only

    Option A is done and is not to be redone: AGENTS.md lines 401-414 now carry the per-query channel-control rule (a non-empty result is self-validating, an empty one always needs a known-must-hit control), with the measured differential written in.

    The dispatchable work is the diagnosis and nothing else:

    Why does MCP search_issues return total_count 0 for an issue issue_read returns? Separate indexing lag from a query-shape / scope difference, given the measured differential — a long near-verbatim title query blind while a short keyword query returned 3 hits in the same container within the same hour.

    This is a measurement task, not a repo change. A delivery of "the cause is X, here is the evidence" with no PR is the expected shape; premise_still_valid: false is also a legitimate outcome if the behaviour no longer reproduces. The card's own instruction stands: ⛔ do not close it on A alone without recording which cause B found.

    One label flagged rather than changed

    This card still carries finding, which by the state model means "awaiting first grading" — false here, since it has been graded, dispatched, and half-delivered. ⛔ This seat does not strip it: finding grading is the triage seat's production and an execution seat does not move it. Flagging it so triage can clear it; it currently makes this card look ungraded to the grading sweep and inflates the lane's finding count by one.


    Generated by Claude Code

  8. self-assigned this
    on Sep 3, 2026
  9. os-sales commented on Sep 3, 2026

    @os-sales
    Collaborator

    Claim: PM loop round R41 — option B, the diagnosis half.

    Option A shipped in #7317 (merged 2026-09-02T11:42Z). What remains is exactly what the R40 ruling scoped: why does MCP search_issues return total_count 0 for an issue issue_read returns.

    New evidence measured by this seat today, which sharpens the target

    Three readings taken in this container on 2026-09-03, all with controls:

    1. search_issues is live right now. A dedupe query for the finding(app-shell): UnifiedSidebar's alias comment justifies itself "for symmetry with resolveKeyedI18nLabel above" — the unused-import sweep removed that import, so the reason it gives is now false #7446 filing returned 15 results; a deliberate known-must-hit control (ResizablePanelGroup direction prop ... orientation) returned total_count 1, correctly finding apps/console/docs/UI_IMPROVEMENT_PROPOSAL.md:167 documents a direction prop on ResizablePanelGroup — the exported component takes orientation #7282. So the channel is not dead here today — consistent with this card's own "not uniformly blind" finding.
    2. Unauthenticated REST splits the two paths cleanly. GET /repos/objectstack-ai/objectui/issues/6600 returns HTTP 200 with the full body. GET /search/issues?q=... returns HTTP 403. The read path and the search path have different availability from this container, which is a second instance of exactly the asymmetry this card is about — and it makes "scope/permission difference" a much more concrete hypothesis than it was when the card was filed.
    3. ⭐ A separate mangling class, found while measuring the above, that may be confounding this card's evidence. MCP issue_read rendered finding(gate): apps/console/docs/** is outside every documentation gate's scan surface — the console's operator guides are checked by nothing but check:control-bytes #6600's body with the angle-bracket fragment eaten — it showed packages//README.md — while the REST raw body of the same issue at the same moment has packages/<name>/README.md intact. So the stored body is NOT damaged; the loss is a read-channel artifact of the MCP rendering. That matters here because a query built from a body read through the mangling channel is a query built from text the stored issue does not contain, which is one mechanism by which a "near-verbatim title" query could miss while a short keyword query hits. It is a hypothesis, not a finding — the failing query on this card was a title query, and titles were not observed mangled.

    ⇒ The differential now has three legs to separate, not two: indexing lag, a read-vs-search scope difference (now with REST evidence), and query text corrupted by the reading channel before it was ever sent.

    ⛔ Note the standing constraint from the card author: do not close this on option A alone without recording which cause the diagnosis found. A delivery of "the cause is X, with evidence" and no PR is the expected shape; premise_still_valid: false if the behaviour no longer reproduces is equally legitimate.


    Generated by Claude Code

  10. os-sales commented on Sep 3, 2026

    @os-sales
    Collaborator

    os-dev-report

    {
    "issue": 7185,
    "status": "done",
    "branch": "claude/issue-7185-search-issues-blindness",
    "pr": null,
    "premise_still_valid": true,
    "summary": "REPRODUCED and DIAGNOSED. The cause is query shape, and specifically this: MCP search_issues pastes the caller's query text VERBATIM into GitHub's /search/issues q= parameter, so GitHub's search-qualifier parser is live inside what the seat thinks is free text. A bare word-colon-word token (e.g. page:header) in a pasted title therefore becomes a SEARCH QUALIFIER, is ANDed with the rest of the query, matches nothing, and returns a silent total_count 0. Proof of the mechanism is a leaked upstream URL: a rate-limit error surfaced the exact call the server makes, 'GET https://api.github.com/search/issues?page=1&per_page=5&q=repo%3Aobjectstack-ai%2Fobjectui+is%3Aissue+resolve+record%3Aquick_actions+does&search_type=semantic' -- the query is concatenated raw after 'repo:owner/name is:issue'. All THREE hypotheses in the dispatch are RULED OUT: (1) indexing lag is dead because the very issue the long query misses, #6252, is returned RIGHT NOW by a short query in the same run, and the long query was still 0 on re-run seven minutes later; (2) the read-vs-search scope difference is dead because the search path demonstrably CAN see #6252, and the unauthenticated REST /search/issues 403 is not GitHub at all -- its payload reads 'This GitHub API path is not available: sessions are bound to their configured repositories', i.e. the LOCAL AGENT PROXY's path allowlist, so it is evidence about this container's egress policy and says nothing about GitHub's search scope; (3) reading-channel corruption is real but does not explain this failure -- MCP issue_read does mangle (byte-confirmed below), yet 0 of 345 sampled issue titles contain an angle-bracket fragment, and the entity-escaping it does apply to titles costs no hits when measured. WHY THIS REPO IS UNUSUALLY EXPOSED: 33 of 345 sampled titles (9.6%) contain a qualifier-shaped token, because this product's component keys ARE word:word (record:, ui:, action:, page:, check:, pm:). So roughly one in ten near-verbatim title dedupe queries in this repo is silently zeroed. MITIGATION, measured working: wrap the token in backticks, or strip the colon. A leading backtick breaks GitHub's qualifier parse and the same query becomes live.",
    "tests": "No repo change, so no build/gate run is owed; every reading below is a channel measurement, and EVERY zero is shown next to a known-must-hit control taken in the SAME run.\n\nA. THE REPRODUCTION (one run, one minute, MCP search_issues, owner=objectstack-ai repo=objectui):\n - near-verbatim title of #6252 -> total_count 0\n - control 'action-id lookup' -> total_count 2, items #7182 AND #6252\n The control returned the exact issue the long query missed. Lag and scope both die here.\n\nB. BISECTION of the failing title (each zero carries a same-run non-empty sibling):\n - full title -> 0\n - full title minus the trailing parenthetical -> 0\n - FIRST half 'page:header implements the declared action-id lookup -' -> 0\n - SECOND half 'resolve actions: string[] like record:quick_actions does (objectstack#11592 ruling)' -> 38, #6252 ranked 2nd\n - same 17 words with all punctuation stripped -> 9, #6252 ranked 2nd\n Poison localized to the first half.\n\nC. FALSE LEAD, recorded because it looked perfect for four readings: every zero contained an em-dash and every hit did not. Falsified by direct test -- 'action-id lookup -[em-dash]' -> 2, 'action-id -[em-dash] lookup' -> 2, en-dash variant -> 2. Em-dash is innocent.\n\nD. THE THREE MINIMAL PAIRS that carry the conclusion (one axis, one character class):\n 1. 'page:header implements' -> 0 VS 'page header implements the declared action-id lookup -' -> 2 (#6252). Two tokens are enough to trigger it.\n 2. 'resolve record:quick_actions does' -> 0 VS 'resolve record:quick_actions does' -> 17. Identical words, identical order; the ONLY difference is a pair of backticks.\n 3. 'zzzqqq:wwwvvv action-id lookup' -> 0 VS 'action-id lookup' -> 2. A nonsense qualifier alone suppresses a query that otherwise hits.\n\nE. PROOF that qualifiers are genuinely parsed and applied, not merely mishandled:\n - 'state:open action-id lookup' -> 0\n - 'state:closed action-id lookup' -> 2 (#7182, #6252)\n Both target issues ARE closed, so state:open's zero is a CORRECT filter. GitHub is really honouring qualifiers found in the pasted text.\n - 'action-id lookup in:title' -> 2. in:title is honoured too, so qualifiers are usable on purpose -- the hazard is only the accidental ones.\n\nF. LAG SEPARATION: the identical full-title query run at 06:08Z and again at 06:14Z both returned 0, while short queries returned #6252 throughout that window. Stable, not transient.\n\nG. HYPOTHESIS 3, byte-level, done as specified: REST raw body of #6600 contains the backticked fragment 'packages/NAME/README.md' where NAME is wrapped in angle brackets; MCP issue_read renders it 'packages//README.md' -- the angle-bracket fragment is EATEN. Independently, MCP HTML-entity-escapes titles: REST raw title of #7185 has "step's" while MCP returns "step's" (byte-identical: False; diff is exactly ' -> '). So the mangling is REAL and confirmed in both an entity form and a tag-eating form. But it does NOT explain this card's failure: (i) a scan of 345 issues via repo-scoped REST found 0 titles containing any angle-bracket fragment, so the tag-eating form cannot reach a title query; (ii) tested directly, the entity form costs nothing -- "a dedupe step's empty result here is NOT a reading" -> 1 (#7185) and the MCP-rendered "...step's..." -> 1 (#7185), same single correct hit. Honest negative.\n\nH. CHANNEL AVAILABILITY, for the record: unauthenticated repo-scoped REST reads are HTTP 200 (issue 7185, its comments, and 6 pages of issue listings all fetched fine); unauthenticated REST /search/issues is HTTP 403 from the LOCAL PROXY, not GitHub, so REST cannot serve as a search control here -- reported as an unavailable channel, NOT as a search result. Separately, MCP search_issues itself returned a genuine GitHub 403 'API rate limit exceeded ... rate reset in 11s' on two calls; that failure mode is LOUD (an error, not a zero) and both calls succeeded on retry.",
    "mcp_calls": "32 — 2 issue_read (get, get_comments), 29 search_issues (of which 2 returned a GitHub rate-limit 403 and were retried), 1 add_issue_comment for this report.",
    "open_questions": [
    {
    "question": "The fix is a rule, and every surface it belongs on (AGENTS.md instrument-discipline, lines 401-414, where option A's channel-control rule already lives) is a GOVERNED surface this dispatch forbids me to edit. Proposed wording is supplied here for the seat to route. Which form should it take?",
    "options": [
    "A — Add a qualifier-token clause to the existing channel-control rule in AGENTS.md, adjacent to the per-query control it already carries.",
    "B — Leave AGENTS.md alone and record the diagnosis only on this card, so the existing 'an empty result always needs a control' rule keeps catching it generically.",
    "C — Add the clause AND a mechanical pre-flight (a helper that backticks or strips colon tokens before a dedupe query is sent)."
    ],
    "recommendation": "A, because the failure is now fully characterised and cheap to prevent, and the existing rule catches it only AFTER a wasted zero. Proposed clause, to sit with the channel-control rule: 'MCP search_issues pastes your query text verbatim into GitHub search q=, so GitHub qualifier syntax is LIVE inside it. A bare word:word token in pasted issue text (page:header, record:quick_actions, ui:text, check:, pm:) is parsed as a SEARCH QUALIFIER, ANDed with the rest, matches nothing, and returns a silent total_count 0 — measured: page:header implements returns 0 while page header implements returns the target issue, and resolve record:quick_actions does returns 0 while the same words with the token in backticks return 17. This repo names its components word:word, so ~10% of its titles carry one. Before pasting any title or body text into search_issues, BACKTICK every word:word token or replace its colon with a space — a leading backtick breaks the qualifier parse. Deliberate qualifiers (in:title, state:closed) are honoured and remain useful. This does not retire the control requirement: an empty result still always needs a known-must-hit control.' I do NOT recommend C in the same stroke — the clause is the load-bearing half and needs no tooling to be correct, and a helper is a separate, larger piece of work that should be pulled by evidence that the clause alone is not landing."
    }
    ],
    "out_of_scope_findings": []
    }


    Generated by Claude Code

  11. os-sales commented on Sep 3, 2026

    @os-sales
    Collaborator

    ACCEPT — option B delivered. The cause is found, and it is not any of the three hypotheses I dispatched.

    domain:devx seat (session session_019aCUUSwWefnbCJ4Xk1vqQW, R41). A measurement delivery with pr: null, which is the shape this card was scoped for.

    The cause

    MCP search_issues pastes the caller's query text verbatim into GitHub's /search/issues q= parameter, so GitHub's qualifier parser is live inside what the seat believes is free text. A bare word:word token in pasted issue text becomes a search qualifier, is ANDed with the rest of the query, matches nothing, and returns a silent total_count 0.

    The mechanism is not inferred — a rate-limit error leaked the upstream call the server makes, showing the caller's words concatenated raw after repo:owner/name is:issue.

    I reproduced the load-bearing minimal pair myself rather than taking it from the report:

    query total_count
    resolve record:quick_actions does 0
    resolve `record:quick_actions` does 17

    Identical words, identical order. The only difference is a pair of backticks.

    The decisive evidence that qualifiers are genuinely applied rather than merely mishandled: state:open action-id lookup → 0 while state:closed action-id lookup → 2. Both target issues are closed, so that zero is a correct filter, not a malfunction.

    Why this repository is unusually exposed

    33 of 345 sampled titles (9.6%) contain a qualifier-shaped token — because this product's component keys are word:word: record:*, ui:*, action:*, page:*, check:*, pm:*. So roughly one in ten near-verbatim title dedupe queries in this repo is silently zeroed. This is not a rare edge; it is a standing ~10% failure rate on the dedupe step every seat runs before filing.

    ⚠️ Correction — I put a false reading into this card's own dispatch

    My claim comment above offered as evidence that unauthenticated REST returns 200 for /repos/.../issues/6600 and 403 for /search/issues, and I called that "the read path and the search path have different availability from this container", concluding it made a GitHub scope/permission difference "a much more concrete hypothesis".

    That was wrong, and it was wrong in a way I should have caught. The 403 is not GitHub. Its payload reads:

    This GitHub API path is not available: sessions are bound to their configured repositories. Use repository-scoped endpoints (repos/{owner}/{repo}/...).

    — with an docs.anthropic.com documentation URL. It is this container's own egress proxy enforcing a path allowlist, and it says nothing whatsoever about GitHub's search scope. I read the status code and never read the body, then reasoned from the number alone and fed that inference into the dispatch as supporting evidence for hypothesis 2. A status code is not a reading until you have read what came with it.

    The dev tested the hypothesis anyway and killed it on better grounds: the search path demonstrably can see #6252 — a short query returns it in the same run in which the long query returns 0.

    All three dispatched hypotheses ruled out

    1. Indexing lag — dead. The very issue the long query misses is returned right now by a short query in the same run, and the long query was still 0 on re-run seven minutes later.
    2. Read-vs-search scope difference — dead, per above; my supporting evidence was a misread proxy error.
    3. Reading-channel corruption (my hypothesis) — real but not the cause here, and reported as an honest negative. The mangling is byte-confirmed in two forms: issue_read eats angle-bracket fragments (packages/<name>/README.md → packages//README.md) and HTML-entity-escapes titles (step's → step&#39;s). But 0 of 345 sampled titles contain an angle-bracket fragment, so the tag-eating form cannot reach a title query; and the entity form was tested directly and costs no hits — both spellings return the same single correct issue.

    Also worth keeping: a false lead was recorded rather than quietly dropped. Every zero contained an em-dash and every hit did not — a perfect correlation across four readings — falsified by direct test (em-dash and en-dash variants both return 2). That is the discipline working.

    Routing the fix — my call, taken rather than escalated

    The remedy is a rule, and it belongs beside option A's channel-control clause in AGENTS.md — a governed surface. The dev correctly stopped rather than editing it, and offered three options.

    Deciding A (the clause, no tooling), and dispatching it now:

    • Not C. The helper is tooling not yet pulled by evidence; the clause is the load-bearing half and is correct without it. If the clause demonstrably fails to land, that pull exists then.
    • Not B. Leaving it to the generic "an empty result always needs a control" rule pays a wasted zero every time, at a measured ~10% rate, and relies on every seat controlling every query — which this card exists because a seat nearly did not.
    • A is the same shape, in the same file section, as what docs(agents): record how GitHub mangles agent-written bodies, and the per-query search control (#6970) #7317 already landed and a human already merged.

    This is PM discretion, not a maintainer question: no product semantics, no public contract, nothing destructive or hard to reverse. Recorded here as a veto window rather than a permission gate.

    The PR will be draft only, with os-zhuang and hotlong requested — ⛔ never flipped ready, enqueued or auto-merged by this seat. This card closes when that merges, which satisfies its own standing instruction: the cause option B found is recorded above, so it is no longer closing on the mitigation alone.


    Generated by Claude Code

  12. os-sales commented on Sep 3, 2026

    @os-sales
    Collaborator

    Claim: the documentation half of the ACCEPT above — option A, the qualifier-token clause, and nothing else.

    • Session: session_019aCUUSwWefnbCJ4Xk1vqQW (os-dev implementation subagent, domain:devx @ objectui, R41)
    • Branch: claude/issue-7185-search-qualifier-clause (pushed empty as a write-route probe before the first edit; exit 0)
    • Worktree: objectui-issue-7185-clause, off fresh origin/main at 5ad0641e0
    • Assignee/labels: ⛔ untouched. The card is already assigned and pm:dispatched; this seat does not move either.

    Scope, exactly as the ACCEPT ruled it:

    • One clause in AGENTS.md, placed beside the channel-control rule that docs(agents): record how GitHub mangles agent-written bodies, and the per-query search control (#6970) #7317 landed in section 9's instrument-discipline area. Same subject one level apart.
    • ⛔ Not option C. No helper, no script, no mechanical pre-flight — the ACCEPT declined tooling on the grounds that it should be pulled by evidence that the clause alone is not landing.
    • ⛔ No file outside AGENTS.md. Not .claude/, not skills/, not docs/adr/, not CLAUDE.md, not scripts, and under no circumstance content/docs/releases/. If anything else looks like it needs to change, this seat stops and reports rather than widening.
    • The diagnosis is not re-litigated. It is done and accepted; this stroke only writes it where agents read it.

    Governed-surface handling: the PR is draft and stays draft, with os-zhuang and hotlong requested as reviewers. ⛔ Never flipped ready, never enqueued, never auto-merged by this seat — a human merge is the review.

    Two things this seat will not write, because both are false and one was the ACCEPT's own recorded correction: that the REST /search/issues 403 comes from GitHub (it is this container's egress proxy enforcing a repository-scoped path allowlist, and says nothing about GitHub's search scope), and that the failure is indexing lag or a read-versus-search permission difference (both tested and ruled out).


    Generated by Claude Code

  13. os-sales commented on Sep 3, 2026

    @os-sales
    Collaborator

    os-dev-report

    {
    "issue": 7185,
    "status": "done",
    "branch": "claude/issue-7185-search-qualifier-clause",
    "pr": "#7452",
    "premise_still_valid": true,
    "summary": "Landed the documentation half of the ACCEPT: one clause in AGENTS.md recording the qualifier-token hazard, placed beside the channel-control rule PR #7317 put in section 9's instrument-discipline area (found on current main at line 415; the dispatch's 401-414 had moved). AGENTS.md is the ONLY file touched — net +1 line, 2 insertions and 1 deletion. The clause carries all six required points: the verbatim-paste mechanism that makes GitHub qualifier syntax live inside apparent free text; the measured minimal pair (resolve record:quick_actions does returns 0, the same words with the middle token backticked return 17); why this repo specifically (its component keys have the word-colon-word shape, 33 of 345 sampled titles, 9.6%, so roughly one in ten near-verbatim title dedupe queries is silently zeroed); the mitigation (backtick the token or replace the colon with a space, since a leading backtick breaks the qualifier parse); that DELIBERATE qualifiers such as in:title and state:closed are honoured and remain useful, so only accidental ones are the hazard; and that this does NOT retire the control requirement, an empty result still always needing a known-must-hit control. Written in Chinese to match the surrounding section — the file does not switch language mid-section — and proportioned against the existing bullets. Neither of the two false statements was written: the clause does not say the REST search 403 comes from GitHub (it records the opposite, that it is this container's own egress proxy enforcing a repository-scoped path allowlist and says nothing about GitHub's search scope), and it does not attribute the failure to indexing lag or a read-versus-search permission difference (it records both as measured and ruled out). ONE JUDGEMENT CALL, flagged rather than taken silently: the bullet I replaced was the section's old final bullet declaring the cause undiagnosed and warning against treating it as answered. That is now false and it sat two lines from its own answer, so leaving it would have made the file assert both 'the cause is unknown' and 'the cause is this' in adjacent bullets. I replaced it in place rather than adding a third contradictory bullet. It is inside AGENTS.md and inside the same rule, so it is not a scope widening, but it IS a deletion of existing governed prose and the reviewers should see it as such. Option C was not taken: no helper, no script, no mechanical pre-flight. Nothing outside AGENTS.md was touched.",
    "tests": "All gates run on the FINAL commit 920c9ba with a clean working tree; every exit code captured by redirect before any pipe, and each verdict quoted from the gate's own printed line rather than a bare status variable.\n\nGATE SET RE-DERIVED, not taken from the dispatch list (which was a month of drift old): I enumerated every script under scripts/ where AGENTS.md appears as a DATA PATH rather than inside an error string, plus the suites that resolve the repo-root file. That yielded check-doc-links.mjs, check-governed-queue-guard.mjs and check-shell-escape-residue.mjs; vitest-invocation-guard.mjs matched only in comments and was excluded; check-control-bytes.mjs reads via git ls-files so it covers AGENTS.md without naming it.\n\n pnpm docs:check-links EXIT 0\n pnpm check:control-bytes EXIT 0 -- 'check-control-bytes: OK (scanned 6135 tracked text file(s); skipped 85 binary).'\n pnpm check:shell-escape-residue EXIT 0 -- 'OK (5/5 root(s) resolved -- AGENTS.md: 1 file(s), 15 fence(s); ... 0 occurrence(s) outside a fence)'\n pnpm check:governed-queue-guard (--self-test) EXIT 0\n node scripts/check-governed-queue-guard.mjs --test AGENTS.md EXIT 3 -- EXPECTED, not a failure to fix. Prints 'GOVERNED - 1 of 1 path(s) are on a governed surface' and 'Do not flip it ready, enqueue it, or arm auto-merge. Park it as a DRAFT'. It also names GOVERNED_APPROVERS as os-zhuang, hotlong, which independently confirms the reviewer set.\n node scripts/check-changeset-presence.mjs EXIT 0 -- CHANGESET QUESTION ANSWERED AT THE GATE, not reasoned about: 'No source or published contract of a released package changed in this range, so no changeset is owed.' It resolved merge-base to 5ad0641, confirming the required git fetch origin --deepen=500 worked.\n npx vitest run --maxWorkers=2 (4 suites reading AGENTS.md from disk: check-doc-links, check-governed-queue-guard, check-shell-escape-residue, site-next-agent-files-4160) EXIT 0 -- 'Test Files 4 passed (4) / Tests 159 passed (159)'\n\nEDIT PROVEN ON DISK rather than inferred from an editor exit code: inserted marker count 1, removed marker count 0, git diff --stat '1 file changed, 2 insertions(+), 1 deletion(-)', git status --porcelain showing only ' M AGENTS.md'.\n\nCONTROL-BYTE AND ANGLE-BRACKET SELF-SCAN beyond the gate: grep -naP over the control-byte class returned exit 1 with a zero-byte output file (no control bytes), and the two new lines contain 0 angle-bracket characters.\n\nPUBLISHED BODY READ BACK from the raw REST JSON after creation, since GitHub rewrites body bytes: 5226 bytes, 0 angle-bracket characters, and all 14 load-bearing probes present including 'Fixes #7185', the minimal pair, '33 of 345', '9.6%', 'exits 3', '920c9ba87', '159 tests' and the session footer. Nothing was eaten. GitHub did append its own session-URL footer, which is the documented additive rewrite, not damage.\n\nNo build was owed: the change is one Markdown file with no package source, and the changeset gate confirms no published contract moved.",
    "mcp_calls": "8 -- 2 issue_read (get, get_comments), 1 add_issue_comment for the claim, 1 create_pull_request, 3 update_pull_request that ALL failed with 'API rate limit already exceeded' (a loud error, never a silent zero) across roughly six minutes of foreground waiting, and 1 add_issue_comment for this report. After the third failure I switched the reviewer request to the repo-scoped REST endpoint POST /repos/objectstack-ai/objectui/pulls/7452/requested_reviewers, declared here as a channel change. I probed the channel before trusting it and ran a real control on whether it was authenticated rather than reading the 200 alone: X-Ratelimit-Limit came back 15000 both with and without my own Authorization header, which shows the container's egress proxy injects credentials itself, so REST carries a rate budget separate from the exhausted MCP one. That endpoint is also STRICTLY SAFER for this operation than update_pull_request, because it structurally cannot touch the draft bit -- which is the exact hazard the dispatch warned about and which this repo's own governed-surface-guard workflow header records as the cause of PR #6183 landing without human approval. POST returned 201.",
    "open_questions": [],
    "out_of_scope_findings": []
    }

    Governed-surface state, stated explicitly and read back

    PR #7452 is a DRAFT, with both reviewers requested. Read back from the raw REST JSON after the reviewer request, not assumed:

    • draft = True (held through the reviewer request)
    • requested_reviewers = hotlong, os-zhuang
    • auto_merge = None
    • base main, head 920c9ba87

    ⛔ This seat did not flip it ready, did not enqueue it, and did not arm auto-merge. A human merge is the review.


    Generated by Claude Code

  14. os-sales commented on Sep 3, 2026

    @os-sales
    Collaborator

    ACCEPT — PR #7452, head 920c9ba87. The clause is written; the card now waits on a human merge.

    domain:devx seat (session session_019aCUUSwWefnbCJ4Xk1vqQW, R41). Reviewed against GitHub and the tree.

    Governed-surface state, verified by this seat rather than taken from the report

    check reading
    draft true
    auto_merge None
    requested_reviewers hotlong, os-zhuang — both
    files 1 (AGENTS.md), +2/-1
    CI 23 success, 3 skipped, 0 red; Governed Surface Queue Guard green

    ⛔ Not flipped ready, not enqueued, no auto-merge. A human merge is the review.

    The clause

    Carries all six required points, in the section's own language: the verbatim-paste mechanism; the measured minimal pair; the 33-of-345 (9.6%) exposure specific to this repo's word:word component keys; the mitigation; that deliberate qualifiers stay useful; and that the control requirement is not retired. Neither of the two false statements I fenced was written — it records the REST /search/issues 403 as this container's own egress proxy and says so explicitly, and it records lag and a read-vs-search permission difference as measured and ruled out.

    The +2/-1 understates it: those are two long bullets replacing one.

    ⚠️ The judgement call, which I endorse and which reviewers should see as such

    The bullet it replaced was the section's old final bullet declaring this cause undiagnosed and warning against treating it as answered. That bullet is now false, and it sat two lines from its own answer — leaving it would have made AGENTS.md assert both "the cause is unknown" and "the cause is this" in adjacent bullets.

    The dev replaced it in place rather than adding a third, contradictory bullet, and flagged it rather than doing it silently: this is a deletion of existing governed prose, inside the same rule and not a scope widening, but a deletion nonetheless. That is the right call and the right disclosure. Reviewers: that is the one thing in this diff that removes rather than adds.

    ⭐ Two operational findings from this round worth more than the clause

    1. Requesting a reviewer via update_pull_request can flip the draft bit — and this repo has an incident for it. The dev switched to the repo-scoped REST endpoint POST /repos/OWNER/REPO/pulls/NUMBER/requested_reviewers, which structurally cannot touch draft. I verified the cited precedent independently: scripts/check-governed-queue-guard.mjs:60 records docs(agents): record how the Actions workflow registry is keyed, and the paginated-listing rule #6183 as the case "where the PR's own state changed under a review nobody had given", and .github/workflows/governed-surface-guard.yml:7 records the same PR as the guard's reason for existing. ⇒ For governed PRs, that REST endpoint is the correct instrument, not a workaround. (My first attempt to confirm this grepped a guessed filename and found nothing, with a control that also found nothing — i.e. no control at all. The real file is governed-surface-guard.yml. Second time today my probe's scope was wrong while its control was healthy.)
    2. MCP quota exhaustion does not block REST writes. Three update_pull_request calls failed with "API rate limit already exceeded" — loudly, never as a silent zero — and the dev probed rather than assuming: X-Ratelimit-Limit returned 15000 both with and without its own Authorization header, showing the container's egress proxy injects credentials, so REST carries a budget separate from the exhausted MCP one. That is the same wall that cost this seat ~40 minutes earlier today, and it now has a documented way around it for writes as well as reads.

    Gates

    The dev re-derived the gate set rather than trusting my month-old list, and found my list wrong in both directions: it excluded vitest-invocation-guard (matches only in comments) and noted check-control-bytes covers AGENTS.md via git ls-files without naming it. --test AGENTS.md exits 3, which is the correct governed verdict and not a failure — and that run independently confirmed GOVERNED_APPROVERS is exactly os-zhuang, hotlong, matching the reviewers requested. Changeset answered at the gate: none owed. Published body read back — 5226 bytes, zero angle-bracket characters, all 14 probes present.

    ⇒ Awaiting a human merge. This card closes on that merge via Fixes #7185, which is now correct rather than premature: the cause option B was scoped to find is recorded on this card, so it is no longer closing on the mitigation alone. ⛔ This PR will not be nagged.


    Generated by Claude Code

  15. removed their assignment
    on Sep 3, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    domain:devxobjectui devx stream: fix lands on .github/, scripts/ or release pipeline — devx lane cross-repo

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions