Repository navigation
feat(lanes): upload each reusable model leg's transcript as a sealed 7-day artifact - #969
Conversation
…7-day artifact Add the upload-lane-transcript action: it scans a leg's execution file, seals it in claude-code-plugins' seal-lane-output record format to the caller's public key, and uploads only the sealed file as lane-transcript-<job-id>-<attempt> for 7 days. An empty key skips the scan and the seal with a notice. pr-review-cli.yml and intake-triage.yml take a new optional lane-transcript-public-key input and run the action for every run-claude-cli leg, after the outcome step, whose timeout check reads the clock. Each model job's limit grows by the new steps' limits. Closes #964 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VQb9Q1VgLEiwGgRVshK3b7
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
The ordering check passed if the outcome step was renamed, since findIndex returns -1. One test name promised behaviour it only pins as step conditions. The intake comment no longer held once a sealed transcript leaves the job. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VQb9Q1VgLEiwGgRVshK3b7
test-contracts already runs .github/scripts/*.test.cjs, so the seal test needs no new step. Leaving pr-require-checks.yml unchanged lets the review lanes run on this pull request: claude-code-action skips itself when the calling workflow differs from the default branch's copy. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VQb9Q1VgLEiwGgRVshK3b7
|
claude-security-review reviewed Incremental review of No security findings. critical-findings: 0 |
|
claude-review reviewed No findings. I reviewed only the change since 1f5acaa, which is in The change is formatting only, in three places:
Reading the diff, the arguments, the paths and the control flow are the same before and after. No test behaviour changes. I did not run I did not re-review the other files of the PR; they are unchanged since the earlier review. |
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VQb9Q1VgLEiwGgRVshK3b7
…nput (#942) Refs: melodic-software/claude-code-plugins#7676 ## Summary Adds `lane-transcript-public-key` to the reviewed `allowedInputs` of `intake-triage.yml` and `pr-review-cli.yml` in `components/runner-policy/policy.json`, so a caller that passes it clears `runner-policy`. ## Fix [melodic-software/ci-workflows#969](melodic-software/ci-workflows#969) added the optional input to those two reusables (`default: ""`). Without a contract entry, `runner-policy` refuses a caller that passes it: "the reusable workflow call has inputs absent from its reviewed contract". The value is a public key and does not affect runner routing. Callers float `@main` (`runner-policy.mjs:490-493`), so `approvedShas` does not change. This PR does not change the hosted caller components. `components/claude-lanes-hosted/claude-review.yml:84` calls `pr-review.yml@main` and `claude-security-review.yml:77` calls `pr-review-security.yml@main`, not `pr-review-cli.yml`, and neither reusable declares this input on ci-workflows main. Adding the line there fails two `runner-policy.test.mjs` tests: - "each hosted claude lane caller equals its fleet sibling" - "every managed target of a hosted claude lane caller is public and audits clean", with the contract message above In this organization `pr-review-cli.yml` is called by locally owned workflows, in claude-code-plugins and pr-pipeline-sandbox (`distribution/sync-manifest.yml:880-882` for the sandbox). ## Verification - `node --test components/runner-policy/runner-policy.test.mjs`: 226/226 pass. - `node components/runner-policy/runner-policy.mjs --root .`: "Runner policy passed." - `biome check --config-path=biome.jsonc components/runner-policy/policy.json`: no errors, schema-version notices only. - Probe, not committed: the two caller lines above, without any `policy.json` entry for `pr-review.yml`, fail the two named tests. ## Related - [melodic-software/ci-workflows#969](melodic-software/ci-workflows#969): the input. - [#940](#940): the same policy-only change for `generated-copies`. ## Remaining - After the standards sync carrying this `policy.json` reaches claude-code-plugins (`distribution/sync-manifest.yml:382`), pass the input from that repository's callers: `.github/workflows/intake-triage.yml:258`, `pr-review-hosted.yml:220` and `pr-review-security-hosted.yml:225`. Before that sync, its `runner-policy` refuses them. - The hosted caller components in this repository get no transcript input until their reusables declare it or they move to `pr-review-cli.yml`. That is an open decision. <!-- session: lane-5 --> 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01VQb9Q1VgLEiwGgRVshK3b7 Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Closes #964
Summary
The model legs inside
pr-review-cli.ymlandintake-triage.ymlkept no transcript, so the daily puller in melodic-software/claude-code-plugins#7676 had nothing to copy for them. After this PR, eachrun-claude-clileg's execution file is scanned, sealed to the caller's public key and uploaded as a 7-day artifact. No plaintext transcript reaches a log or an artifact. Nothing changes for a caller until it passes the key.Fix
.github/actions/upload-lane-transcript. It runs../scan-upload/scan.cjson the plaintext, gzips and seals the file in memory, and uploads onlytranscript.json.gz.sealedaslane-transcript-<job-id>-<attempt>,retention-days: 7,overwrite: false. The record is claude-code-plugins'seal-lane-outputformat, bound to the run id and job id.lane-transcript-public-keyon both reusables, default empty. An empty key skips the scan and the seal with a notice. In the model action the scan runs first; here it does not, because callers float on@mainwith no key until the follow-up below lands, and every leg would otherwise clone claude-code-plugins and could raise a scan error for a transcript nobody keeps.if: always() && steps.<leg>.outputs.execution-file != '',continue-on-error: trueandtimeout-minutes: 3. That is 4 per job in the threepr-review-cli.ymlmodel jobs and 1 per job in the threeintake-triage.ymlmodel jobs. A sealing failure or a malformed key fails only that step.report-lane-outcome/action.yml:360-362compares the clock at the outcome step withstep-started-at, so upload time before it could turn a failed leg near its budget intotimeout.continue-on-errordoes not stop a job timeout from turning a lane red.<lane>-<github.job>plus-2,-3,-4for the chunk legs (code-review-key-2), andintake-<github.job>(intake-subscription).github.jobmust stay an expression because the parity testsdeepEqualthe three jobs' steps. In a reusable it is the called job's key: real artifacts readpr-review-cli-handoff-code-review-subscription-<run>-<sha>. The lane prefix keeps ids apart if one run calls both lanes, as this file's other artifact names already do. The puller'sARTIFACTregex (scripts/lane-transcripts/store.mjs) is greedy up to the last-, solane-transcript-code-review-key-2-1parses as jobcode-review-key-2, attempt 1.run-claude-cliexecution-fileoutput no longer says "do not upload it" and now points at this action. Both SECURITY MODEL headers, both canonical callers and the README action list name the new step.No README or
package.jsonfor the action, as withscan-uploadandrun-claude-cli; itsaction.ymldescription holds the contract. The seal code is CommonJS, since this repository has no.mjsand CI globs*.test.cjs, and it keeps only the single-record path (no bundle or chunk code).Verification
origin/mainopen-lane-output/open.mjsopenRecordand named withstore.mjsparseArtifactName, for all 27 job ids:opened 27 of 27 job ids, every name parsed, every plaintext byte-identical,transcriptMetricsread each one.node --testonupload-lane-transcript-seal.test.cjs,intake-triage-legs,pr-review-cli,lane-outcome-emitters,network-timeout-policy,github-script-bodies,ci-fanout-consolidationandrun-claude-cli/lane.test.cjs: 261/261 pass. New tests assert every leg has an upload with a unique, parseable job id. The seal test lives in.github/scripts, so the existingtest-contractsglob runs it with no workflow edit; that also lets the review lanes run here, sinceclaude-code-actionskips itself whenpr-require-checks.ymldiffers from main.node --teston the 11 other suites that read these workflows (claude-lane-job-gates,claude-review-declared-outputs,claude-review-outcome-wiring,pr-review-cli-api-calls,report-api-budget,check-key-credit,intake-triage-apply,intake-triage-gate,claude-lane-status-check,claude-lane-fallback,claude-lane-install-isolation): all pass.actionlintfrom the repository root on the three changed workflows: no findings.zizmor --offlineon the two reusables and the newaction.yml: no findings (7 suppressed).composite-run-shellcheck.shon the newaction.yml: 2 run blocks checked, no findings.biome checkon the edited tests,markdownlint-cli2 README.md,typos: clean..github/scripts/*.test.cjs, which CI runs.Related
Remaining
lane-transcript-public-keytoallowedInputsof both reusables' entries in melodic-software/standardscomponents/runner-policy/policy.json, synced to.github/standards/runner-policy/policy.jsonin each repository. Neither entry lists it today, so the caller edit below would fall outside the policy's allowlist. The policy pins no job limit, so the limit changes here need no policy edit.lane-transcript-public-key: ${{ vars.LANE_TRANSCRIPT_PUBLIC_KEY }}to thewith:of thepr-reviewjob inpr-review-hosted.yml, thepr-review-securityjob inpr-review-security-hosted.ymland theintake-triagejob inintake-triage.yml. Until it lands, every leg only prints the skip notice. The close of claude-code-plugins#7676 depends on it.🤖 Generated with Claude Code
https://claude.ai/code/session_01VQb9Q1VgLEiwGgRVshK3b7