Skip to content

[finding] dispatch-gates lists check-governed-queue-guard among the LOCAL gates, but it can only ever exit 1 outside a workflow run #14004

Description

@claude

Found while implementing the internal fold of #13729 (PR #14003). Filed rather than ridden along: it is a different file surface than that card's declared one.

Measured

On origin/main at c0770d0b, a diff touching .claude/agents/os-dev.md derives ten gate families. node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack prints them under the heading

Local gates for this card (paste into the dispatch prompt):

and --commands emits all ten as runnable lines. One of the ten cannot produce a verdict outside a workflow run, by construction:

node scripts/pm/check-governed-queue-guard.mjs   [governed-surface-guard.yml]   matched via .claude/agents/os-dev.md

Run locally, at that same head, with nothing else wrong in the tree:

EXIT=1
⛔ Governed Surface Queue Guard: could not read GITHUB_EVENT_PATH (ENOENT: no such file or directory, open '').
   This guard reads the workflow event payload and nothing else; without it there is no diff to judge,
   and "could not look" must never exit 0 here.

The guard's own refusal is correct and deliberate — "could not look" must not exit 0. The defect is upstream of it: the derivation advertises the command as a LOCAL gate, so every dev on a .claude/agents/** (or any governed-surface-guard.yml-covered) diff harvests a command whose only possible local outcome is a nonzero exit.

Why it matters

Two costs, both repeatable:

  1. A guaranteed nonzero in a list a dev is told to run. The caption says "paste into the dispatch prompt" and --commands promises "one runnable command per line". A dev following that gets 9 green and 1 red on a clean tree, and has to go read the gate source to learn the red is structural. That is a cycle burned per card, on the highest-traffic agent surface in the repo.
  2. It degrades the red signal. A refusal that always fires trains the reader to discount reds in that list. The sibling gates already distinguish these two states in their own output — check:doc-formula-expressions prints an explicit PREREQUISITE NOT MET ... Nothing was measured ... It is NOT a finding banner. The queue guard prints an equally clear message, but nothing at the DERIVATION layer tells the dev, before running, that this row is not locally measurable.

Note this is NOT the "derived versus ran" reconciliation gap of #13774, and NOT a bucket-placement question like #13813: the family is derived correctly and it really does gate these paths. The gap is that the derivation's local-gate list mixes families a dev can measure with one it structurally cannot, without marking which.

Options, not a decision

  • Mark CI-only families in the output (a third bucket, or an annotation on the row) so --commands can omit them and the human-readable list still names them as CI-measured.
  • Leave the row and have it exit 0 with a "not measurable outside CI" verdict when GITHUB_EVENT_PATH is absent — cheaper, but it weakens the guard's deliberate "could not look must never exit 0" stance, so probably wrong.
  • Detect CI-only families by shape (reads GITHUB_EVENT_PATH; declared only in a workflow with no local invocation) rather than by an allowlist, so a family added tomorrow classifies itself.

Unassigned and untriaged on purpose. Dedup: full enumeration of all 410 open issues via the REST list endpoint plus local grep on this container (positive control: #13729 present in the listing) found nothing on check-governed-queue-guard or GITHUB_EVENT_PATH; the only two hits for "queue guard" are PM seat posts.

Refs: #13729 · PR #14003 · #13774 and #13813 (adjacent dispatch-gates cards, both distinct from this one).

Generated by Claude Code


Generated by Claude Code

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions