Repository navigation
fix(workflow-run): a blank hold is not a hold (#1337) - #1342
Conversation
A hold names its holder or it is no hold at all: '' (the shape "the pull-request query answered nothing" arrives in), ' ', a blank or non-string `by`, and any hold with no name answer null, so planJob opens or refuses each node by enterNode alone instead of refusing the whole job with no sentence. A real name still refuses every card-less node with that name and the definition's own sentence. The contract is stated in the module header; heldSentence, the refusal shape and enterNode are untouched. copies: core/ + tests + doc + changelog; patches/ds-bot/ left at main for a generate commit
node scripts/ds-bot-patches.mjs generate after the heldBy change: only the two whole-file mirrors under patches/ds-bot/files/ move, no numbered patch. check: pin 2b10c70, 10 patches apply cleanly, core/ equals ds-bot/ + patches.
|
Verdict: PASS at ae5bf4e Ran: What I checked at
Non-blocking notes:
|
…hinkFlowLab#1348 prose (ThinkFlowLab#1350) (ThinkFlowLab#1352) Owner: Dev Reviewer: Reviewer Fixes ThinkFlowLab#1350 Fixes ThinkFlowLab#1354 Tip SHA: a09f330 **Executor decision B: remove the frozen-branch seal whole, and keep what `ThinkFlowLab#1350` asked for that is not about the seal.** `ThinkFlowLab#1354`'s repair — read the list from `main`, list `ThinkFlowLab#1328`'s number in code — is on this branch's earlier commits and is now moot: with no seal there is nothing to read and no number to list. ## Reproduce **The seal could not hold.** `ThinkFlowLab#1328` (branch `dev4/1265-step2-one-board`, base `8d0a99c1`) entered the queue at `06:01:1xZ`, was turned draft, and merged at `06:03:21Z` — the seal never answered it: the base read found no document, and its branch matched no listed prefix. `ThinkFlowLab#1350`/`ThinkFlowLab#1354` patched the reading so the next such pull request would be refused. The owner then decided the mechanism goes instead of being patched: the cost of the seal (a queue-entry stop re-read on every `labeled` event, a policy document no pull request may edit, a number list that had to live in code) was not worth what it caught, and it never was a permission boundary — a direct push, an admin merge or a renamed branch walked past it. **The check that judges this pull request is `main`'s copy, and that copy still carries the seal.** That is why the document is not deleted here. The first push of this change (`8f97e4f5`) deleted `.github/agent/frozen-branches.json` along with the code; the check run at that head answered `do not enqueue ThinkFlowLab#1352: frozen: .github/agent/frozen-branches.json is not editable by a pull request`. `enqueue-guard.yml` checks out `ref: main` and never runs pull request code, so any diff that deletes or edits that document is refused by the seal until the seal itself is on `main`. Removing the reader and removing the document cannot travel in one pull request; this one removes the reader, keeps the document byte-identical to the previous tip, and the deletion is the follow-up pull request. ## What changed 1. **`.github/scripts/enqueue-guard.mjs` — the whole path, not a branch of it.** `FROZEN_PATH`, `FROZEN_REASON`, `FROZEN_EDIT_REASON`, `FROZEN_PULL_REQUESTS`, `frozenList()`, `frozenRefusal()` and the `decidePull` call site with its early return are gone; `decidePull` returns `{ decision, pull, repo, pr }` from `guard()` alone. No dead import, no dead export, no always-true branch left. `guard()`, `recordOnlyAt` and every other rule are untouched. 2. **The nine cases that existed only for the seal are deleted** — the five `ThinkFlowLab#1344` cases, the `ThinkFlowLab#1350` `merged`-short-circuit case (it pinned the seal's own ordering and has no meaning without it) and the three `ThinkFlowLab#1354` cases. The fixture loses the `/contents/` answer and the `frozen`, `prNumber` and `baseSha` knobs and the `BASE` constant that fed them: nothing reads them now, so none is left dangling. **No other test was touched** — every remaining assertion is byte-identical. 3. **`AGENTS.md` — the section is gone.** `#### Untrusted external agent branches` goes whole: the three `Do not` bullets, the `dequeuePullRequest` paragraph, the "never the baseline for `generate`" paragraph and the Chinese release sentence. What `ThinkFlowLab#1350` asked for that is *not* about the seal stays: rule 7 now matches `accountRefusal()` — only `type === 'User'` writes a conclusion, a report with **no `type` at all** is refused too, the account and its text stay as a note in `judged`, and the refusal's sentence stays `no Verifier report names the tip <sha>`. 4. **`.github/agent/frozen-branches.json` is unchanged here** — `cmp` against the previous tip is identical. Nothing reads it after this change; it is deleted in the follow-up pull request, which is possible only once `main` no longer carries the rule that refuses that deletion. 5. **`ThinkFlowLab#1354` is not needed any more.** Nothing is read from `main` or from a base, so the base-vs-main defect cannot recur; the issue is closed by this pull request as "the mechanism was removed", not by a fix. ## What I ran On `/data/w1352`, a tree extracted from the repository's own tarball of this branch's previous tip `c6ba9a09`, with `git` 2.53.0 and its `libexec/git-core` on `PATH`: - `node --test ".github/scripts/*.test.mjs"` → **543 / 543 pass** (this branch was 552 before this change; `−9` is exactly the nine cases removed with the seal). - `node .github/scripts/enqueue-guard.test.mjs` → **88 / 88 pass** (was 97; `−9` again). - `node .github/scripts/testing-layers.mjs --check` → `440 test file(s) in 16 workflow(s)`, every file and every workflow classified. - `node .github/scripts/check-mojibake.mjs` → `ok — 0 occurrences of U+FFFD in 1155 text file(s)`. - `diff -rq` against `c6ba9a09` → exactly three files, all listed under Scope. `cmp` on the JSON document: identical. - `grep -rn 'frozen-branches\|frozenRefusal\|frozenList\|external agent branch'` over `*.mjs`, `*.md`, `*.json`, `*.yml` → **0** (22 lines at the previous tip). The document file exists but no text names it. - GitHub's own read of the pull request: `3 files, +3 / −116`. ## Red without the source change **A deletion has nothing that can go red.** The nine cases were removed together with the code they pinned, so restoring the source would restore the tests and the suite would be green either way — claiming a red here would be a fabrication. The mechanical substitutes, each run: 1. **The grep.** The command above returns no line and exit status 1 (22 lines at the previous tip). 2. **The module's exports.** `import()` of `enqueue-guard.mjs` names exactly `decidePull, guard, main, recordOnlyAt`; `frozenRefusal` is `undefined`. The export does not exist to be called. 3. **The same fake `gh`, both tips, through the printed reader.** A pull request on a listed prefix branch with no conclusion, run through `main()`: - previous tip: `REFUSE — do not enqueue ThinkFlowLab#566: frozen: external agent branch; owner decision pending` - this tip: `REFUSE — do not enqueue ThinkFlowLab#566: no Verifier report names the tip aaaa…; no checkpoint: …` The seal's sentence no longer appears in any decision; the pull request is judged by the ordinary rules, which is the point of B. 4. **The one genuine red this change can show, and it did:** main's own seal refused this pull request while its diff deleted the document — the `8f97e4f5` run quoted under Reproduce. That red is real and is the reason the document is not in this diff. ## Not run - `npm run verify`: nothing under `core/`, `plugins/` or `apps/` is touched. - `generate` / `scripts/ds-bot-patches.mjs check`: `patches/` is untouched. - `check-arch-docs`: its rule reads `core/` changes; there are none. - A live queue entry: the next real enqueue exercises `decidePull`; a pull request cannot stage one without merging. ## Risk - **The seal is gone, so its refusal is gone.** A branch under a listed prefix, or a body carrying the agent's PR-body marker, is now judged by the ordinary rules only; one that carries a PASS at its tip and a clean body can enter the queue. That is the owner's decision, and the seal never stopped a direct push or an admin merge anyway. - **The document outlives its reader for one pull request.** Between this merge and the follow-up, `.github/agent/frozen-branches.json` sits in the tree with nothing reading it; nothing can act on it, and the follow-up deletes it. Anyone reading the tree in that window should read it as a stale config, not a live rule. Deleting the reader and deleting the file cannot travel in one pull request: `enqueue-guard.yml` is `pull_request_target` with `ref: main` and never runs pull request code, so main's copy of the seal refuses any diff that edits or deletes the document. That is a mechanical constraint of the check, not a choice made here. - **`ThinkFlowLab#1328` stays merged.** This removes the mechanism, not that merge; nothing here reopens or reverts it, and `ThinkFlowLab#1346`'s branch is untouched. ## Scope - `.github/scripts/enqueue-guard.mjs` — removal only. - `.github/scripts/enqueue-guard.test.mjs` — removal of the nine seal cases and of the fixture knobs they used. - `AGENTS.md` — the external-agent section deleted; rule 7's `accountRefusal()` alignment kept. Unchanged, deliberately: `.github/agent/frozen-branches.json`. Not touched: `core/`, `patches/`, `package*`, `.github/workflows/pass-label.yml`, tier calculation, and every tip belonging to `ThinkFlowLab#1328`, `ThinkFlowLab#1346`, `ThinkFlowLab#1348`, `ThinkFlowLab#1342`, `ThinkFlowLab#1349`. ## Checkpoint <!-- flybot:checkpoint --> ```json { "pr": 1352, "claim": "Dev", "issue": 1350, "summary": "Executor decision B: the frozen-branch seal is removed whole, not patched. enqueue-guard.mjs loses the four constants, frozenList(), frozenRefusal() and the decidePull call site; the nine cases that only pinned the seal go with the fixture knobs they used; the AGENTS.md external-agent section goes, while ThinkFlowLab#1350's rule-7 alignment stays. ThinkFlowLab#1354's fix is moot with no list to read. The document itself is not in this diff: main's own check refuses any pull request that edits it, so it stays byte-identical for a follow-up.", "files": [".github/scripts/enqueue-guard.mjs", ".github/scripts/enqueue-guard.test.mjs", "AGENTS.md"], "checks": { "ran": "node --test \".github/scripts/*.test.mjs\" (543/543); enqueue-guard 88/88; testing-layers --check; check-mojibake; grep for the seal names; cmp of the JSON document", "result": "green", "at": "a09f330ab2b298fc1095884a7f79dd086435c26f" }, "next": "none — ready for review at this tip" } ``` <!-- /flybot:checkpoint -->
Owner: Dev2
Reviewer: Reviewer2
Tip SHA: ae5bf4e
Fixes #1337
Two commits, cut from
main@2908bb9b.f59a7ed5is the fix —core/+ tests + doc + changelog only,patches/untouched;ae5bf4e4isDev3'sgeneratecommit, which rewrites the two whole-file mirrors underpatches/ds-bot/files/and moves no numbered patch. Nothing underpatches/is hand-written, andcheckis green at this tip: thepackagejob ran it and printed the three lines quoted under "What I ran".Reproduce
At
main c67ed0e4,planJobreads a blankholdas a hold:heldByanswers''for'', for' ', and for every shape it cannot name, andplanJobtestsholder !== null, so''refuses every card-less node. Two probes on that tree (facts all current, so the unheld plan openstriage):That second shape is the failure the issue names: slice (b) resolves the hold from the pull request's labels/state, and "the query returned nothing" arrives as
''— a job refused whole, on a definition that guards no pull request, with no sentence to show.The cases were written first; against the unfixed source:
What changed
core/src/shared/workflow-run.js:59-64—heldBygets one contract: a hold names its holder or it is no hold.''and a whitespace-only string answernull; a non-string with nobyanswersnull; abythat is blank or not a string answersnull; a real name is returned trimmed.planJob'sholder !== nulltest is untouched, so a hold that names nobody now plans exactly likehold: null— each node opens or refuses onenterNodealone.core/src/shared/workflow-run.js:24-27— the module header states the contract for slice (b): "a hold names its holder, or it is no hold", with''named as the shape an empty query arrives in.core/test/workflow-run.test.js:172-203— two new cases: blank ('',' ','\t\n') plans byte for byte likehold: null, with noheldBy: ''anywhere; and a hold with no name ({},{ by: '' },{ by: ' ' },{ by: 7 },42,true) is read as not held, deliberately, while a padded real name still holds and is reported trimmed. Test 7 (a held job opens nothing…,:148-171) loses the one assertion that encoded the bug — it assertedhold: { by: '' }refuses the job; a maintainer-Bot line stands in its place.docs/subsystems/core-host.md:131— the running-layer paragraph now carries the contract sentence, so the page and the module cannot disagree about what "held" means.changelog.d/1337-workflow-blank-hold.fixed.md— user-visible: a blank hold no longer refuses a job.heldSentence, the refusal entries' shape ({ node, refusal, heldBy, unreadable }),enterNode, andplanJob's order. No UI, no host writer, no second refusal sentence.What I ran
Local,
TZ=UTC, Node v22.23.3, tree cut frommain@2908bb9b:test/workflow-run.test.js→ 15/15 (13 before this fix; the two workflow-run: a blank hold string is read as a hold, so every node is refused and the sentence may be missing #1337 cases are new).workflow,workflow-repo-dev,workflow-store,workflow-board,workflow-preview,workflow-doc,workflows-page→ 76/76.core(--test-concurrency=4): 1871 tests, 1866 pass, 5 fail — the five are this machine's known environment failures, none in this diff:check-arch-docs"empty diff passes" (nogit),composer-label/header-effects/read-pageMODULE_NOT_FOUNDresolved from/opt/flybot/package.json, and theusage-fieldsfixture (usage.sqlitenot generated here). CI runs them.node scripts/ds-bot-patches.mjs check. This machine has nogit, so my own run atf59a7ed5(before the generate commit) printed a skipped pin and the honest red third line:Both files are whole-file mirrors under
patches/ds-bot/files/(neither is in a numbered group oftopics.json), sogeneraterewrites them and no numbered patch moves. At this tip the same script runs in CI withgitpresent — thepackagejob (122024085410, completed2026-10-27T05:16:33Z) prints, verbatim:ae5bf4e4touches only those two mirrors: both are byte-identical to thecore/files atf59a7ed5(same blob SHAs), and all ten numbered patches keep theirf59a7ed5blob SHAs.Red without the source change
Put the blank branch back in
heldBy('string' → trim() === '' ? '' : trim(), and the object branch back totypeof hold.by === 'string' ? hold.by.trim() : ''), keep the tests, runtest/workflow-run.test.js:The two finer knives separate the halves: reverting only the string branch reds 9 alone (14/15), only the object branch reds 10 alone (14/15), and test 7 — a real holder still refusing every node — stays green in all three, so the knife kills the blank reading and not the hold. Restored afterwards,
cmpbyte-identical and 15/15 again.Not run
node scripts/ds-bot-patches.mjs generate— not run here (this machine has nogit);Dev3ran it on this branch inae5bf4e4, which touches nothing but the two mirrors.npm run lint— nooxlinton this machine (lint: no oxlint found, exit 2); CI's lint job is the check.check-arch-docs— it needs a diff againstHEADand this checkout has no.git("cannot diff against HEAD" is one of the five known failures above). The page it would ask for is touched anyway, in the same diff.build-clientand the client suites — no client file changed.planJobyet.Risk
Low. The change is inside one pure function that nothing in
core/calls yet (grep -rn planJob core/srcreaches only this module), so no running behaviour moves until slice (b) lands. The deliberate judgement call: a hold this layer cannot name is read as "not held" rather than "held by nobody". Refusing every node withheldBy: nullwould be indistinguishable from a fact refusal — the card could not tell "held by an unnamed party" from "the query answered nothing" — so the module states the contract instead, and slice (b) must pass a name ('<holder>'or{ by: '<holder>' }) when it means a hold. A caller that passes a bare truthy non-string and expects a hold gets none; that is the documented contract, and the alternative is a silent whole-job refusal, which is the bug this issue removes.Checkpoint
{ "pr": 1342, "claim": "Dev2", "issue": 1337, "summary": "A blank hold is no hold now: heldBy answers null for the empty string (an empty pull-request query), for whitespace, and for any hold no name can be read from. planJob then opens or refuses each node by enterNode alone instead of refusing the whole job with no sentence. A real holder still refuses every card-less node with that name and the definition's own sentence, and test 7 keeps that half green. The contract lives in the module header for slice (b) and in the doc paragraph. The knives separate the blank string from a blank by, and the real-hold case stays green.", "files": [ "core/src/shared/workflow-run.js", "core/test/workflow-run.test.js", "docs/subsystems/core-host.md", "changelog.d/1337-workflow-blank-hold.fixed.md", "patches/ds-bot/files/src/shared/workflow-run.js", "patches/ds-bot/files/test/workflow-run.test.js" ], "checks": { "ran": "workflow-run 15/15, workflow neighbours 76/76, full core 1866/1871 (5 known env); knife 13/15 with cases 9 and 10 red; at this tip the package job 122024085410 runs ds-bot-patches check = 'ok 10 patches apply cleanly in order' + 'ok core/ equals ds-bot/ + patches/ds-bot/'", "result": "green", "at": "ae5bf4e49b9347145f9098cb741398df60f7ab7e" }, "next": "Reviewer2 judges this tip; the two generate mirrors are in (ae5bf4e4)." }