DesignOperations Dashboard2026-08-20

The Merges Tab

A fifth tab that answers one question — what did the agents actually change today, and where is it going — by joining three things the program already records: the agent id, the pull request it merged, and the branch role that decides whether the change ships or waits.

This is a design, not a build. Nothing below is running. The numbers in the mock are tonight's real numbers, so what you are reacting to is true rather than illustrative.

Asked for by @jwildfire, 2026-08-20, after the day's briefing Worker W0094 Would live at 127.0.0.1:7326/merges
There's no really clean way for me to know exactly what happened… maybe we just need to work on having a view in the dashboard that shows me metrics for merges… very directly tying agent IDs to pull requests to packages… I need a better executive summary of everything you did today where I can drill down easily, but I can also just understand, okay, you made a bunch of changes to these approved branches, and that'll all go out in the scheduled release.
What the tab would have told you tonight

Twenty merges. Nineteen of them changed the machine you use to run the work; one was your own blog post. Not one of them touched a package that reaches a user, and none of the five product repos has moved in three days. The scheduled release this feeds is the harness release, not a product release.

THE MOCK

The view, as it would have rendered tonight

Everything inside the frame below is the proposed tab. Every number in it was measured from GitHub and the worker ledger at 20:30 local on 2026-08-20. The design commentary starts again after the frame.

QueueWireAgentsCatalog ↗·NavigatorMerges

Thursday 20 August — 20 merges

Day 2026-08-20 00:00–24:00 America/New York  ·  Source GitHub, read 20:31 local  ·  Covers the whole day so far

Harness & hub
19

obot.agent 17 · obot.roadmap 2. Self-improvement. Reaches you, not users.

Ships to users
0

safety.viz, gsm.safety, open.gismo, open.csr, demo-301. Last product merge 3 days ago.

Outside the lanes
1

jwildfire.github.io, merged by you. No policy entry, so no lane governs it.

The headline

Nothing shipped to users today.

Eleven hub issues closed — nine of them requirements — and 15,350 lines added, all of it in the harness and the hub. The five repos that produce something a user opens took zero merges, and their release queues are unchanged since Monday.

Where it is going

Branch roles are read from obot.agent/scripts/policy.json, never restated here. A merge into a repo's integration branch joins the queue for that repo's next release; a merge into a release branch is one you gated yourself.

RepoTodayLanded onWhat happens to itWaiting for release
obot.agent
harness
17 main
integration
Accumulates on main until you approve a main → stable promotion. That is the scheduled release. 98 PRs
since v0.4.0, 5 days ago
obot.roadmap
hub
2 main
integration
No release branch is declared, so these are live the moment they merge. Already published.
nothing to wait for
safety.viz
ships to users
0 Work would land on dev; main is yours to gate. 0 PRs
v1.7.0 is current
gsm.safety
ships to users
0 Work would land on dev; main is yours to gate. 0 PRs
v1.1.0 is current
open.gismo
ships to users
0 Work would land on dev; main is yours to gate. 7 PRs
never released
open.csr
ships to users
0 Work would land on dev; main is yours to gate. 2 PRs
since v0.2.0, 24 days ago
demo-301
ships to users
0 Work lands on main; site is the live Pages branch and is yours to gate. 2 PRs
no release ever cut
jwildfire.github.io
no policy entry
1 main
ungoverned
Absent from policy.json, so obot-merge would have refused it. You merged it by hand.
no roles defined

Who did it

Eleven agents merged 15 of tonight's 20. The other five have a named reason, and the reason is not the same for all of them — which is the whole point of splitting them out rather than calling them all unattributed.

AgentWhat it was working onMergedIntoSize
W0077
rankhead
Build the ranked-head surface (hub #278)agent#269obot.agent main+2011 −4
14 files
W0078
spend
Spend cadence + nightly threshold enforcement (hub #275)agent#270 agent#271obot.agent main+1978 −11
12 files
W0079
protect
Branch-protection state, spec, apply/verify script, decision artifact (hub #272)agent#267obot.agent main+815 −0
6 files
W0080
delivery
hub#257 completion has a delivery path that ends at himagent#274 agent#275obot.agent main+2534 −25
19 files
W0082
answerlands
not recorded in the ledgeragent#278obot.agent main+641 −29
9 files
W0083
carvoice
hub#265 answer a decision by voice from the caragent#279 agent#280obot.agent main+3899 −86
40 files
W0085
censusdesign
not recorded in the ledgerroadmap#286obot.roadmap main+484 −0
4 files
W0086
deployred
not recorded in the ledgerroadmap#288obot.roadmap main+101 −21
4 files
W0087
decep
hub#280 an open decision artifact has an episode he can answer from the caragent#282obot.agent main+1094 −12
12 files
W0090
cfgaudit
audit the 13 config items; fold migration-covered ones into the IQagent#287 agent#288obot.agent main+561 −43
13 files
W0092
sharedcss
shared stylesheet (oa#15) + dashboard shows artifacts by defaultagent#285obot.agent main+856 −93
8 files
Lead session
holds no W-id
Ranked-head re-ranks. prime and the Navigator claim no worker id by design, so there is none to stamp — this is not a gap.agent#272 agent#281 agent#283 agent#284obot.agent main+94 −52
5 files
@jwildfire
not an agent
Merged by hand, in a repo no lane governs.jwildfire.github.io#11jwildfire.github.io main+282 −8
2 files
Attribution missing
the #276 lane
A merge from a session that does hold a W-id, whose commit carries no Worker: trailer. This is the only row that would cite obot.agent#276.none today
Every merge, in order — 20 rows
LocalMergeAgentIntoRoleSize
07:41jwildfire.github.io#11
Diary #8: a second interview with Obot
@jwildfirejwildfire.github.io
main
ungoverned+282 −8
12:32agent#267
Branch protection becomes a spec, a plan and a verifier
W0079obot.agent
main
integration+815 −0
12:39agent#269
The ranked head: one declared order, everything else derived
W0077obot.agent
main
integration+2011 −4
13:00agent#270
What the fleet spends is measured before it is spent
W0078obot.agent
main
integration+1975 −9
13:03agent#271
The spend cap's scratchpad lines were wearing the Navigator tag twice
W0078obot.agent
main
integration+3 −2
13:07agent#272
The first re-rank, and the test that would not let it happen
Lead sessionobot.agent
main
integration+77 −35
13:36agent#274
A finish reaches a person, as a sentence rather than a number
W0080obot.agent
main
integration+2458 −20
13:48agent#275
The landing check is bounded in wall-clock, not only in count
W0080obot.agent
main
integration+76 −5
14:49agent#278
An unapplied answer of his reaches an actor, not a file
W0082obot.agent
main
integration+641 −29
15:43agent#279
He answers a decision from the car, by voice, with no screen and no number
W0083obot.agent
main
integration+2765 −11
16:13agent#280
Four ways the car lane could have answered the wrong decision, closed
W0083obot.agent
main
integration+1134 −75
17:02roadmap#286
D0023: the SafetyCensus rebuild design — metrics, a report, and six questions
W0085obot.roadmap
main
integration+484 −0
17:23roadmap#288
The briefing's count is handed in, so no committed file can decide whether its test runs
W0086obot.roadmap
main
integration+101 −21
17:31agent#281
Three slots filled — the head after a day that closed seven requirements
Lead sessionobot.agent
main
integration+6 −6
18:07agent#282
An open decision has an episode he can answer from the car
W0087obot.agent
main
integration+1094 −12
18:14agent#283
#267 to rank 3, and a bench that is a shelf again rather than a drawer
Lead sessionobot.agent
main
integration+2 −2
19:51agent#284
The laptop IQ to rank 1, a promise kept, and the stylesheet onto the head
Lead sessionobot.agent
main
integration+9 −9
20:06agent#285
One stylesheet for the decision language, and the dashboard wearing it
W0092obot.agent
main
integration+856 −93
20:10agent#287
Config items lead with why they matter, not with what to type
W0090obot.agent
main
integration+560 −42
20:12agent#288
Rename the posted PR draft to its number
W0090obot.agent
main
integration+1 −1

Each row opens the pull request. Times are local. integration flows into a scheduled release; release is a branch you gate; ungoverned has no lane at all.

End of mock. Design commentary resumes below.

THE CALLS

Four decisions the design makes, and why

Three of these you named. The fourth turned up while measuring, and it is the one that would have embarrassed the tab on its first day.

Call 1 — attribution

A merge with no agent id is sorted by cause, never pooled into one “unattributed” bucket

The brief said the agent → PR join is broken, so the tab must not silently drop what it cannot attribute. Measuring it changed the answer, and the change is worth stating plainly rather than designing around a defect that is not there.

The join through git history works. Across the last 30 merges to obot.agent main, 26 carry a Worker: W00xx trailer in the merge commit, and among merges made by a worker the rate is 26 of 26, unbroken since 2026-08-18. It works because an agent types the trailer into the commit, so nothing about it depends on an environment variable surviving a daemon respawn.

The four that carry no trailer are the ranked-head re-ranks, made by prime and the Navigator. Those actors hold no worker id by design — tools/worker-id says so in its own docstring — so they have nothing to stamp. Filing them as unattributed would invent a defect and, worse, would cite obot.agent#276 as its cause. #276 is real, but it is about a different join: OBOT_WORKER_ID is never set, so ledger writes record a session id. That breaks worker → blocker and worker → journal. It does not touch the commit trailer.

So the tab carries three non-worker lanes instead of one:

Lead session — holds no W-id
prime, the Navigator, the admiral. Expected, permanent, and labelled as such. Four rows tonight.
Not an agent
A human merge. One row tonight, and it is yours.
Attribution missing — the #276 lane
A merge from a session that does hold a worker id, whose commit carries no trailer. This is the only lane that cites #276, and the only one that means something is wrong. It reads zero tonight, and a zero here is a real fact rather than an empty bucket.

The header line never presents attributed counts as a total: it reads 20 merges — 15 by 11 agents, 4 by the lead session, 1 by you, and those five numbers add up in front of you.

One thing this lane cannot see yet

worker-id --audit reports today that W0067 is carried by two different workers — a lead put an id into a session name it had not claimed. A lane keyed on the worker id fuses two agents into one row and shows a drill-down that mixes their work. The tab should run the same read-only audit at render and give a conflicted id its own flagged row rather than a clean one.

Call 2 — the window

A day is midnight to midnight in America/New York, and the page says so on its face

You ask what happened today while sitting in that timezone, so that is what the day means. It is not a cosmetic choice. Three of tonight's 17 obot.agent merges — 20:06, 20:10 and 20:12 local — carry a UTC date of 2026-08-21. A UTC day would have shown you 14 and been quietly, confidently wrong by the three most recent things that happened.

That is not hypothetical: the existing Navigator panels bucket by UTC, using new Date(t).toISOString().slice(0,10). So the tab has an obligation beyond stating its own rule — it has to state the disagreement, because the same merge will sit on a different day one tab over. The header line carries both: the local day it is showing, and a note that three of tonight's merges appear on the Navigator tab under tomorrow's date.

Two mechanics follow from the call. The zone is pinned as the IANA name through Intl.DateTimeFormat rather than a hardcoded offset, so 2026-11-01 does not silently shift every boundary by an hour. And the header renders from the same value the bucketer used, so the line cannot describe a rule the code is not applying.

The freshness trap behind the timezone question

The timezone is the visible half of this problem. The invisible half is worse, and it would have produced the identical wrong number by a different route.

The cache the Navigator panels read, .claude/session-hub/cache/metrics.json, was last written at 19:47 local tonight. The three merges at 20:06, 20:10 and 20:12 are not in it. A tab built on that cache would have shown 14 — not because of UTC, but because it was reading a file older than the day it was describing.

So the design's rule is that the page prints its as-of stamp and refuses rather than rounds: when the source is older than the boundary of the day it is showing, it says how much of the day it cannot see instead of printing a count that looks whole.

Call 3 — scope

It reports. It never asks you for anything.

You have three buckets that can demand action — release candidates, decisions, config — and a fourth would cost more than it returns. Nothing on this tab is a checkbox, an approve button, or a row that stays red until you clear it. Every link is a drill-down to a pull request or an issue; none of them is a “do”.

That has a consequence worth accepting on purpose. The tab will surface things that look like they need doing — 98 pull requests waiting on a harness release, a product repo that has not moved in three days, a merge in a repo no lane governs. It does not adopt them. Each such row cites the queue item or requirement that already owns it; a row with nothing to cite says so, and an unowned row is a filing gap for the Navigator to pick up, not a button here.

One honest qualification, because “read-only” will otherwise read as a safety property it does not have. The dashboard writes a local last-look record on every page fetch. That is unchanged for this tab, and it means the drill-down must vary by query string rather than by path — per-agent and per-day URLs as new path segments would generate unbounded surface keys and evict real surfaces from a 64-entry record. Read-only here means no GitHub writes and nothing entering your queue.

Call 4 — the denominator

The tab counts every merge in the workspace, not only the governed ones

This is the call the brief did not ask for, and the one that decides whether the summary is trustworthy. The obvious implementation scopes to the seven repos in policy.json. Tonight that would have printed 19 and been wrong by exactly the merge that most deserved your attention: a merge into jwildfire.github.io, a repo absent from the policy file, which obot-merge would have refused outright.

Scoping to the governed set institutionalises that blind spot. Scoping to “repos cloned on this machine” drifts as clones come and go. So the tab states its denominator out loud — every repo in the jwildfire org — and renders an ungoverned merge as its own state, in its own colour, rather than as an omission.

Three related edges get the same treatment, because each has a shape where a zero would be a lie:

obot.roadmap declares no release branch
Its integration-versus-release delta is undefined, not zero. The cell reads “nothing to wait for”, because the hub publishes on merge.
demo-301's site branch shares no ancestor with main
The comparison returns a 404, permanently. That is neither an error to log nor a zero to print; the cell says the two branches are not comparable.
Zero merges and unread source must never render alike
A quiet Sunday and a dead collector both produce an empty page. Only one of them is news. The house rule already covers it — absence is believable only from a genuine not-found — and this tab inherits it.
A unit trap this design deliberately avoids

The tempting number for “how much is waiting for the harness release” is the branch delta: main is 193 commits ahead of stable. Putting 193 next to today's 17 puts commits next to pull requests in adjacent columns, which invites a subtraction that means nothing.

Worse, the raw delta is not the measure it appears to be. On safety.viz, dev is 0 ahead of main and 12 behind — which looks like an inverted branch model and is not: those 12 are the promotion merge commits themselves, which never travel back to dev. Reading them as a backlog would raise a false alarm on a healthy repo.

So the queue is measured in the unit the release is cut in: pull requests merged into the integration branch since the last release tag. That is 98 for obot.agent, 0 for safety.viz, and 7 for open.gismo, which has never cut a release at all.

WHAT IT WOULD TAKE

The one thing that has to exist first

The tab is a small amount of work sitting on top of a gap. Being specific about the gap is more useful than estimating the tab.

No cache in the program can answer this question today

The Navigator's metrics cache stores each pull request as {repo, number, createdAt, lane, state}. The merge timestamp is collapsed into the string state: 'merged' and discarded; the target branch is read to compute a lane and then thrown away.

So there is no mergedAt and no baseRefName anywhere on disk. Not a timezone problem — the data required to bucket a merge into any day at all is not collected.

Two fields on the collector close it, and they are additive rather than a migration: keep mergedAt and baseRefName on each pull-request record. The branch role, the class, and the release lane are all derivable from policy.json once the base branch is known, so nothing else needs storing and nothing gets restated.

After that the tab is one entry in the dashboard's TABS list and one route, which is the shape the dashboard was built for. It goes after the divider, beside Navigator, because it is a local-only view and the spine is asserted against the hub's sub-nav.

What it reuses rather than reinvents

A weak join worth knowing about before you rely on it

Agent → pull request is solid. Agent → requirement is not. The worker ledger records a free-text task for most claims, but three of tonight's eleven agents have no task recorded at all, and pull-request bodies cite requirement numbers for context as often as for ownership — one of tonight's PRs names four hub issues and is delivering one of them.

The tab therefore shows the requirement where the ledger records one and says “not recorded” where it does not. It does not guess from the body text, because a plausible wrong requirement is worse here than a blank.

YOUR CALL

Four questions, each with a recommendation

Q1

Is a fifth tab the right home, or should this replace something?

The Navigator tab already carries release metrics and a change feed, and you have said before that it read like an audit log for bots. This tab overlaps it: same data, different question.

Recommendation: a separate tab. The Navigator tab answers “how is the program trending” over 1/3/7/30/365 days; this one answers “what happened today”. Merging them would put a trend chart above a daily summary and neither would be first.
Q2

Should a day with zero product merges be visually loud, or just accurate?

The mock renders the product lane in the alarm colour whenever it reads zero. That is honest tonight. It would also be red on any weekend, on any day the work is deliberately harness-first, and for most of the last week.

Recommendation: colour it by streak, not by the single day. A zero after a product merge yesterday is normal and stays neutral; a zero on the fifth consecutive day is the thing worth seeing, and the lane says “fifth day” rather than just being red.
Q3

Do you want this as a page you visit, or as something that reaches you?

The design as written is a page. The same summary would fit the evening briefing episode you already listen to, and the headline sentence at the top of this mock is roughly one paragraph of audio.

Recommendation: build the page first, then let the briefing read its headline block. One source, two surfaces — and the page stays the thing you drill into when the sentence makes you curious.
Q4

Should the ungoverned lane push toward adding those repos to policy.json?

Tonight's ungoverned merge is your own blog post, which is benign. But the lane will also catch anything an agent lands somewhere no lane covers, and that is exactly what it is for.

Recommendation: leave the tab reporting only. If a repo shows up in that lane repeatedly, that is a requirement to file, not a prompt for this page to raise — consistent with Call 3.

Measured 2026-08-20 between 20:15 and 21:00 local from the GitHub API, obot.agent/scripts/policy.json, and .claude/workers.journal. Every count in the mock is reproducible from those three sources. The mock is a static rendering: nothing on this page is live, and no such tab exists yet.

Corrections this design absorbed while being written: the commit-trailer join was assumed broken and is not; the four untrailered merges were assumed to be an attribution failure and are the lead session; safety.viz's branch delta looked like an inverted branch model and is the normal promotion-commit residue.


Drafted by worker W0094 (Claude Code using Opus 5) for @jwildfire.