Trace a PR back to its session¶
Someone asks "who wrote this, and why?" about a merged pull request. Find out
which of your AI teammates worked on it, what each one recorded doing (opening
it, pushing to it, reviewing it, merging it), and which worker sessions those
records came from. Ask your agent, or look it up in the portal's Worklog. Naming
the person who drove a colleague's session, and the model it ran on, needs the
hadron CLI or the API. The portal and your agent show a colleague's
session id, but neither can open the session yet.
This is the question the whole Worker model exists to answer. It works on a team App whose workers recorded their work as it happened (see Set up an AI team). The worklog is append-only, so anything recorded is still there.
Ask your agent¶
What you need: an agent connected to Hadron, and access to the team App's worklog: membership of the App, Contributor or above on its organization, ownership of a user-owned App, or the App's own key (the full rule). Name the team and the pull request:
In our Hadron Dev Team, which workers worked on
hadron-memory/hadron-server#1362? For each one, tell me what it recorded
doing (opened, pushed, reviewed, merged), when it was recorded, and which
worker session the record came from. If there are more records than fit in one answer, fetch
them all.
What you get back: one line per recorded milestone, each with its worker, its action, when it was recorded, and its worker session. That time is when the record was made, which can be later than the work itself: a milestone can be recorded after the fact. For when a pull request was actually opened or merged, look at GitHub. A pull request worked across several sessions lists every one of them, which is correct. The worklog returns 50 records at a time and says how many there are in total; if the agent reports fewer than the total, ask it for the rest.
What to check before you rely on it:
- What the records can and can't prove. They are milestones each worker
logged, not commit authorship: GitHub's commit history is the record of
who wrote which lines. Every record against the pull request comes back, so
a worker who only reviewed or merged it appears next to the ones who built
it. The action tells them apart:
openedmarks who opened the pull request,pushedmarks a worker that pushed commits to it (possibly not the one who opened it), andreviewedandmergedare neither. Ask the agent to show the actions if it doesn't. - A teammate, or a person. A milestone logged from a session that wasn't bound to a worker still comes back, but the name on it is the person's handle, not a teammate's. The portal's Worklog marks such a row as not attributed to a worker. If a name in the answer isn't one of your teammates, ask the agent whether that record has a worker at all.
- A pull request, not an issue with the same number. GitHub numbers pull requests and issues from one sequence. Say "pull request" and ask the agent to look only at pull requests if the answer mentions an issue.
- What it can't tell you. Your agent can say that you drove a session only when that session is among your open or recently ended ones, which is all it can see of your own. For an older session, or anyone else's, it can't name the person or the model. That needs the CLI or the API.
In the portal¶
- Open the portal, go to Apps, and open your team's App.
- Choose the Worklog tab.
- Paste the pull request into Artifact, as
owner/repo#123or as its GitHub URL. Set Kind toprand choose Filter.
Each row is one recorded milestone, with columns When (when it was recorded), Worker, Action, Kind, Artifact, Tool and Worker session. When several worker sessions recorded work on the same artifact, each record is still its own row, and the Artifact cell also shows a badge such as 3 worker sessions; choosing it filters the worklog to that artifact. Compare the Worker and Worker session columns across those rows. The filter lives in the page address, so you can send the link to whoever asked.
The Worker session column shows the session's id, even for a session you can't open. A worker's own page lists only the sessions you drove as that worker, so a colleague's session isn't shown there.
The whole chain, with the CLI or the API¶
The rest of this page follows the chain to its end: pull request, worklog, session, person and transcript.
Step 1 — Ask the worklog who produced the artifact¶
Spelling does not matter; the artifact KIND does. A PR can be written as
a GitHub URL or as owner/repo#N — both normalize to the same record, so
https://github.com/hadron-memory/hadron-docs/pull/271 and
hadron-memory/hadron-docs#271 are interchangeable.
owner/repo@sha and owner/repo:branch are different artifacts, not other
ways of writing a PR. On the CLI each kind has its own flag — --commit,
--branch, --issue — and passing a commit ref to --pr finds nothing.
And owner/repo#N does not distinguish a PR from an issue. PRs and issues
share GitHub's number space, so they share a canonical spelling and a ref-only
lookup finds both. The CLI's --pr
already says which you mean; on the other two surfaces, pass kind: "pr" —
otherwise issue #271 comes back alongside PR #271.
Several rows are expected and correct. A PR spanning three sessions yields three transcripts. A recorded session you cannot read appears as an id-only stub rather than vanishing, so the count never silently understates the work.
But the rows answer "who touched this", not "who wrote it". Every record
against the ref comes back — opened, reviewed, merged, closed — so a
worker who only merged the PR appears beside the one who wrote it. The
distinguishing field is action, which the MCP and GraphQL forms show and the
CLI's human table does not: reach for --json when the difference matters.
Step 2 — Read the session for the rest¶
Step 1 gives you a session id. Turning that into the rest of the provenance
takes the CLI or the GraphQL API — --json on the command above, or:
query($id: ID!) {
session(id: $id) {
workerName workerRole userId
repo branch prNumber startedAt
host tool transcriptPath llmModel
}
}
MCP cannot read an arbitrary session
There is no MCP tool for it. hadron_whoami returns your own sessions,
so it cannot open a colleague's. An MCP-only host can do step 1 — which
worker, which session — but not step 2, and so not step 2b either: naming
the person needs the userId that only step 2 returns. Both need the CLI
or the API. That is why the page carries mcp and
portal badges for the worklog lookup, but its full chain runs on cli
and api.
--json gives you everything the bind recorded:
{
"workerName": "Tove",
"workerRole": "docs-engineer",
"userId": "019d28f166d979d09438cd92e832194c",
"repo": "hadron-memory/hadron-docs",
"branch": "main",
"prNumber": 271,
"startedAt": "2026-08-25T08:29:43.359Z",
"host": null,
"tool": null,
"transcriptPath": null,
"llmModel": "claude-opus-5"
}
That chain — PR → worklog → session → transcript — is the provenance the
Worker: commit trailer promises. How much of its last link you actually get
depends on what the bind recorded, which is the next section.
Step 2b — Turn userId into a person¶
userId is an opaque id, and "who drove it" is usually the point. Once step 2
has given you the userId, resolve it on either surface:
user(ref:) takes the id or the URN, so this is also the step that turns a
hrn:user:… you already have into a display name.
The provenance you get out is the provenance that went in¶
Look again at the sample above: tool, host and transcriptPath are
null. Nothing is broken. That session was bound from an MCP host, where
those are not passed, so they were never recorded.
None of them are backfilled, and none can be reconstructed later. If you want a transcript path in the answer, it has to be supplied at bind time:
hadron team session start --as Tove --tool claude-code \
--host "$(hostname)" --transcript ~/.claude/projects/…/session.jsonl
So the useful habit is to decide before a stint what the person asking in six months will need, rather than discovering the gap while answering them.
The other direction — everything one worker has done¶
The same worklog answers the reverse question, which is often the one you actually have:
11 record(s) total, showing 8:
2026-08-26T10:16:40.563Z [Tove] pr opened hadron-memory/hadron-docs#271
2026-08-26T09:55:22.684Z [Tove] pr merged hadron-memory/hadron-docs#270
2026-08-26T09:35:36.593Z [Tove] pr opened hadron-memory/hadron-docs#270
…
A retired worker still resolves: retirement ends the casting, not the history.
Mind the envelope. The sample says 11 records total, showing 8 — the
response is a page, not the set. total is the number to compare against, and
limit / offset are how you walk the rest. "Everything one worker has done"
is what the query means, not what one call returns.
Each step is gated more narrowly than the last¶
The three lookups do not share one permission, so a chain that starts fine can stop partway — and it stops by returning less, not by refusing:
| Step | Who can do it |
|---|---|
| 1 — the worklog | Any AppMember (any role), an org CONTRIBUTOR+, a user-owned App's owner, or the App's own key |
| 2 — read the session | The attributed driver, a member of the owning org, the session's App, or a platform admin |
| 2b — name the person | A caller sharing an organization with them, or a platform admin — anyone else gets null |
So an AppMember who is neither the driver nor in the owning org sees the work
happened and gets the id-only stub described above — that stub is this gate,
not a missing record. And an org member tracing a PR driven by an external
App member can open the session but cannot resolve the userId.
Neither is a bug to route around. If you need the answer and the gate says no, the route is to ask someone who has it, not to widen your own access.
Why Session.prNumber is not the index¶
A session row carries a prNumber, and it is tempting to query on it. Don't.
It is a denormalized display field — the convenience that lets a session
list show a PR number without a join. Each pr record overwrites it, so it
holds the most recent PR that session touched, not all of them. In the JSON
above it reads 271 because that was the last one; the same session also
produced #265, #266, #267, #268 and #270, and only the worklog knows that.
The worklog is the many-to-many join.
prNumberis a label on one end of it.
Related¶
- Teams, workers, and sessions — why a worker is a casting, what a session records, and why the worklog is a separate index rather than a column.
- Set up an AI team — getting to the point where there is a worklog to query.
hadronCLI reference —session log,session list, and the flags in full.- MCP tools —
hadron_record_workandhadron_team_work_items.