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.
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.
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.
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.
Day 2026-08-20 00:00–24:00 America/New York · Source GitHub, read 20:31 local · Covers the whole day so far
obot.agent 17 · obot.roadmap 2. Self-improvement. Reaches you, not users.
safety.viz, gsm.safety, open.gismo, open.csr, demo-301. Last product merge 3 days ago.
jwildfire.github.io, merged by you. No policy entry, so no lane governs it.
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.
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.
| Repo | Today | Landed on | What happens to it | Waiting 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 |
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.
| Agent | What it was working on | Merged | Into | Size |
|---|---|---|---|---|
| W0077 rankhead | Build the ranked-head surface (hub #278) | agent#269 | obot.agent main | +2011 −4 14 files |
| W0078 spend | Spend cadence + nightly threshold enforcement (hub #275) | agent#270 agent#271 | obot.agent main | +1978 −11 12 files |
| W0079 protect | Branch-protection state, spec, apply/verify script, decision artifact (hub #272) | agent#267 | obot.agent main | +815 −0 6 files |
| W0080 delivery | hub#257 completion has a delivery path that ends at him | agent#274 agent#275 | obot.agent main | +2534 −25 19 files |
| W0082 answerlands | not recorded in the ledger | agent#278 | obot.agent main | +641 −29 9 files |
| W0083 carvoice | hub#265 answer a decision by voice from the car | agent#279 agent#280 | obot.agent main | +3899 −86 40 files |
| W0085 censusdesign | not recorded in the ledger | roadmap#286 | obot.roadmap main | +484 −0 4 files |
| W0086 deployred | not recorded in the ledger | roadmap#288 | obot.roadmap main | +101 −21 4 files |
| W0087 decep | hub#280 an open decision artifact has an episode he can answer from the car | agent#282 | obot.agent main | +1094 −12 12 files |
| W0090 cfgaudit | audit the 13 config items; fold migration-covered ones into the IQ | agent#287 agent#288 | obot.agent main | +561 −43 13 files |
| W0092 sharedcss | shared stylesheet (oa#15) + dashboard shows artifacts by default | agent#285 | obot.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#284 | obot.agent main | +94 −52 5 files |
| @jwildfire not an agent | Merged by hand, in a repo no lane governs. | jwildfire.github.io#11 | jwildfire.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 | — | — |
| Local | Merge | Agent | Into | Role | Size |
|---|---|---|---|---|---|
| 07:41 | jwildfire.github.io#11 Diary #8: a second interview with Obot | @jwildfire | jwildfire.github.io main | ungoverned | +282 −8 |
| 12:32 | agent#267 Branch protection becomes a spec, a plan and a verifier | W0079 | obot.agent main | integration | +815 −0 |
| 12:39 | agent#269 The ranked head: one declared order, everything else derived | W0077 | obot.agent main | integration | +2011 −4 |
| 13:00 | agent#270 What the fleet spends is measured before it is spent | W0078 | obot.agent main | integration | +1975 −9 |
| 13:03 | agent#271 The spend cap's scratchpad lines were wearing the Navigator tag twice | W0078 | obot.agent main | integration | +3 −2 |
| 13:07 | agent#272 The first re-rank, and the test that would not let it happen | Lead session | obot.agent main | integration | +77 −35 |
| 13:36 | agent#274 A finish reaches a person, as a sentence rather than a number | W0080 | obot.agent main | integration | +2458 −20 |
| 13:48 | agent#275 The landing check is bounded in wall-clock, not only in count | W0080 | obot.agent main | integration | +76 −5 |
| 14:49 | agent#278 An unapplied answer of his reaches an actor, not a file | W0082 | obot.agent main | integration | +641 −29 |
| 15:43 | agent#279 He answers a decision from the car, by voice, with no screen and no number | W0083 | obot.agent main | integration | +2765 −11 |
| 16:13 | agent#280 Four ways the car lane could have answered the wrong decision, closed | W0083 | obot.agent main | integration | +1134 −75 |
| 17:02 | roadmap#286 D0023: the SafetyCensus rebuild design — metrics, a report, and six questions | W0085 | obot.roadmap main | integration | +484 −0 |
| 17:23 | roadmap#288 The briefing's count is handed in, so no committed file can decide whether its test runs | W0086 | obot.roadmap main | integration | +101 −21 |
| 17:31 | agent#281 Three slots filled — the head after a day that closed seven requirements | Lead session | obot.agent main | integration | +6 −6 |
| 18:07 | agent#282 An open decision has an episode he can answer from the car | W0087 | obot.agent main | integration | +1094 −12 |
| 18:14 | agent#283 #267 to rank 3, and a bench that is a shelf again rather than a drawer | Lead session | obot.agent main | integration | +2 −2 |
| 19:51 | agent#284 The laptop IQ to rank 1, a promise kept, and the stylesheet onto the head | Lead session | obot.agent main | integration | +9 −9 |
| 20:06 | agent#285 One stylesheet for the decision language, and the dashboard wearing it | W0092 | obot.agent main | integration | +856 −93 |
| 20:10 | agent#287 Config items lead with why they matter, not with what to type | W0090 | obot.agent main | integration | +560 −42 |
| 20:12 | agent#288 Rename the posted PR draft to its number | W0090 | obot.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.
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.
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:
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.
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.
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 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.
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.
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:
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.
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.
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.
policy.json, read rather than restated, so a repo promoted or demoted tomorrow changes this tab with no edit.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.
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.
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.
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.
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.
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.