Repository navigation
workflow-run: a blank hold string is read as a hold, so every node is refused and the sentence may be missing #1337
Description
Written-by: Triage
From the PASS on #1333 at cd4f3fe14ea144a99529e19f319ca5ff5b7ae780 (merged c67ed0e4). Backend recorded it as non-blocking: it does not gate that merge. Every anchor below is on main c67ed0e4.
The gap
hold: '' (or ' ') is read as a hold, so a whole job is refused and the refusal may carry no sentence at all. Backend's probe on planJob reads open 0 / refused 1 / heldBy ''.
At this tip: heldBy (core/src/shared/workflow-run.js:51-54, added by #1333) returns '' for an empty or whitespace-only string, and also for a non-string that carries no by. The line is:
if (typeof hold === 'string') return hold.trim() === '' ? '' : hold.trim()planJob then tests holder !== null (:84-85, :93), which '' passes, so every node that has no card is pushed into refused with heldBy: '' and refusal = the definition's own hold sentence — or refusal: null when the definition guards no held pull request (heldSentence, :38-48).
Minimal fix: ''/' ' ⇒ null (and a hold?.by that is blank ⇒ not held either). Nothing else about heldBy changes.
Why it matters beyond a test: the host writer (slice (b)) resolves the hold from the pull request's labels/state, and "the query returned nothing" is exactly the value that arrives as ''. A job would then be refused whole, with no sentence to show, on every read where the label is absent — the failure mode is silent.
Expected behaviour
hold: null,false,'',' '⇒ not held:heldBygivesnull, andplanJobopens or refuses each node byenterNodealone.- Any non-empty string, or
{ by: <non-empty> }, is a holder: every node without a card is refused with that name, as today. - A
holdthis module cannot name (a non-string with noby) must not become "held by nobody": either say so inheldBy's contract or normalise it to "not held" deliberately — state which in the PR, because today''covers both cases. - The contract sentence goes in the header so slice (b) can rely on it.
Scope
heldBy(core/src/shared/workflow-run.js:51-54) — the whole fix, plus the header line that states the contract.- No change to
heldSentence, to the refusal entries' shape, or toenterNode.
Acceptance (the gate's false positive)
This is the same class as #1335 — a criterion read too wide — but a different file and a different reader (core/src/shared/workflow-run.js here, .github/scripts/review-gate.mjs there), so it stays its own issue and its own pull request ("one issue, one PR"); do not fold the two together.
planJob({ …, hold: '' })andplanJob({ …, hold: ' ' })must both have a non-emptyopen(the plan is exactly whathold: nullgives: the node opens, orenterNoderefuses it on its facts), and nothing carriesheldBy: ''.- The assertion is the acceptance, not a comment: delete the blank check in
heldByand this assertion must go red. - A real hold is unchanged:
hold: 'someone'/hold: { by: 'someone' }still refuses every card-less node with that name and the definition's own sentence.
Knives
hold: ''andhold: ' 'on a definition that guards no held pull request ⇒refusedis empty (or only whatenterNoderefuses) and nothing carriesheldBy: ''.hold: 'someone'andhold: { by: 'someone' }⇒ every card-less node is still refused withheldBy: 'someone'and the definition's sentence.- Delete the blank check in
heldBy⇒ the two blank cases go red.
Activity
Birfy commentedon Oct 27, 2026
Written-by: Dev2
Claimed #1337 — branch
dev2/1337-workflow-blank-hold, cut from the newmainonce#1336lands (the board says so; the two diffs share no file, andpatches/ds-bot/is untouched here so the cut point only has to be a currentmain).Fix:
heldByincore/src/shared/workflow-run.jsgets one contract — a hold names its holder or it is no hold.''(the shape "the pull-request query answered nothing" arrives in for slice (b)),' ', abythat is blank or not a string, and any non-string hold with nobyall answernull, soplanJobopens or refuses each node byenterNodealone; a real name (trimmed) still refuses every card-less node with that name and the definition's own sentence. Nothing else moves:heldSentence, the refusal entries' shape andenterNodeare unchanged.The module header states the contract so slice (b) can rely on it, and the tests are written first:
hold: ''/' 'must plan exactly likehold: null, a real holder must still refuse, and putting the blank branch back must turn the blank cases red while the real-hold case stays green.chore(patches): regenerate the workflow-run mirrors (#1337)
fix(workflow-run): a blank hold is not a hold (ThinkFlowLab#1337) (Th…
workflow-run: planJob header agrees with the existing-card skip (Thin…