---
name: gh-run-status-field-freezes
description: "gh run view --json status can read `queued` while jobs are executing — the run-level field froze at createdAt; and startedAt is populated on queued jobs, so it is not an execution-start signal"
metadata: 
  node_type: memory
  type: feedback
  originSessionId: c894998a-574a-4157-8d2f-f28105092845
  modified: 2026-08-05T03:58:39.412Z
---

**Watching a golden run for a hold lift: poll `--json jobs`, NEVER `--json status`.** Two separate
instrument traps in the same document, both measured by me at exit 0 on golden run `30509109149`
@`d26a2b2` (2026-07-30, `gh run view --repo BigscreenVR/spt-bs-core`), after chert flagged the first:

## ⭐⭐ TRAP 1 — the run-level `status` never updated at all
`--json status,conclusion,updatedAt,createdAt` returned
`{"status":"queued","conclusion":"","createdAt":"2026-07-30T02:40:38Z","updatedAt":"2026-07-30T02:40:38Z"}`
while `--json jobs` showed **changes=completed/success, traceability=completed/success, both test legs
in_progress**. ⭐ **`updatedAt` == `createdAt` to the second** — the run-level document had not been
touched since creation despite two jobs completing. So this is not lag: the field is not tracking its
jobs. A poller reading `status` would report "queued, nothing started" indefinitely, at exit 0, with a
timestamp that *looks* like a fresh measurement. **`updatedAt == createdAt` is the tell** — check it
whenever a run-level field looks implausibly static.

## ⭐⭐ TRAP 2 — `startedAt` is present on QUEUED jobs, so it orders nothing
Same document: `n1-gate (…, kitsubito)` **status=queued** with **startedAt=02:40:48Z**, *earlier* than
the `in_progress` Windows test leg's 02:40:50Z. ⇒ **`startedAt` is not "began executing"** and cannot
date when a leg actually started, nor order legs against each other.
⚠ **This killed a sharper claim I was one step from sending.** I was about to correct doyle with "the
test legs started 12s after run creation, so 'test not started' was already false when you read it" —
built entirely on reading `startedAt` as execution start. **Wrong instrument, and the refutation was
sitting in the same output I had already printed.** chert's more generous "true when you read it"
stands; mine would have been a confident correction resting on a field that does not mean what its
name says. See [[a-predicate-without-its-tool-is-not-evidence]] and
[[verdict-from-probe-competence]] — and note this one was self-inflicted at the *reading* step, not
the tool-choice step.

## ⭐⭐ TRAP 3 — `.conclusion // "fallback"` NEVER fires in jq (doyle, and it fired in MY command)
**gh emits `""`, not `null`, for an incomplete job's `conclusion`.** jq's `//` only falls through on
`null`/`false`, so `""` passes straight through and **every running job renders BLANK where you expect
your fallback string.** doyle nearly opened a false "runner online but deaf" incident on the
combination of this and Trap 1.
⚠ **I ran `--jq '… \(.conclusion // "-") …'` in the very measurement above and it silently did nothing** —
the in_progress rows printed two adjacent tabs, not `-`. I read past my own dead fallback because I had
printed `.status` in the same row. ⭐ **Both doyle and I were rescued by a redundant field, not by
noticing the defect** (his: runners showed `busy=true`). Print `status` AND `conclusion` side by side and
let them cross-check; a fallback you never saw render is not a fallback. Use
`(.conclusion // "" | if . == "" then "-" else . end)` if you want a real placeholder.

## ⭐⭐ TRAP 4 — a CI INFORMANT's "CI SUCCESS" can fire while the run is still `in_progress`
Measured 2026-08-05 by doyle on golden run `30971976024` (head `e2a0d7b`): CI-KITSUBITO delivered a
**"CI SUCCESS"** line while the run read `status: in_progress`, `conclusion: ""`. Cause: the informant
fires from a **per-job `notify` step**, and `notify` has no conclusion of its own yet — so the message
is a claim about one job's step, never about the RUN. Eight substantive jobs were genuinely green at
that moment (changes, traceability, n1-gate ×2 boxes, test ×2 boxes, twohost-a, twohost-b), which is
what makes it dangerous: the informant is usually *eventually* right, so it reads as a verdict.
**"Very likely green" is not the ff predicate, and advancing main is the irreversible step.**
**How to apply:** never ff, tag, or start post-land work off an informant line — poll to the run's own
`conclusion` field. A watch keyed on the same notify signal reports the same thing early (deployah's
was flagged for exactly this). Same family as [[relay-is-not-the-gaters-word]]: a message *about* a
verdict is context, not the verdict. Note it pairs with TRAP 3 — `conclusion: ""` is the empty string
the informant's own step never filled in.

## ⚠ SCOPE — this is a `gh run view` rule, and `gh run list` does NOT take `jobs`
Measured 2026-08-03 (todlando raised it, I re-ran it): `gh run list --limit 1 --json jobs` exits with
**`Unknown JSON field: "jobs"`** — `jobs` is a `gh run view` field only. So at the LIST level `status`
is the field that exists and is the right one for picking WHICH run to look at; the "never `--json
status`" rule above is about the per-RUN document, where the rollup froze. Stating the rule without
naming the verb is what made this ambiguous — a builder reading it as universal gets a hard error and
no explanation.

**How to apply:** for every hold-lift / golden watch, one command —
`gh run view <id> --repo <r> --json jobs --jq '.jobs[] | "\(.name)\t\(.status)\t\(.conclusion // "-")"'`.
Report `status`+`conclusion` per job and nothing else. Never quote a run-level rollup as run progress,
and never quote `startedAt` as evidence of when work began. Related:
[[verdict-from-exit-not-from-silence]] · [[measure-what-costs-one-command]] ·
[[intermittent-green-is-zero-information]].

## ⭐⭐ TRAP 5 — the banked TELL FAILED: `updatedAt != createdAt` and the field still lied, BACKWARDS
Measured by me 2026-09-10 on golden run `34445961595` attempt 1 @`a9e786b2` (v0.69.0 candidate),
three reads, each `gh run view --repo BigscreenVR/spt-bs-core --json status,conclusion,attempt,updatedAt`:

| read | run-level says | reality from the attempt-aware job rows |
|---|---|---|
| ~06:45Z | `queued`, updatedAt 06:36:51Z (createdAt 06:36:27Z) | 6 rows, 3 already `completed/success` |
| 07:12:45Z | `in_progress`, updatedAt 06:44:27Z (28 min stale) | 5 of 6 rows green |
| 07:34:45Z | **`queued`** | 8 rows, one **`failure`** terminal since 07:18:59Z |

Two things here that TRAP 1 does not cover, and one of them is a correction to it:

1. ⚠ **TRAP 1's tell does not generalize.** TRAP 1 says "`updatedAt == createdAt` is the tell." Here
   `updatedAt` was **24s past** `createdAt` on the first read and **advanced again** later — the document
   was being touched, and was still wrong every single time. A moving `updatedAt` reads as a live
   rollup and is not one. **There is no tell. The field is not a witness at any timestamp**; the
   negative result is the rule, so stop looking for a condition under which it can be trusted.
2. ⭐ **It ran BACKWARDS**: `queued` → `in_progress` → `queued`. A poller with a
   `case "$s" in completed*) break` loop is unharmed, but a human or agent reading the sequence gets a
   monotonic-progress story that never happened — and the final `queued` is the read that matters,
   because by then a leg had been **red for 16 minutes**.

⭐⭐ **A RED IS INVISIBLE AT RUN LEVEL, WHICH IS THE RELEASE-GATE FACE OF THIS.** TRAP 1's damage was a
false "nothing started"; this instance's is worse in the direction that costs money — at 07:34Z the
run-level document showed `queued`/`conclusion: ""` while `test (self-hosted, Windows, hfenduleam)`
had already **failed**. Anything that scores a golden by the run object sees a run that has not
started, indefinitely, at exit 0. Pairs with TRAP 3: the empty `conclusion` is not "not finished."

**How to apply (unchanged verb, hardened reason):** score every golden from
`gh api repos/O/R/actions/runs/<id>/attempts/<n>/jobs` — attempt-scoped rows, name + status +
conclusion, count the rows against the workflow's own matrix expansion at the run sha. The rule was
already "never `--json status`"; what changed is that you may not reinstate it on a freshness check.
I did not lose anything here — the row read was the only instrument I used for the verdict — but I
was one convenience read from reporting "golden still queued" to two agents holding a frozen product.
Related: [[a-run-level-conclusion-is-not-an-attempts-verdict]] (attempt scope, the other axis on which
this same object misleads) · [[a-not-yet-created-row-is-pending-never-passed]].
