Automated Issue Review - #276
Conversation
Add .github/workflows/speckit-reviewissue.yml so labelling an issue `review`
produces a /speckit.reviewissue gap analysis as a comment on that issue, with
no terminal involved.
The review logic stays single-sourced in .claude/commands/speckit.reviewissue.md
— the prompt instructs Claude to read and apply that file rather than restating
it, so the workflow carries only environmental deltas: the issue snapshot, the
default branch, and the trigger-label cleanup.
Guards, per the issue's confirmed decisions:
- `github.event.sender.login` gates on the account that applied the label, not
the account that raised the issue (the repository is public).
- The run exits without posting when a `<!-- speckit:review -->` comment already
exists, and removes the `review` label only after a successful post, so
"labelled" means pending and "unlabelled with a review comment" means done.
- No failure comment: a failed run leaves the label in place, making "still
labelled, no review" the visible failure state.
claude.yml is untouched, so `@claude` on issues and PRs behaves exactly as before.
RED-GREEN (Constitution Principle I, configuration/tooling carve-out) — the gate
is the real trigger, not a hand-rolled test:
RED, before this change:
$ python3 - <<'PY' # parse every workflow's `on:` block
...
Workflows triggered by the issues event: NONE
$ gh run list --limit 50 --jq '[.[]|select(.event=="issues")]|length'
0
GREEN, after:
.github/workflows/speckit-reviewissue.yml: on = ['issues', 'workflow_dispatch']
issues trigger types: ['labeled']
A workflow file cannot be triggered by an event from a feature branch, so the
end-to-end evidence is merge-then-verify: the real label trigger observed working
on main against a throwaway issue, linked in the PR. `workflow_dispatch` with an
`issue_number` input makes every later prompt tweak runnable against a branch ref
instead of another merge.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01YaRuiUQHdPMeGugFuvGfeK
Add the CI table row and a short section to docs/agentic-workflow-NetPace.md explaining the label-as-state contract (labelled = pending, unlabelled with a review comment = done, still labelled with no comment = failed), and a CIR recording why this is a separate workflow, why the issue is snapshotted rather than granting Claude a third `gh` verb, and why there is no failure comment and no --max-turns. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]> Claude-Session: https://claude.ai/code/session_01YaRuiUQHdPMeGugFuvGfeK
Three clean-context reviewers converged on two defects that broke the contract the issue's confirmed decisions describe, plus a set of doc claims that outran the implementation. Workflow correctness: - The action exits green whenever the model finishes its turn — a failed `gh issue comment` is tool output it reads, not a step failure. So "GitHub's failed-run notification is the alert" never fired on the failures most likely to happen. Add a verification step that re-reads the issue and fails the job unless the `<!-- speckit:review -->` sentinel is present and the label is gone. - Move the already-reviewed guard out of the prompt into a `run:` step, and have it clear the label. Previously "post nothing, change nothing" left label + review comment, which the docs' own state model reads as "pending" — forever, and reachable by reviewing locally then labelling. - Add a per-issue `concurrency` group: the guard reads the comment list twenty minutes before the post, so overlapping runs both passed it and both posted, orphaning answers — the exact harm the guard exists to prevent. - Grant `Read,Glob,Grep,Write`. The prompt requires the Write tool and the whole codebase-grounding pass requires reads; if `--allowedTools` is exhaustive these were missing, and if it is additive they are merely explicit. Correct either way. - Paginate the sentinel scan, validate the snapshot is well-formed JSON, and write `gh` output to a file before grepping so a failed fetch fails the step instead of being swallowed by a pipeline's exit code. Doc corrections — several claims were simply untrue: - CIR decision 1 justified a separate workflow by permissions widening. `claude.yml` already grants a strict superset, so it would widen nothing; the real reason is independent failure and blast radius. - "Workflows execute the default branch's copy" was stated unqualified eight lines above the `workflow_dispatch` trigger that contradicts it. - "Given: an account with permission to label it" described a system that does not exist; the gate is one named account. - "Exactly the two writes it needs" was wrong: the checked-out `.claude/settings.json` grants `Bash(gh:*)` and `Edit(**)`, and `--allowedTools` adds to that rather than replacing it. Recorded as a known residual instead. - "Author-gated" in the CI table is the term the workflow's own comment disclaims; the gate is on the labeller. Also records why the snapshot step is necessary rather than merely convenient: the action sanitises injected content by stripping HTML comments, which would destroy the `<!-- speckit:review -->` sentinel the whole design keys on. Full suite green: 648 passed, 0 failed, 0 skipped. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]> Claude-Session: https://claude.ai/code/session_01YaRuiUQHdPMeGugFuvGfeK
|
@claude Review this pull request. Analyse the code changes and provide feedback covering:
End the review with a recommendation whether to merge the PR, and if not, what you suggest needs addressing first. |
|
Claude finished @FrankRay78's task in 3m 17s —— View job Review of #276 — Automated Issue Review
I read all three changed files ( Bugs / correctnessNone found. I traced the control flow carefully:
Security
CLAUDE.md / constitution adherence
Test coverageAdequate for what this change is. There's no C# production code, so xUnit coverage doesn't apply. The Principle I carve-out is the right lens, and it's satisfied per above. The one honest gap — real label-triggered E2E on Spec/CIR consistencyChecked the three declared prompt deltas against RecommendationApprove / merge. No blockers found. The three deliberate deviations from the issue's confirmed decisions are well-argued and necessary (HTML-comment stripping would have destroyed the sentinel either way), and the two previously-found defects are genuinely fixed rather than papered over. The remaining items (allowlist being cosmetic, non- |
Why
/speckit.reviewissueis the pre-specification gate, and it is the one step in the loop whose cost is the wait — it carries the codebase-grounding pass. Running it only from a developer machine means the first step of a three-step loop gates the other two: an issue raised from a phone sits untouched until the author sits down. Labelling an issuereviewnow runs it unattended, so the gap analysis is waiting rather than requested.What changes
reviewlabel to an issue produces a pre-specification gap analysis as a comment on that issue — numbered gaps, a recommendation per gap, an inline answer slot — grounded in this codebase's real paths and conventions. The author answers inline and runs/speckit.confirmissueexactly as before.<!-- speckit:review -->comment = done, still labelled = the run did not complete. There is no failure comment; GitHub's failed-run notification is the alert.claude.ymlis untouched, so@claudeon issues and PRs behaves exactly as before.The review logic itself is not duplicated — the workflow prompt reads
.claude/commands/speckit.reviewissue.mdand applies it, so the analysis stays single-sourced and a change to the command file takes effect without touching the workflow.Non-obvious things a reviewer should know
Three deliberate deviations from the issue's Confirmed decisions, each because the decision as written was not implementable or did not hold:
ghinstead of reading the context the action injects. The confirmed decision said injection made fetching unnecessary. It does not: the action sanitises injected content by stripping HTML comments, which would destroy the<!-- speckit:review -->sentinel that the guard,/speckit.confirmissue, and the entire design key on. Separately,workflow_dispatchcarries no issue payload to inject at all.if:gained aworkflow_dispatchdisjunct. The confirmed expression alone would have made the dispatch path — which the same decision mandates — unreachable.--allowedToolsgainedRead,Glob,Grep,Write. The prompt requires the Write tool, and the whole codebase-grounding pass requires reads. The confirmed decision was about which writes to GitHub Claude gets; bothBash(gh …)grants are unchanged.Two defects found in review that broke the contract the confirmed decisions describe, both fixed in
b1d6d00:claude-code-actionexits green whenever the model finishes its turn — a failedgh issue commentis tool output the model reads, not a step failure. A prompt instruction saying "let the job fail" cannot be honoured. So the run that posted nothing went green and notified nobody, which is precisely the state "GitHub's failed-run notification is the alert" was meant to cover. There is now a verification step that re-reads the issue and fails the job unless the sentinel is present and the label is gone.run:step (a model can skip a prompt instruction) and clears the label.Known residuals, all recorded in the CIR:
--allowedToolsadds to the permission rules Claude Code loads from the checked-out.claude/settings.json, which already grantsEdit(**),Bash(gh:*)anddefaultMode: acceptEdits. The real grant is everyghsubcommand, and the prompt's "do not edit the issue body" is a request, not an enforced limit. Narrowing it properly means changing settings that govern every local session — separate work.issues: writetoken. The label gate is the mitigation: untrusted text only reaches the model if a maintainer deliberately labels it.FrankRay78is silent. The job-levelif:means GitHub creates no run, so nothing appears in the Actions tab and no notification fires — yet the resulting state (labelled, no review) is the one reserved for a failed run. The gate expression is pinned by the confirmed decisions, so this is flagged rather than changed.fetch-depth: 0andid-token: writeare both unused here and were flagged in review. Kept deliberately for parity with the known-workingclaude.yml: this workflow cannot be triggered before merge, so changes were confined to correctness rather than untested tidying.RED–GREEN evidence (Constitution Principle I, configuration/tooling carve-out)
The gate is the real trigger, not a hand-rolled test. No bespoke test was written to stand in for GitHub Actions.
RED, before the change — parsing every workflow's
on:block:GREEN, structural, after:
GREEN, end-to-end, is merge-then-verify and is still outstanding — no event can trigger a new workflow file from a feature branch. Per the issue's own confirmed decision, the completing evidence is the real label trigger observed working on
mainagainst a throwaway issue. I will link that run here after merge.How to verify
.claude/commands/speckit.reviewissue.mdis unmodified — the analysis must stay single-sourced, and the prompt delegates rather than restating it.gh repo viewdefault-branch resolution, step 5's heredoc.claude.ymlis untouched; mention@claudeon any issue or PR and confirm unchanged behaviour.review. Confirm a review comment appears citing at least three repo-specific paths, and that thereviewlabel is removed.reviewto that same issue. Confirm no second comment is posted and the label is cleared again.workflow_dispatchwith anissue_numberinput works against a branch ref.Related
Closes #271
.claude/commands/speckit.reviewissue.md— the review logic this automates..claude/commands/speckit.confirmissue.md— the downstream step that consumes the review comment; it finds the review by sentinel and does not filter on author, which is what lets a CI-posted comment be folded in unchanged.docs/change-intent-records/2026-09-07-automated-prespec-review.md— decisions and residuals.🤖 Generated with Claude Code
https://claude.ai/code/session_01YaRuiUQHdPMeGugFuvGfeK