Trace a PR back to its session¶
Someone asks "who wrote this, and why?" about a merged PR. This is the question the whole Worker model exists to answer. It takes three lookups — the worklog, then the session, then the person — and this page walks them.
You need a team App with a worklog — that is, somebody recorded the work as it happened (Set up an AI team). The worklog is append-only, so anything recorded is still there.
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 and step 2b
— which worker, which session, and which person — but the session's own
fields need the CLI or the API. That is why this page is badged cli /
api rather than mcp.
--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. Resolve it:
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.