Skip to content

feat(work-resolve-issue): summarize a reused branch in its PR body - #7831

Merged
kyle-sexton merged 10 commits into
mainfrom
feat/reuse-summary-pass
Oct 17, 2026
Merged

kyle-sexton merged 10 commits into
mainfrom
feat/reuse-summary-pass

Conversation

@kyle-sexton

@kyle-sexton kyle-sexton commented Oct 17, 2026 •

Copy link
Copy Markdown
Member

No related issue: an owner decision in session. When work-resolve-issue reuses a crashed run's branch, a read-only summary pass now writes the PR body; the automatic Hold stays until #7815 is proven on live runs.

Summary

A reuse PR (opened from an earlier run's unreported lane branch, #7790) used to carry a fixed body with no model text. Now a short, read-only summary pass over the issue and the branch diff writes the PR body when its summary is usable and bound to this issue, branch and tip; otherwise the PR keeps today's fixed body. Every reuse PR also lists the files the branch changes, with denied paths marked. Either way the PR keeps the automatic "Hold: a human merges this" line: reuse PRs stay human-merged until the review-lane fix in #7815 is proven on live runs.

Fix

  • The summary pass runs in its own summary job in pr-run-activity-write.yml: needs: run, starts only when the run job's reuse-branch output is set (so that job minted nothing), and holds contents: read only, with no id-token. No model turn runs in the job that holds id-token: write.
  • The job checks out the run job's base-sha (persist-credentials: false), and write-issue.mjs writes the issue with the job's read-only token. A blobless fetch into a separate repository with GIT_CONFIG_GLOBAL=/dev/null plus git diff --no-ext-diff --no-textconv writes the branch diff as data, so nothing from the branch is checked out or run. The fetch is unauthenticated, so the pass needs a public repository; elsewhere the PR keeps the fixed text.
  • claude-code-action runs on a new summarize-reuse route (profile summary: sonnet, effort medium, 10 turns) after runner isolation, with Read limited to the data directory and every other tool disallowed, plus --json-schema, no plugins and no commit signing. The prompt passes through a random heredoc delimiter, and the model output passes through env only.
  • Every summary job step is continue-on-error and gated on the one before it, so a failed pass turns no job red. report-outcome needs the whole resolve-issue call and runs with !cancelled(), so it opens the PR whether the summary job succeeded, failed or skipped.
  • report-outcome uses the summary only when it names this issue and branch and the branch is still at that tip. Every reuse PR lists the files GET /compare names, with denied-paths matches marked and listed first. If the compare fails, the PR says the list is unavailable and still opens. reusePlan builds the body frame once and varies only the Summary, Fix, Verification and Left out text.
  • post-statuses never reads the summary, so the model cannot set a status. The caller's permissions: are unchanged.
  • Docs: pr-run-activity.md has a new "The summary job" section, plus run-job step 5 and Issue mode updates. summarize-reuse and the summary profile are named in pr-pipeline.schema.json, docs/conventions/pr-pipeline/README.md (Models), the resolve-config README and the pr-pipeline CHANGELOG (2.4.0).

Verification

  • Node over .github/scripts/*/*.test.mjs and .github/actions/*/*.test.mjs: 1291 pass, 0 fail (report-outcome 115/115, resolve-config 315/315). write-runner-never-rules.test.sh: PASS=83 FAIL=0. It now asserts the run job has no summary or second model step, the summary job's needs, if, contents: read-only permissions, continue-on-error on every step, and the base-only checkout. actionlint, zizmor (offline) and Runner policy passed. are clean on the changed workflows. The pre-push check-affected hook passed.
  • Hold pinned: "main opens a held reuse PR from a summary bound to this issue, branch and tip" uses readable denied paths, a README-only compare and an issue with no held class. A mutant that holds only where decide()'s runner-fact predicate would hold fails it and the reusePlan unit test (2 fail, 113 pass). The real code was restored before the commit.
  • Not run live yet. A failure of the summary job falls back to the fixed body with the Hold.

Related

🤖 Generated with Claude Code

https://claude.ai/code/session_01YcPjcqFavvzkTGBKXMgdXR

kyle-sexton and others added 5 commits October 17, 2026 17:51
…ow to the merge lane

On a reuse the skill run now runs a read-only summary pass instead of
nothing: no lane token is minted, the job's read-only GITHUB_TOKEN writes
the issue and a blobless-fetched diff of the branch as data, and a second
claude-code-action step on the new summarize-reuse route (sonnet, effort
medium, 10 turns) reads them with Read alone and returns title, summary
and changes as structured output. The runner binds that output to its own
issue, branch and diffed tip in reuse-summary.json.

report-outcome uses the summary only when it names this issue and branch
and the branch is still at that tip; the PR then carries it and the hold
line only where a done PR would. Otherwise, as before, fixed text and the
hold line.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YcPjcqFavvzkTGBKXMgdXR
# Conflicts:
#	.github/scripts/work-resolve-issue/report-outcome.mjs
A reuse PR carries the hold line whether or not the summary pass wrote
its body, until the review-lane fix (#7815) is proven on live runs.
The tests assert the hold on the summarized body, and the route docs,
schema descriptions and convention changelog now name summarize-reuse.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YcPjcqFavvzkTGBKXMgdXR
@kyle-sexton kyle-sexton changed the title feat(work-resolve-issue): summarize a reused branch and let its PR flow to the merge lane feat(work-resolve-issue): summarize a reused branch in its PR body Oct 17, 2026
kyle-sexton and others added 4 commits October 17, 2026 18:32
…nly job

The summary model step ran in the run job, which holds id-token: write
with its one broker mint unused on a reuse. It now runs in a summary job
that needs the run job, holds only contents: read, and keeps the
blobless fetch, the no-config diff and env-only model output. Every step
is continue-on-error and gated on the one before, so a failed pass turns
no job red.

Every reuse PR now lists the files GET /compare names, denied paths
marked and first, or says the list is unavailable. reusePlan builds the
body frame once. The tests pin the hold with readable denied paths, a
README-only diff and an unheld work class, so a conditional hold fails
them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YcPjcqFavvzkTGBKXMgdXR
…mat characters

neutral() stripped everything from `<!--` to the end before escaping
`<`, so a branch file named `README.md<!--x` listed as `README.md`. A
file path now keeps that text, escaped. Format characters (\p{Cf}:
bidi overrides, zero-width) are now dropped from paths and summary
text alike.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YcPjcqFavvzkTGBKXMgdXR
@kyle-sexton
kyle-sexton marked this pull request as ready for review October 17, 2026 22:43
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 17, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-17T22:44:59.928506Z 59478cd Draft marked ready
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@github-actions

github-actions Bot commented Oct 17, 2026 •

Copy link
Copy Markdown
Contributor

claude-review needs no new review of 3ade7ebff18e9bf61463b4a8b06500ce5f3a529b: no file of this pull request changed since the review of 59478cd.

Code review: PR 7831 (summarize a reused branch in its PR body)

I found nothing to report and posted no inline comments.

I read the whole diff and checked these points against the code in the checkout. I did not run the tests, the workflow or the summary job.

  • Summary binding in report-outcome.mjs: readSummary and the tip re-check in main bind the summary to this issue, this branch and the branch's current tip. Any miss falls back to the fixed text. A failed compare gives the "list unavailable" text and does not block the PR.
  • Sanitising: model text and file names go through neutral, quoted and bullets. The new \p{Cf} strip runs before the < and & escaping, so a zero-width character cannot rebuild a comment opener.
  • Title: prTitle re-validates the model's title, so a non-conforming title falls back to the issue-derived one.
  • Workflow wiring: the summary job has the needs.run.outputs.reuse-branch and base-sha outputs it uses. It holds contents: read only, and every step is continue-on-error and gated on the one before it. Its resolve-config outputs and the write-issue.mjs env vars it sets match the action and the script. The model's output reaches the artifact through env and jq only.
  • Artifact: the artifact download and REUSE_SUMMARY_DIR are set only when reuse-branch is set, and a missing artifact leaves the directory empty, so the fixed text is used.
  • Docs and config: the docs, schema, README and pr-pipeline.yaml agree on the new summarize-reuse route and summary profile.

The PR's own verification numbers are the author's claims. I did not run the Node tests or the never-rules shell test, so I haven't confirmed them. The description also says the summary job has not run live yet.

@github-actions

github-actions Bot commented Oct 17, 2026 •

Copy link
Copy Markdown
Contributor

claude-security-review needs no new review of 3ade7ebff18e9bf61463b4a8b06500ce5f3a529b: no file of this pull request changed since the review of 59478cd.

Security review: PR #7831 (head 59478cd)

No security findings. I read the whole diff, plus the surrounding neutral, quoted, bullets and prTitle code in report-outcome.mjs and the write-issue.mjs env contract. The protected-class register is not relevant: the diff deletes or softens no standing instruction.

The new summary pass feeds untrusted text (the issue, the branch diff) to a model and puts the model's output into a PR body. I checked the following and found each one holds:

  • Model isolation.
    • The summary job has permissions: contents: read only.
    • The pass runs in its own job, so no model turn shares a job with id-token: write.
    • Read is the only allowed tool, and only under the runner-written data directory; Bash, Edit, Write, web, Agent and the GitHub MCP tools are all denied.
    • The runner isolation step gates the model step.
    • The reuse-summary-data directory holds only runner-written files.
  • Fetching the diff.
    • The branch name is regex-bound to this issue number before use.
    • The fetch goes into a fresh repo outside the workspace.
    • --no-ext-diff --no-textconv is set, with system and global git config disabled, so nothing on the branch runs.
    • The fetch is unauthenticated, and the diff is capped at 1 MB.
  • Issue context. The step passes the same label-event env (LABEL_*, LANES_APP_ID, TRUSTED_ACTORS_PATH) that the run job's write-issue.mjs step passes, so the trusted-comment filter and the post-label edit check are unchanged.
  • Output binding in readSummary.
    • The runner, not the model, writes issue, branch and tip into the artifact.
    • readSummary requires issue and branch to match this run and tip to be 40 hex characters.
    • output must be a non-array object.
    • main also re-checks the live ref tip against summary.tip before the summary is used, so a branch pushed to after the summary pass read it falls back to the fixed text.
    • The summary is used only when unreported is true.
  • Neutralising model text in the PR body.
    • neutral() strips HTML comments and escapes &, <, >, [ and ].
    • It removes @, replaces #, and breaks GH-, /issues/, /pull/, :// and www. so nothing links or closes an issue.
    • This PR adds stripping of Cf format characters (bidi, zero-width).
    • Every line of the summary is blockquoted and every change is a one-line bullet, so neither can open a section.
    • The title goes through the existing prTitle PREFIX/SUBJECT validation, with the issue-title fallback.
    • File names from the branch go through neutral(..., {path: true}), which escapes <, so a path name cannot inject HTML or comments.
  • Artifact trust.
    • download-artifact has no run-id, so it reads only this run's artifacts.
    • The report job parses the file as JSON data and never executes it.
    • A missing or invalid artifact falls back to the fixed text.
  • Other changes.
    • The summarize-reuse route added to resolve.mjs, the config and the schema only selects a models profile.
    • The compare listing only adds text to the PR body. A failed compare does not block the PR, and the denied-path marking is informational: the merge lane's own diff check still enforces the hold.
    • The new action pins are SHA-pinned, and the Actions hardening items are left to zizmor.

One residual, not reportable: the oauth token is an input to the model-running action, so the isolation rests on the Read allowlist plus --permission-mode dontAsk. That matches the run job's existing pattern. I found no path in this diff that reads the token or writes outside the neutralised PR body.

@kyle-sexton
kyle-sexton added this pull request to the merge queue Oct 17, 2026
Merged via the queue into main with commit bd0da2c Oct 17, 2026
29 checks passed
@kyle-sexton
kyle-sexton deleted the feat/reuse-summary-pass branch October 17, 2026 23:16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant