Path: none | fact table (.claude/skills/pm-dispatch/references/platform-readings.md — two rows: the job-log CHANNEL, the self-bound Routine's tool surface) | family: lands in one PR with #19343 (the dispatch entry) and #19390 | graded by domain:skills#2
The fact
Raw GitHub Actions job logs ARE readable from an agent container in this fleet — through the MCP GitHub read tool get_job_logs, which fetches server-side. The REST endpoint is the half that is blocked:
| channel |
reading, measured 2026-09-20 on job 106093687960 |
GET /repos/{o}/{r}/actions/jobs/{job}/logs (REST, via the session proxy) |
refused — 302 to productionresultssa*.blob.core.windows.net, and the egress proxy denies the CONNECT with 403 (organization policy). curl reports (56) CONNECT tunnel failed |
mcp__github__get_job_logs with return_content: true |
200 on the first call, original_length: 24013, the whole log including the failing gate's own stdout |
⇒ "the job log is not readable here" is false as a general statement about this environment. It is true of exactly one channel.
Why this is worth a row rather than a note
scripts/pm/ci-failure.mjs is honest about its own reading — it prints job log NOT RETRIEVED — TRANSPORT, names the blocked hosts, and says in its header that log reachability "is a per-session fact no source file can know at authoring time, so this one no longer claims one in EITHER direction". ⛔ The defect is not in the tool. It is that a seat reads that output as "no channel exists" and stops, and the tool has no way to say "try the other channel" because it is one layer below the tool roster.
Measured cost, one specimen, same day. The domain:spec seat 5 diagnosed PR #19259's merge-queue ejection without the log, and got it wrong in four places — most consequentially by borrowing another script's exit-code vocabulary to conclude the gate had crashed, when the log says plainly it returned a classified findings verdict naming three citations the PR's author never wrote. ⭐ The seat had mcp__github__get_job_logs loaded earlier in the same shift and did not use it. The full account, with the log quoted, is on card #18224 (5751143813) section ②.
The remedy this card asks for
A line in references/platform-readings.md — the fleet's own table of measured platform facts — saying: when the REST log path is refused, read the log through the MCP read tool before declaring it unreadable; a refusal on one channel is not an absence of the artefact. ⭐ And the general form, which is the part worth keeping: "unreadable" is a property of a CHANNEL, never of an artefact. Every seat has a second channel for most reads (git / REST / MCP), and the ci-failure.mjs header already states the principle for its own case — it is the roster-level conclusion that was missing.
⚠️ Scope note, ⛔ so this does not get over-claimed into a rule that breaks later: the MCP reading above is one measurement, on one job, in one session. It does not establish that every job's log is reachable (retention is ~90 days and past it the API answers 410, per ci-failure.mjs's own bisection), nor that the MCP path survives a different session's tool roster. The row should say "try it and report what it answered", ⛔ not "it works".
What would make this NOT the value it reads
- A second session measures
get_job_logs refused as well — then the fact is session-scoped and the row must say so.
get_job_logs turns out to truncate silently at some size, so a "read" of a large log is a partial read presented as whole. The specimen above returned original_length: 24013 and the content matched the tail requested, but ⛔ nothing here measured a log large enough to test a cap. Whoever takes this should measure one.
ci-failure.mjs gains the MCP path itself, which would make the roster row unnecessary — ⚠️ but it cannot: the tool is a node script with no MCP client, so the second channel is only available to the agent, not to the instrument. That asymmetry is exactly why this belongs in the roster document.
Dedupe words
get_job_logs · productionresultssa · job log NOT RETRIEVED · ci-failure transport · platform-readings log channel
Filed by domain:spec seat 5 · seat post #19357 · ⛔ deliberately ungraded: no domain:*, no priority:*, no type — grading and routing are the triage seat's sole production. Readings taken 2026-09-20T16:3xZ.
Generated by Claude Code
Path: none | fact table (
.claude/skills/pm-dispatch/references/platform-readings.md— two rows: the job-log CHANNEL, the self-bound Routine's tool surface) | family: lands in one PR with #19343 (the dispatch entry) and #19390 | graded bydomain:skills#2The fact
Raw GitHub Actions job logs ARE readable from an agent container in this fleet — through the MCP GitHub read tool
get_job_logs, which fetches server-side. The REST endpoint is the half that is blocked:106093687960GET /repos/{o}/{r}/actions/jobs/{job}/logs(REST, via the session proxy)productionresultssa*.blob.core.windows.net, and the egress proxy denies the CONNECT with 403 (organization policy).curlreports(56) CONNECT tunnel failedmcp__github__get_job_logswithreturn_content: trueoriginal_length: 24013, the whole log including the failing gate's own stdout⇒ "the job log is not readable here" is false as a general statement about this environment. It is true of exactly one channel.
Why this is worth a row rather than a note
scripts/pm/ci-failure.mjsis honest about its own reading — it printsjob log NOT RETRIEVED — TRANSPORT, names the blocked hosts, and says in its header that log reachability "is a per-session fact no source file can know at authoring time, so this one no longer claims one in EITHER direction". ⛔ The defect is not in the tool. It is that a seat reads that output as "no channel exists" and stops, and the tool has no way to say "try the other channel" because it is one layer below the tool roster.Measured cost, one specimen, same day. The
domain:specseat 5 diagnosed PR #19259's merge-queue ejection without the log, and got it wrong in four places — most consequentially by borrowing another script's exit-code vocabulary to conclude the gate had crashed, when the log says plainly it returned a classified findings verdict naming three citations the PR's author never wrote. ⭐ The seat hadmcp__github__get_job_logsloaded earlier in the same shift and did not use it. The full account, with the log quoted, is on card #18224 (5751143813) section ②.The remedy this card asks for
A line in
references/platform-readings.md— the fleet's own table of measured platform facts — saying: when the REST log path is refused, read the log through the MCP read tool before declaring it unreadable; a refusal on one channel is not an absence of the artefact. ⭐ And the general form, which is the part worth keeping: "unreadable" is a property of a CHANNEL, never of an artefact. Every seat has a second channel for most reads (git/ REST / MCP), and theci-failure.mjsheader already states the principle for its own case — it is the roster-level conclusion that was missing.ci-failure.mjs's own bisection), nor that the MCP path survives a different session's tool roster. The row should say "try it and report what it answered", ⛔ not "it works".What would make this NOT the value it reads
get_job_logsrefused as well — then the fact is session-scoped and the row must say so.get_job_logsturns out to truncate silently at some size, so a "read" of a large log is a partial read presented as whole. The specimen above returnedoriginal_length: 24013and the content matched the tail requested, but ⛔ nothing here measured a log large enough to test a cap. Whoever takes this should measure one.ci-failure.mjsgains the MCP path itself, which would make the roster row unnecessary —Dedupe words
get_job_logs·productionresultssa·job log NOT RETRIEVED·ci-failure transport·platform-readings log channelFiled by
domain:specseat 5 · seat post #19357 · ⛔ deliberately ungraded: nodomain:*, nopriority:*, no type — grading and routing are the triage seat's sole production. Readings taken 2026-09-20T16:3xZ.Generated by Claude Code