feat(seidroid-review)!: remove the cross-repository target grammar - #83
Conversation
`@seidroid review owner/name#123` reviewed a pull request in another repository. `ai-review.yml` never offered this. A review must only ever be invokable on the repository the pull request is on, so a maintainer decision withdraws the grammar. The whole surface goes in one commit, because a half-removal is worse than either end state. A parse that still admits a target reviews the local pull request. The requester then reads a verdict on code the review never read. This commit deletes the regex target group, the parse block, the guard's three target outputs, and every expression that read them. That is 31 occurrences over 27 lines, in 11 steps, the guard `outputs:` block, and the review `concurrency.group`. Two fallbacks stay. The review job's `concurrency.group` and the drive step's `PR:` keep `github.event.pull_request.number`. A close that arrives as a `pull_request` event skips the guard, so the event is the only source of a number there. Collapsed, every reclaim run shares one group. Two closes for different pull requests then cancel each other, and close is the only thing that reclaims a sandbox. Both App-token mints keep `owner:` and `repositories:`. `owner` on its own mints a token that reaches every repository the installation is on. So `repositories` narrows to `github.event.repository.name` rather than going away. The guard refuses the withdrawn form by name. `ai-assistant.yml` reserves the bare `@seidroid review` body only, so a request that carries a target reaches the conversational assistant. A chatty answer to it reads as a review that ran somewhere. The refusal captures the match through `grep -m1 ... || true` rather than reading a `grep -q` exit status. `-q` stops at the first match. On a body larger than the pipe buffer that leaves `printf` writing into a closed pipe, and `pipefail` reads the SIGPIPE as a failed pipeline. Measured at 232 kB, the `-q` form reports status 141 and writes no refusal. Verified. actionlint drops from 5 findings to 4. The lone SC2129 sat on a deleted line, and the four SC2102 hold at the same script offsets. Nothing new appears. `shellcheck -s bash` is clean on the extracted parse script. The extracted script also answers the ordinary grammar byte-identically to the base. That covers the bare and `@`-prefixed forms, leading, trailing and doubled whitespace, a command line inside a longer comment, `close`, and the `pull_request` path. Every run body in the file except `parse` and `Post the verdict` hashes identical to the base. BREAKING CHANGE: `@seidroid review owner/name#123` and `@seidroid review close owner/name#123` earn an error annotation instead of a review of the named pull request. No `workflow_call` input or secret changes, so no caller needs an edit. Refs: #82 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
PR SummaryMedium Risk Overview The guard drops Downstream steps always use Reviewed by Cursor Bugbot for commit da6b7ca. Bugbot is set up for automated code reviews on this repo. Configure here. |
There was a problem hiding this comment.
A clean, complete withdrawal of the cross-repository target grammar — all 31 expression sites, the guard's parse block, the outputs, the concurrency key and the now-tautological Place findings condition are removed consistently, with no dangling references and no workflow_call input changes. One non-blocking concern: the explicit refusal that replaced the grammar is emitted as an ::error:: annotation on a run that isn't linked to the PR, and it doesn't stop ai-assistant.yml from posting the conversational reply it was written to pre-empt.
Findings: 0 blocking | 4 non-blocking | 1 posted inline
Blockers
- None at the file/PR level.
Non-blocking
- Verified the removal is complete and self-consistent: no
review_repo/review_repo_name/review_pr/REPO_OWNER/target_rereferences remain anywhere in.github/workflows/, no in-repo caller or doc (.github/seidroid/ai-review/README.md) documented the withdrawn target grammar, and theworkflow_callinput surface is unchanged so no caller breaks. - The
named_repocapture correctly reuses thegrep -m1 … || trueidiom rather thangrep -q; withset -o pipefailthe|| trueinside the command substitution absorbs the SIGPIPE-derived 141, so the fix described in the PR body holds. - Keeping
repositories:alongsideowner:on bothcreate-github-app-tokenmints is the right call — dropping it would have widened the token to every repository the installation reaches. - 1 suggestion(s)/nit(s) flagged inline on specific lines.
| named_repo="$(printf '%s\n' "$cmd" \ | ||
| | grep -m1 -E '^[[:space:]]*@?seidroid[[:space:]]+review([[:space:]]+close)?[[:space:]]+[A-Za-z0-9][A-Za-z0-9._-]*/[A-Za-z0-9][A-Za-z0-9._-]*#[0-9]+[[:space:]]*$' || true)" | ||
| if [ -n "$named_repo" ]; then | ||
| echo "::error::seidroid review takes no repository target; a review runs only on the repository the pull request is on" |
There was a problem hiding this comment.
[suggestion] The refusal doesn't reach the person who asked, and it doesn't achieve the goal the comment above it states.
::error:: writes an annotation onto this workflow run's summary. An issue_comment-triggered run is not attached to the pull request, so it appears in no check list and on no timeline — the requester would have to go find the Actions run to see it.
Meanwhile the outcome this block is meant to prevent still happens. ai-assistant.yml reserves only the body that normalizes to exactly @seidroid review (.github/workflows/ai-assistant.yml:107-110), so @seidroid review owner/name#123 is not reserved, is_review_command is false, and the conversational assistant posts its generic reply exactly as before. The net effect of this change is the chatty answer the comment calls "a worse signal than silence", plus an invisible annotation.
If the refusal is meant to be seen, it has to be a PR comment (the guard has permissions: {}, so this would need a scoped grant or to be deferred to a step that already holds a token). If an annotation is genuinely all that's wanted, the comment above should say that it's a log-only record rather than a user-facing refusal, since the stated justification for existing at all is the signal to the requester.
Separately, the pattern only matches the exact whole-line owner/name#N shape, so @seidroid review owner/name#123 please or @seidroid review other-repo#5 fall through with no annotation at all — worth noting in the comment if that narrowness is deliberate.
There was a problem hiding this comment.
Both correct, and the second one names the sharper version: the comment justified the refusal with a benefit it does not deliver. Fixed in the latest commit.
You are right on both mechanisms. An issue_comment run attaches to no pull request, so the annotation reaches whoever opens the run and nobody else. And ai-assistant.yml reserves the body that normalises to exactly @seidroid review, so the target form was never reserved and still reaches the conversational assistant.
I kept the refusal and rewrote the justification rather than escalating it, because both of those match ai-review: there the target form was never a review command either, so an unreserved body reaching the assistant is the incumbent's behaviour, not a regression this branch introduces. Parity is the standard for this work, so matching it is the answer rather than exceeding it.
What the line now claims is only what it does: it names the reason in the one place a person debugging "why did my request do nothing" will look. If we later want the refusal to reach the requester, the change is to ai-assistant.yml's reservation, not here — and that is a behaviour change past this ticket.
ai-review denies at notice throughout: an unauthorised actor, an unlisted bot, a draft, an empty team. error there is reserved for a caller that wired the workflow wrongly, which this is not -- the request is well-formed and refused. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
A faithful, mechanical withdrawal of the cross-repository target grammar: every review_repo/review_pr/target_re/REPO_OWNER reference is gone, each removed expression fallback collapses to exactly the value it produced when no target was named (concurrency group, Place findings condition, Post the verdict REPO/PR ternaries all verified identity-preserving), and both App-token mints keep repositories: so token scope is not widened. The only net-new logic is the refusal notice, which is correctly written (captured through grep -m1 … || true rather than a pipefail-sensitive grep -q, and it no longer interpolates comment-derived text into a workflow command); my remaining notes are about whether that refusal ever reaches the person who wrote the request.
Findings: 0 blocking | 4 non-blocking | 1 posted inline
Blockers
- None at the file/PR level.
Non-blocking
- The refusal's reachability depends on caller wiring that lives outside this repo. The header states routing is the caller's ("it reads the body and passes the mode"), so a caller whose dispatch condition matches only the exact
@seidroid reviewbody never invokes this workflow for@seidroid review owner/name#12— and the newnamed_repobranch is then unreachable dead code. Worth confirming against at least one real caller, or noting in the input docs that callers must dispatch on a looser body match for the refusal to fire. - The PR description and the code disagree on the annotation level: the Verification block says "one
::error::and oneshould_runkey each time" and the "Not changed, worth a look" section reasons about::error::not failing a step, while the merged code emits::notice::(correctly, per its own inline rationale). Worth correcting the description so a later reader does not go looking for an error annotation that is not emitted. - The refusal regex restates the command grammar's
^[[:space:]]*@?seidroid[[:space:]]+review([[:space:]]+close)?prefix verbatim three lines below the grammar itself. The comment notes they are deliberately matched on the same anchor, but nothing enforces that; a future change to the accepted command shape has to be made in both places or the refusal silently stops covering the fall-through set. - 1 suggestion(s)/nit(s) flagged inline on specific lines.
| named_repo="$(printf '%s\n' "$cmd" \ | ||
| | grep -m1 -E '^[[:space:]]*@?seidroid[[:space:]]+review([[:space:]]+close)?[[:space:]]+[A-Za-z0-9][A-Za-z0-9._-]*/[A-Za-z0-9][A-Za-z0-9._-]*#[0-9]+[[:space:]]*$' || true)" | ||
| if [ -n "$named_repo" ]; then | ||
| echo "::notice::seidroid review takes no repository target; a review runs only on the repository the pull request is on" |
There was a problem hiding this comment.
[suggestion] This refusal is written only to the guard job's log. should_run=false follows, the review job is skipped, and the workflow concludes success — so the requester sees a green check and has to open the run and read annotations to find out anything was refused.
The comment above justifies the refusal by saying silence is not available, because ai-assistant.yml reserves only the exact bare body and "a chatty answer to this reads as a review that ran somewhere." But this code does not stop the chatty answer: ai-assistant.yml's route step compares the normalized body against exactly @seidroid review, so @seidroid review owner/name#12 still fails that check and still reaches the conversational assistant. Net effect on the PR page is unchanged from before this branch — a generic reply — with the refusal recorded where nobody looks.
Since the stated goal is that the requester learns their request was declined, the refusal needs a channel they will actually see: a reaction or a short comment on the triggering comment (the workflow already has that pattern in the "Post the verdict" / trigger-reaction steps), or widening the ai-assistant.yml reservation so the refusal lands where the request was written. The PR description flags this as out of scope, which is a fair call — but as merged, the block does not achieve the outcome its own comment claims for it.
There was a problem hiding this comment.
Both correct, and the second one names the sharper version: the comment justified the refusal with a benefit it does not deliver. Fixed in the latest commit.
You are right on both mechanisms. An issue_comment run attaches to no pull request, so the annotation reaches whoever opens the run and nobody else. And ai-assistant.yml reserves the body that normalises to exactly @seidroid review, so the target form was never reserved and still reaches the conversational assistant.
I kept the refusal and rewrote the justification rather than escalating it, because both of those match ai-review: there the target form was never a review command either, so an unreserved body reaching the assistant is the incumbent's behaviour, not a regression this branch introduces. Parity is the standard for this work, so matching it is the answer rather than exceeding it.
What the line now claims is only what it does: it names the reason in the one place a person debugging "why did my request do nothing" will look. If we later want the refusal to reach the requester, the change is to ai-assistant.yml's reservation, not here — and that is a behaviour change past this ticket.
The justification claimed the refusal stops a chatty answer from the assistant. It does not: ai-assistant.yml reserves the bare command alone, so the target form reaches it either way. It also implied an audience the annotation does not have, because an issue_comment run attaches to no pull request. Both match ai-review, where the target form was never a review command. The refusal earns its line by naming the reason where a person debugging the silence will look, and the comment says that now rather than a benefit it does not deliver. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Clean, complete removal of the cross-repository target grammar from seidroid-review.yml: all 3 guard outputs, the parse block, the dead REPO_OWNER env, the now-tautological placement condition and every consumer expression are gone, workflow_call inputs are untouched so no caller breaks, and both App-token mints keep repositories: alongside owner: so the token scope is not widened. No blocking issues; two minor notes.
Findings: 0 blocking | 2 non-blocking | 1 posted inline
Blockers
- None at the file/PR level.
Non-blocking
- The refusal for a repository-targeted request is a
::notice::on anissue_commentrun, which is only visible to someone who opens the Actions run. Meanwhileai-assistant.ymlstill answers that body conversationally (its reservation is the exact@seidroid reviewalone), so the person who typed the withdrawn form sees a normal assistant reply and no indication the target was refused. The PR body acknowledges this tradeoff; worth revisiting if users start reporting "my review request did nothing", e.g. by teaching the assistant prompt about the withdrawn form so the reply the user actually sees says so. - 1 suggestion(s)/nit(s) flagged inline on specific lines.
| # reads the SIGPIPE as a failed pipeline and the refusal is not written. | ||
| # Measured: at 232 kB the -q form reports status 141 and stays silent. | ||
| named_repo="$(printf '%s\n' "$cmd" \ | ||
| | grep -m1 -E '^[[:space:]]*@?seidroid[[:space:]]+review([[:space:]]+close)?[[:space:]]+[A-Za-z0-9][A-Za-z0-9._-]*/[A-Za-z0-9][A-Za-z0-9._-]*#[0-9]+[[:space:]]*$' || true)" |
There was a problem hiding this comment.
[nit] The command prefix ^[[:space:]]*@?seidroid[[:space:]]+review([[:space:]]+close)? is now written out three times in this step (the cmdline= grep at 347, this refusal grep, and the close-detection grep at 391). The removed target_re variable existed to avoid exactly this; hoisting the shared prefix into a local (e.g. cmd_re='^[[:space:]]*@?seidroid[[:space:]]+review') would keep the refusal from silently diverging from the grammar it is supposed to mirror when the trigger word or an option is next changed. The comment above already promises they are "matched on the same whole-line anchor" — a shared variable is what makes that structurally true rather than true by inspection.
The SIGPIPE fix itself is right: grep -m1 … || true inside a command substitution with a -z/-n test matches the cmdline= idiom above and is not exposed to pipefail propagating 141.
… reaches no verdict (#85) Closes PLT-1143 (workflow half). The driver half shipped in sei-protocol/sei-internal-skills#405 and releases as `v0.14.0`. ## Problem A review that reaches no verdict publishes nothing. The pull request carries no comment, no check run, and no reaction that says anything happened. A reviewer sees a red job and an untouched pull request, and cannot tell a review that ran and could not be read from one that never ran. Only one of the two is a reason to look. ## What changes **The check run publishes whichever check the driver wrote.** The step dropped its `verdict_produced` gate; the file's presence is the gate instead. A no-verdict run writes a check under the title `no verdict` concluding `failure`, carrying the reason and no `counts` key. A run that never reached the driver still publishes nothing. Read the **title**, not the conclusion, to tell the two apart — a decided review carrying blockers concludes `failure` too. **A new step reports the run in a comment.** It is the exact complement of `Answer the request`, on one output: `== 'true'` there, `!= 'true'` here. So a review run that was not cancelled posts one comment or the other, never both and never neither — including a run that died before the driver, where `verdict_produced` is unset and this is the only record left. The comment quotes the check run's own summary, so the two cannot disagree. When the run stopped before the review started there is no reason to quote, and the body says that rather than implying the review ran. **The position step keeps its gate.** `verdict_produced` is load-bearing there twice over: the withdrawal at the end reads the driver's finding counts, the no-verdict check carries none, and the no-counts branch proceeds by design. An ungated no-verdict run would clear a standing block on the strength of a review that produced nothing. The check run and the comment publish without the gate; anything that clears a merge gate keeps it. ## Parity `ai-review.yml` answers the same case the same way, and forces `failure` on unreadable output for the same reason. ## Requires Driver `v0.14.0`. On an older pin the check file is absent on a no-verdict run, the publish step self-gates on `[ ! -s "$CHECK" ]`, and the comment step still reports — so this degrades rather than breaks. ## Verification ``` python3 -c "import yaml; yaml.safe_load(...)" parses actionlint base 4 [shellcheck] actionlint head 4 [shellcheck] unchanged grep review_repo\|review_pr 0 (the cross-repo grammar #83 removed stays removed) ``` Not verified from here: whether the new step's condition fires as written in GitHub's runner. That needs a live no-verdict run, which the next `@seidroid review` after the pin bump will produce. --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
…the dismissal (#88) The step that records the review's position required the App identity, so a repository with no `SEIDROID_APP_ID` got a verdict comment and never appeared in the reviewers list. It now falls back to `github.token`, which is what every sibling publisher in this file already does. ## What changed - `State the review's position on the pull request` drops `steps.identity.outputs.token != ''` from its condition and reads `steps.identity.outputs.token || github.token`. The App still authors the position wherever a caller configured one. - The position post gains a `COMMENT` rung, mirroring `.github/workflows/ai-review.yml:983-986`. When the API refuses `APPROVE` or `REQUEST_CHANGES`, the step retries the same body as `COMMENT` and warns. - Two comments now state the present: the position step names what the fallback identity costs, and the `Answer the request` step names why it stands apart from the position step. The withdrawal, its interlocks and its loud failure on a stuck block are untouched. ## What a GITHUB_TOKEN review can and cannot do **Measured in this repository.** `github-actions[bot]` — the login `GITHUB_TOKEN` carries — has created `APPROVE` reviews here through `ai-review.yml`'s identical fallback: PRs #84, #83, #75, #74, #73, #72, #71, #70 and #61 all carry a `github-actions[bot]` `APPROVED` review whose body opens with `<!-- ai-combined-review -->`. It has also created `REQUEST_CHANGES` reviews and dismissed them: the timeline of #75 records two `review_dismissed` events with `actor=github-actions[bot]`, `state=changes_requested`, message `Superseded: latest AI review found no blocking issues.` So the create path and the dismissal path both work under this repository's settings. That the approvals land at all means sei-protocol allows Actions to approve a pull request. GitHub refuses the call outright when that setting is off, and none of these calls was refused. **Cannot.** A `COMMENT` review casts no vote. Where the rung fires, an `APPROVE` clears no approval requirement and a `REQUEST_CHANGES` blocks no merge. The check run remains the gate in both cases. ## The branch-protection question **Neither identity clears the review requirement on `sei-chain` today.** sei-chain PRs #4094 and #4100 each carry exactly one review — `seidroid[bot]` `APPROVED`, from the App — and GraphQL `reviewDecision` on both reads `REVIEW_REQUIRED`. sei-chain's `main` is protected and carries `.github/CODEOWNERS`. So a bot approval, App-authored or not, does not satisfy that gate now. The fallback costs nothing there that the App identity was buying. `uci` itself has no gate to satisfy. `main` reports `protected: false`, and the one ruleset (id 7955617, `~DEFAULT_BRANCH`) is `enforcement: disabled`. Were it enabled it would ask for `required_approving_review_count: 1`, `required_reviewers: []`, `require_code_owner_review: false`, `dismissal_restriction.enabled: false` — a rule that names no reviewer and restricts no dismissal. ## What I could not verify - **Whether a `github-actions[bot]` approval satisfies a required-approval rule.** `GET /repos/sei-protocol/uci/actions/permissions/workflow` and the org equivalent both returned 403 for my token, so I could not read `can_approve_pull_request_reviews` directly — the successful approvals above are the inference. No repository in reach has a merge gate that an Actions approval has ever been put to. - **sei-chain's protection detail.** `GET /repos/sei-protocol/sei-chain/branches/main/protection` returns 404 for my token. CODEOWNERS is the probable reason `reviewDecision` stays `REVIEW_REQUIRED`, not a measured one. - **A live run.** Nothing here ran on a runner. The evidence is the incumbent's history, the API state above and the local battery below. - **The `COMMENT` rung under a real refusal.** I never saw the API refuse a position, so the rung is exercised against a stub, not against GitHub. ## Two consequences worth naming 1. A repository with no App now runs the withdrawal where it previously skipped the whole step. A dismissal the API refuses fails the job, by the design this step already states. That is the intended loud failure, and it is new exposure for those repositories. 2. If the first post lands server-side but the client reports a failure, the `COMMENT` retry writes a second review. `ai-review.yml:968-991` carries the same hazard; the cost is one extra review, never a changed gate. ## Verification `actionlint` 1.7.12, against base `543f11f`: ``` base: 4 findings, exit 1 -> 4 SC2102:info new: 4 findings, exit 1 -> 4 SC2102:info rule set diff: identical ``` Both findings pre-date this change and sit in steps it does not touch. ``` $ python3 -c "import yaml; yaml.safe_load(open('.github/workflows/seidroid-review.yml'))" yaml ok $ shellcheck -s bash <the position step's script> (clean) ``` **Behaviour battery.** I extracted the step's script from the YAML at both revisions, put a stubbed `gh` on `PATH`, and ran twelve paths against each: event accepted, event refused, every event refused, approve off, blocking, blocking with the event refused, dismissals refused, review list unreadable, neutral with a blocker, neutral with nothing written down, neutral with no counts, and no check file. The base and the new step produce byte-identical output on every path but the two where the new `COMMENT` rung fires: ``` --- clean, approve on, event refused -::warning::could not record APPROVE ...; the verdict comment stands +::warning::... would not take APPROVE, so this review is recorded as a comment; + the check run carries the success conclusion --- blocking, request changes refused -::warning::could not record REQUEST_CHANGES ...; the verdict comment stands +::warning::... would not take REQUEST_CHANGES, so this review is recorded as a + comment; the check run carries the failure conclusion ``` Both loud failures still exit 1: the stuck dismissal and the unreadable review list. All four withdrawal interlocks are unchanged — a blocker beside a soft conclusion stops the withdrawal, a soft conclusion with zero counts stops it, absent counts let it proceed with a warning, and a `failure` conclusion exits before it. ## One thing the ticket got wrong The ticket's phrasing — "drop the identity requirement from the step's condition" — was written against a base that still carried the cross-repository target grammar. `bf507f3` removed that grammar, so the step now only ever posts to `github.repository` and dropping the requirement outright is safe. Against the older base it was not: `github.token` reaches no other repository, the withdrawal's review listing would have 404'd, and the step would have failed the job reporting a block it could not see. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Implements PLT-1148. seidroid review is now only invokable on the repository
the pull request is on.
@seidroid review owner/name#123reviewed a pull request in another repository.ai-review.ymlnever supported this — every call there usescontext.repo— sothis is net-new capability the rewrite introduced, withdrawn by decision.
What went
31 expression occurrences of
review_repo/review_repo_name/review_pron27 lines, across 11 steps plus the guard's
outputs:block and the review job'sconcurrency.group. Plustarget_re, the guard's target-parse block, the deadREPO_OWNERenv entry, thePlace findings on the codeidentity-or-same-repocondition (a tautology once there is no target), and 17 prose sites.
No
workflow_callinput changes, so neither caller breaks.What replaced it
An explicit refusal.
ai-assistant.ymlreserves only the exact body@seidroid review, so the withdrawn form would otherwise fall through and get aconversational answer — a worse signal than silence, because it looks like the
system worked.
A bug found and fixed inside this change
The refusal's first cut read a
grep -qexit status. Underset -o pipefailthat is wrong:
-qexits at the first match,printfis then killed by SIGPIPEwriting into a closed pipe, and
pipefailpropagates 141 — so the refusal issilently not written. Measured: at 232 kB the pipeline returned 141 and the
refusal was missed.
The threshold is a race between
printf's write andgrep's read-then-exit, soit passes every small-body test and fails nondeterministically on a long
comment. GitHub's comment cap is 65,536 characters, so the input is reachable.
Fixed by capturing through
grep -m1 … || trueand testing for emptiness — thesame idiom the
cmdline=line three lines above already uses. Re-measuredthrough the extracted script: the refusal fires at 1 kB, 65 kB and 200 kB, one
::error::and oneshould_runkey each time.Two traps avoided
repositories:stays on both App-token mints. Withownerset andrepositoriesabsent, the token covers every repository the installationreaches — a privilege widening disguised as cleanup.
The regex target group and the parse block go in the same commit as the
expression collapse. A half-removal parses a target, admits it, and reviews the
local pull request while the requester believes otherwise.
Verification
20 primary behavioural rows plus 12 supplementary, driven through the extracted
parsescript. Every row required to stay identical is byte-identical to thebase — including a CRLF body, tab separators, uppercase, backtick-quoted
mentions, and both malformed-target shapes.
One drift row beyond the four expected: a body carrying both a target line
and a valid bare command line. The base matched the target; this matches the
bare line and runs an ordinary local review with no annotation. That is the
right precedence and the honest signal.
Not changed, worth a look
The refusal annotates but does not fail.
::error::does not fail a step, sothe guard still concludes
successand the requester may never open the run —while
ai-assistant.ymlstill answers the target form conversationally.Widening that reservation would put the refusal where the request was written.
Out of scope here.
github.repositoryin theconcurrency.groupis now redundant — a reusableworkflow's group is already scoped to the calling repository. Kept, because
dropping it would change a live group key for any in-flight run.
Pre-merge check
The driver keys a session on the reviewed
(repo, pr)pair, so a session createdby a cross-repo review becomes unreclaimable through this workflow — and the
launcher sets no lifetime cap while the server runs no sweep. Enumerate live
sessions for a key naming anything other than a caller's own repository, or
search run history for the guard's
::notice::reviewing <target>annotation, andclose any found before a caller's pin moves.
🤖 Generated with Claude Code