Skip to content

Link only a closing keyword, and read every comment page, on the delivery board (#1189) - #1190

Merged
Birfy merged 1 commit into
mainfrom
dev2/fix/1189-delivery-board-links
Oct 25, 2026
Merged

Birfy merged 1 commit into
mainfrom
dev2/fix/1189-delivery-board-links

Conversation

@Birfy

@Birfy Birfy commented Oct 25, 2026

Copy link
Copy Markdown
Collaborator

Owner: Dev2
Reviewer: Reviewer
Tip SHA: 36ba643
Base: fc19e6d (current main)

Fixes #1189

Reproduce

On fc19e6d2, core/src/host/delivery-board.js:242 builds the issue → pull request link with text.matchAll(/#(\d+)/g) over the pull request's title and body, so a body that says also see #12 makes issue #12 show a pull request that closes nothing. :128-130 reads comments?per_page=100&page=1 once, and verdictOf (:41) scans only those, so on a pull request with more than 100 comments — ordinary here (#1170, #1183) — the newest Verdict: is never read and the badge falls back to no verdict at this tip.

Both are visible through the existing fake-fetch tests: give a pull request the body also see #12 and the card for #12 claims it; put the newest verdict on page two and the card says no-verdict.

What changed

  • core/src/host/delivery-board.js — closingLinks(): only GitHub's own closing keywords (close[sd]?, fix(?:es|ed)?, resolve[sd]?, case-insensitive, word boundary, optional colon) followed by #n link a pull request to an issue (Delivery board: linkedTo treats any #12 in a PR body as a link, and verdictOf only reads the first 100 comments #1189). commentsOf() now goes through the existing pages() helper, so every comment page is read up to the existing MAX_PAGES cap; the comment in the function records what a long thread costs (each page is a cached GET with If-None-Match, a normal thread still costs one read). Nothing else changes: still read-only, no new endpoint, verdictOf still reads from the end because GitHub returns comments oldest → newest.
  • core/test/delivery-board.test.js — two cases: a pull request whose body only mentions #10/#12 leaves #10 unlinked and #12 on the pull request that closes it, while Closes: #13 links; and a 120-comment thread whose newest Verdict: INC at <tip> sits at index 101 decides the badge (inc) with page 2 actually requested.
  • docs/subsystems/core-host.md — the delivery-board row now states the link rule and that every comment page is read.
  • patches/ds-bot/files/src/host/delivery-board.js, patches/ds-bot/files/test/delivery-board.test.js — the mirrors, byte-identical.
  • changelog.d/1189-delivery-board-links.fixed.md — user-visible (the board is in the UI).

What I ran

  • cd core && node --import ./test/harness-resolve.mjs --test --test-concurrency=1 test/delivery-board.test.js test/delivery-view.test.js test/delivery-write-failure.test.js test/kanban.test.js test/bot-tasks.test.js → 30/30, 0 failed. delivery-board alone is 14/14 (base 12 + 2).
  • oxlint (-c core/.oxlintrc.json on the two changed files) → 0 warnings, 0 errors.
  • node scripts/ds-bot-patches.mjs check → skipped pin (no git), ok 10 patches apply cleanly in order, ok core/ equals ds-bot/ + patches/ds-bot/.
  • node .github/scripts/changelog.mjs check → 57 fragment(s) … well formed.
  • node core/scripts/build-client.mjs --check → exit 0 (no client change).

Red without the source change

Not run

  • npm run verify end to end: its arch-docs step needs git and origin/main, and this box has no git; the whole core suite is not runnable here either (a pristine fc19e6d2 reports the same eight failures on this box). CI's tests-ok is the terminal answer.
  • A live GitHub read: the tests use an injected fake fetch; nothing here touched the real API.

Risk

  • Low. A long thread now costs up to MAX_PAGES reads of one URL instead of one — bounded, cached per URL with the ETag, and only open pull requests and issues reach commentsOf.
  • The link rule is stricter: a body that merely mentions an issue no longer shows a pull request on that card. That matches what GitHub's own closing keywords mean, and the change makes cards claim less, never more. The title is still searched next to the body (as before), so a title that starts with a closing keyword still links.

…nt page (#1189)

The board counted any `#12` in a pull request body as a link, so "also see #12"
attached a pull request that closes nothing; and it read one page of comments, so a
verdict past the first hundred read as "no verdict at this tip".

Only GitHub's closing keywords (close/fix/resolve + #n, case-insensitive) link now, and
comments are read page by page through the existing pages() helper, capped at MAX_PAGES.
Read-only, no new endpoint, no change to the snapshot shape.
@Birfy Birfy added the agent-pr Pull request opened by an agent label Oct 25, 2026
@Birfy Birfy self-assigned this Oct 25, 2026
@Birfy

Birfy commented Oct 25, 2026

Copy link
Copy Markdown
Collaborator Author

Verdict: PASS at 36ba643
Written-by: Reviewer
Attestation: 36ba643 role=Reviewer

Both #1189 points are fixed in the two places the issue named, and nothing else moved: the diff against fc19e6d2 is 6 files — the host module, its test, the delivery-board line in core-host.md, the two patches/ds-bot/files/ mirrors (byte-identical to core/) and one changelog fragment.

  • Linking: closingLinks reads GitHub's own closing keywords (close[sd]?|fix(?:es|ed)?|resolve[sd]?, case-insensitive, word boundary, optional colon) and nothing else, so also see #10, same as #12 links neither, while Closes: #13 does. The map is still first-wins, so a prose mention can no longer take an issue away from the pull request that really closes it.
  • Comments: commentsOf goes through pages(), the helper the other lists already use — same MAX_PAGES, each page a cached GET with its ETag, and it stops at the first short page so a normal thread still costs one request. verdictOf still scans from the tail, which is now the true newest comment, so a Verdict: past the first hundred decides the badge instead of falling back to no verdict.
  • Read-only invariant intact: no new endpoint, no request helper beside get, payload shape gains no field — only the meaning of linked narrows.

Ran:

$ TZ=UTC node --import ./test/harness-resolve.mjs --test --test-concurrency=1 test/delivery-board.test.js   # from core/: 14/14 pass
$ # knife: CLOSING_KEYWORD back to /#(\d+)/g      -> 13/14, only "only a closing keyword links a pull request to an issue" reds
$ # knife: commentsOf back to page=1 only         -> 13/14, only "a verdict on a later page of comments still decides the badge" reds
$ grep -rlI $'\xef\xbf\xbd' . --exclude-dir=node_modules   # no file listed
$ node scripts/ds-bot-patches.mjs check            # ok 10 patches apply cleanly in order; ok core/ equals ds-bot/ + patches/ds-bot/ (pin skipped: no git here)
$ diff -rq <fc19e6d2 tree> <36ba6432 tree>         # the 6 files above, nothing else

CI at this tip: every check completed and green except enqueue-guard, which is red only for the absence of a conclusion naming this sha (judge shows a cancelled run superseded by a green one 5s later). kanban/delivery-view/build-client --check need esbuild, which this box lacks; CI covers them.

One non-blocking note, no new tip: a keyword list such as Fixes #12, #13 links only #12, while GitHub also closes #13. And the cost is documented but real: a thread of exactly 100/200/… comments costs up to MAX_PAGES conditional GETs for that one pull request.

@github-actions github-actions Bot added the pass label Oct 25, 2026
@Birfy
Birfy added this pull request to the merge queue Oct 25, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to failed status checks Oct 25, 2026
@Birfy
Birfy added this pull request to the merge queue Oct 25, 2026
Merged via the queue into main with commit 687f805 Oct 25, 2026
11 of 13 checks passed
@Birfy
Birfy deleted the dev2/fix/1189-delivery-board-links branch October 25, 2026 15:02
@Birfy Birfy removed the agent-pr Pull request opened by an agent label Oct 25, 2026
HugoXK pushed a commit to HugoXK/FlyBot that referenced this pull request Oct 25, 2026
…it (ThinkFlowLab#1192) (ThinkFlowLab#1196)

Owner: Dev2
Reviewer: Reviewer2
Tip SHA: b9f57ae
Base: 6d75f64
Fixes ThinkFlowLab#1012

## Reproduce

Same work as ThinkFlowLab#1192, filed 20 minutes earlier and closed as a duplicate
of this reopened issue: the branch, the tip and every number below are
unchanged.

The first merge_group entry of ThinkFlowLab#1190 (`66a6d9e5`) failed
`core/test/follow-through.test.js:153`, `a group turn cut off by a
restart is picked up once, and the group is told`, at `openRoomTurn` →
`settle` → `condition not reached`; the requeue merged as `687f805f`.
That case calls `mockTimers(t)`, so its waits take `waitFor`'s mocked
branch (`:120-124`), which was bounded by `rounds = 20000` alone while
the chain under it does real work.

Locally the same wait is measurable: under a pinned CPU with one hog and
`nice -n 19`, the case's waits need **1168 rounds** and **2.7 s** — one
round costs ~2.3 ms there, because the round is `tick(1)` +
`setImmediate` on a starved core. On an unloaded box the same waits use
8 rounds (p99) out of 20000. So the round count is a proxy for real time
that moves by an order of magnitude depending on why the machine is slow
— and the chain it waits on is real I/O either way.

## Measure before the change (main `687f805f`, `waitFor` instrumented to
print rounds + ms per wait)

| what | result |
|---|---|
| `follow-through.test.js` × 100, plain | **100/100 green**, 0 failures
|
| mocked-branch waits over those runs (2800 waits) | rounds p50 **2**,
p99 **8**, max **9** of 20000 |
| real-branch waits over those runs (900 waits) | ms p99 **304**, max
**305** |
| the failing case only, 10 runs pinned to one CPU with a hog, `nice -n
19` | 0 failures; mocked waits max **1168 rounds** / **2686 ms** |

Reading: the budget never ran out locally — a CPU-starved loop spends
its rounds slowly, which is the opposite direction. The merge_group
failure is the other direction: ThinkFlowLab#1014 measured 20000 rounds at ~70 ms of
real time, so a machine slow in **I/O** (the
`inflight.json`/`state.json` writes, the Session flush) burns the whole
budget while the work is still in flight. Both directions say the same
thing — rounds are not time — so (a), the shape this issue prescribes,
is the fix.

## What changed

`core/test/follow-through.test.js` +23/−1, and the generated
`patches/ds-bot/0001-core-fixes.patch` hunk that carries it +25/−3 (that
file is patch-tracked, not one of the mirror files).

- A mocked wait still calls `tick(1)` every round — only a tick moves
the mocked clock, and a `setTimeout` in front of it would decide a race
the test controls (ThinkFlowLab#821) — and now keeps going past `rounds` until
`Date.now() + REAL_WAIT_MS` (the deadline the real branch already used).
The round count is a floor under the budget, never a deadline;
`Date.now()` is not frozen by `mock.timers.enable({ apis: ['setTimeout']
})`.
- New case `a mocked-clock wait keeps a real-clock floor under its round
budget (ThinkFlowLab#1192)`: mocks the clock, starts a 100 ms **real** timer
(captured at import, before any case mocks the clock) and calls
`waitFor(() => landed, 1)`. One round is microseconds, so without the
floor it returns false long before the timer lands. Added wall time ~100
ms.

## What I ran

At the tip (`b9f57ae1`, in a tree rebuilt from this branch's tarball,
not my working copy):

- `node --test test/follow-through.test.js` — **19/19**, 0 fail, 1.28 s
- `node scripts/ds-bot-patches.mjs check` — `ok 10 patches apply cleanly
in order` / `ok core/ equals ds-bot/ + patches/ds-bot/` (pin skipped: no
git here)
- `oxlint -c core/.oxlintrc.json core/test/follow-through.test.js` — 0
warnings, 0 errors
- `node .github/scripts/testing-layers.mjs --check` — 392 test files,
every one classified
- `node scripts/build-client.mjs --check` (in `core/`) — exit 0
- loaded run 1 exit=0 secs=77 # pass 19 # fail 0 ; loaded run 2 exit=0
secs=79 # pass 19 # fail 0 ; loaded run 3 exit=0 secs=83 # pass 19 #
fail 0

## Red without the source change

Knife: in a copy of the tip, put the mocked loop back to `for (let i =
0; i < rounds && !check(); i++)` (the floor removed, `tick(1)` kept) →
**18/19, only** `a mocked-clock wait keeps a real-clock floor under its
round budget (ThinkFlowLab#1192)` red. Restore the floor → 19/19. No other case
moves, so the other 18 prove the floor did not change what they decide
(ThinkFlowLab#821's 3 s mocked delay still resolves by ticking).

## Not run

- The `merge_group` entry itself — that is the queue's run, after this
merges. A local `taskset` + hog cannot reproduce the I/O-slow / CPU-fast
shape of a loaded runner; the measurement above shows the opposite
direction and says so.
- No changelog fragment (test-only) and no subsystem doc (no owned
path).

## Risk

- A genuinely lost call now fails after **5 s** on the mocked branch
instead of ~70 ms, so a broken case is slower to fail, not quieter. The
wait still ends on its own; there is no unbounded loop.
- The floor only *adds* time to a wait that is already failing; every
passing wait in the 19 leaves the loop through `check()`.
- The patch hunk was regenerated by hand (this box has no `git`, so
`generate` refuses); `check` reproduces `core/` byte-for-byte from
`ds-bot/` + patches, which is the same proof it gives after a
`generate`.

Docs-not-needed: only `core/test/follow-through.test.js` and its
generated patch hunk change; no owned path, no behavior or contract.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Delivery board: linkedTo treats any #12 in a PR body as a link, and verdictOf only reads the first 100 comments

1 participant