ci: rebuild task board state machine with composite actions + bidirectional sync#75
Conversation
Summary of ChangesHello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed! This pull request overhauls the Task Board state machine to improve consistency and reduce manual overhead. By standardizing assignee roles and implementing bidirectional status mirroring, the system now ensures that PRs and their linked issues remain in sync regardless of whether transitions are triggered by automated workflows or manual board interactions. The refactor also consolidates redundant logic into reusable composite actions, simplifying maintenance and improving the reliability of the orchestrator. Highlights
Ignored Files
Using Gemini Code AssistThe full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips. Invoking Gemini You can request assistance from Gemini at any point by creating a comment using either
Customization To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a Limitations & Feedback Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for GitHub and other Google products, sign up here. Footnotes
|
There was a problem hiding this comment.
Code Review
This pull request introduces several composite GitHub Actions to automate and synchronize Task Board operations, including reviewer assignment, item upsertion, and status mirroring for linked issues. It also updates AGENTS.md to document the refined state machine logic. Feedback focuses on optimizing API usage by avoiding N+1 query patterns in GraphQL and batching assignee updates to improve performance.
There was a problem hiding this comment.
Pull request overview
Rebuilds the CI-driven Task Board state machine around reusable composite actions, switches to set-once-per-card assignees with mirrored PR↔Issue statuses, surfaces Dependabot major bumps onto the board, and adds bidirectional sync when humans manually move cards.
Changes:
- Introduces composite actions to upsert board items, apply assignee+review-request together, and mirror status to linked issues.
- Refactors
project-orchestrator.ymlto the new mirrored-status model (including the missing linked-issue transition onreeval) and updates required checks for bot-clean. - Extends Dependabot major-bump handling and adds a new manual-move mirroring workflow (
projects_v2_item: edited), plus updates docs + changelog.
Reviewed changes
Copilot reviewed 8 out of 8 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
CHANGELOG.md |
Documents the new composite actions, bidirectional sync, Dependabot major bump surfacing, and orchestrator refactor. |
AGENTS.md |
Updates the authoritative “Task Board state machine” spec to set-once assignees + mirrored statuses + bidirectional sync semantics. |
.github/workflows/project-orchestrator.yml |
Refactors orchestration logic to use composite actions and implements the mirrored-status transitions (incl. reeval linked-issue promotion). |
.github/workflows/dependabot-automerge.yml |
Adds board placement + assignment/review-request for major bumps via the shared composite actions. |
.github/workflows/board-state-sync.yml |
Adds new workflow to mirror manual Status moves across linked PR↔Issue counterparts with loop prevention. |
.github/actions/board-upsert-status/action.yml |
New composite action to idempotently add to project + set Status. |
.github/actions/assign-and-request-review/action.yml |
New composite action to keep PR assignees and requested reviewers in sync. |
.github/actions/set-linked-issues-status/action.yml |
New composite action to update the board Status for PR-linked issues. |
|
Claude finished @EricAndrechek's task in 3m 14s —— View job Review — ci: rebuild task board state machineThis PR has been through eight Copilot rounds and one prior Claude pass; most obvious gaps are closed. Two issues remain. Findings[Medium]
|
|
Pushed What went wrong on this PR's smoke test:
Root cause: Fix:
Three re-eval paths now (previously only one worked):
The fix itself won't help verify this PR's behavior since — Posted by Claude Code on behalf of @EricAndrechek |
Bundle addressing the five legitimate review findings on #75: 1. [Copilot] reeval guard compared Status by display *name* ("Backlog" / ""); renaming the project option would silently break the guard. Switched to optionId comparison against STATUS_BACKLOG, with name kept for log readability. 2. [Gemini] assign-and-request-review looped per-user through `gh pr edit --add-assignee`. gh accepts a comma-separated list — one call assigns everyone, idempotent on already-assigned. 3. [Copilot] set-linked-issues-status description said "fetches all linked issues" but the query had `first: 10`. Bumped to first: 50 + totalCount check + a warning log when the cap is hit. Updated description to reflect the 50-item ceiling. 4. [Gemini] N+1 GraphQL in set-linked-issues-status: the old loop made one query per linked issue to fetch its project items. Collapsed into a single query using the PR's closingIssuesReferences with a nested projectItems lookup — one round-trip for any number of linked issues up to the cap. The jq extraction uses `map(select ...)[0]` to safely pick the single matching project item per issue. 5. [Copilot] board-state-sync's `echo $counterparts` was unquoted (SC2086). Switched to `printf '%s '` with a trim, same semantics without the shellcheck warning. Also fixed a comment that referred to `closingPullRequestsReferences` while the code used `closedByPullRequestsReferences`. Open intentionally: the Copilot comment flagging the Issue-side query falling back to a timelineItems/ConnectedEvent heuristic. We'll address that *if* we observe closedByPullRequestsReferences being unavailable in practice — no need to pre-emptively write the fallback.
|
Pushed Copilot round 2 (4 findings, all legit):
All 4 threads replied + resolved. Admin-approval stale-red fix (long-standing Eric-observed issue):
Also merged — Posted by Claude Code on behalf of @EricAndrechek |
|
Pushed Claude's findings (all addressed):
Copilot's 3 unresolved:
On the All threads resolved after this round. — Posted by Claude Code on behalf of @EricAndrechek |
… sync
Phase 1 of the Task Board state-machine rebuild. Three changes, all
grounded in extracting shared board operations into composite actions:
1. Three composite actions under .github/actions/:
- board-upsert-status: add-to-board-if-missing + set Status,
idempotent (no-op when already correct).
- assign-and-request-review: set PR assignees and request reviews
together, so the board queue and GitHub notifications stay in
sync. The set-once assignee model (Issue = implementer, PR =
reviewer) means this only runs on reeval/re_requested, not
on every transition.
- set-linked-issues-status: iterates a PR's closingIssuesReferences
and upserts each to the given Status. Replaces three duplicated
inline loops in the orchestrator.
2. project-orchestrator.yml refactor:
- Uses the three composite actions throughout.
- Adds the missing "linked issues → In review" step on the
bot-clean reeval path (previously only the re_requested path
moved linked issues; the initial Ready transition silently
left them in Backlog / Ready).
- Extends the bot-clean required-checks list to match the updated
ruleset: Check, Build, Validate, Lint, Test, Integration Tests,
SDK Tests. (Admin approval deliberately excluded — self-ref.)
- Preserves the "don't yank In progress / In review / Done back
to Ready" guard.
- Workflow length drops from 476 → 381 lines despite adding
functionality, because the composite actions own the GraphQL.
3. dependabot-automerge.yml:
- Major-version bumps now get added to the Task Board with
Status=Ready and both admins assigned + review requested.
Parity with the non-Dependabot reeval path. Uses the same
composite actions, so no duplicate shell.
- Patch/minor path unchanged.
4. board-state-sync.yml (new):
- Listens on projects_v2_item: edited, filtered to Status-field
changes via changes.field_value.field_node_id.
- Mirrors moved card's Status to the linked counterpart(s):
PR Ready ↔ Issue In review, PR In review ↔ Issue Ready,
both Done together. Ignores In progress (manual mid-review
state — don't clobber).
- Loop-prevention guard: no-ops when the target is already in
the expected mirror state, so the mirror's own Status write
doesn't re-fire the workflow.
AGENTS.md §Task Board state machine rewritten as the source of
truth for this model. CHANGELOG entry covers all four pieces.
Known limitations carried forward:
- projects_v2_item is a real webhook event but actionlint's
outdated event list flags it as unknown. Harmless false positive.
- Orchestrator reeval paths and board-state-sync haven't been
exercised end-to-end on a non-Dependabot internal PR yet —
this PR is the natural smoke test. If regressions surface,
fix-forward rather than revert.
Smoke-testing on this PR surfaced a pre-existing bug we inherited
from main: the orchestrator never re-evaluates bot-clean state after
CI finishes on an internal PR. Root cause:
- check_suite: completed was the original "CI finished" signal,
but GitHub suppresses check_suite events when the suite was
created by a GITHUB_TOKEN-triggered workflow. Our CI uses
GITHUB_TOKEN, so check_suite was never firing.
- pull_request_review: submitted was the fallback, but my state
machine mapped COMMENTED reviews (bots: Gemini, Copilot) to
action=none. So Gemini's review comment didn't trigger re-eval.
- That left only pull_request_target: synchronize (pushes) as a
working re-eval trigger. On a PR that passes CI on the first
push and has no subsequent pushes, the orchestrator never fires.
Fix:
- Swap check_suite for workflow_run watching the CI workflow.
workflow_run is not suppressed by GITHUB_TOKEN provenance and
is the documented chain-off-another-workflow trigger.
- COMMENTED pull_request_reviews → action=reeval. A bot comment
is usually a good signal that CI has progressed and a re-eval
is worth doing. The reeval path is idempotent (guarded).
- APPROVED stays action=none — admin-approval.yml + merge flow
handle that path.
Three independent re-eval paths now: push, bot review, CI completion.
CHANGELOG entry extended with the triggering-model explanation.
Bundle addressing the five legitimate review findings on #75: 1. [Copilot] reeval guard compared Status by display *name* ("Backlog" / ""); renaming the project option would silently break the guard. Switched to optionId comparison against STATUS_BACKLOG, with name kept for log readability. 2. [Gemini] assign-and-request-review looped per-user through `gh pr edit --add-assignee`. gh accepts a comma-separated list — one call assigns everyone, idempotent on already-assigned. 3. [Copilot] set-linked-issues-status description said "fetches all linked issues" but the query had `first: 10`. Bumped to first: 50 + totalCount check + a warning log when the cap is hit. Updated description to reflect the 50-item ceiling. 4. [Gemini] N+1 GraphQL in set-linked-issues-status: the old loop made one query per linked issue to fetch its project items. Collapsed into a single query using the PR's closingIssuesReferences with a nested projectItems lookup — one round-trip for any number of linked issues up to the cap. The jq extraction uses `map(select ...)[0]` to safely pick the single matching project item per issue. 5. [Copilot] board-state-sync's `echo $counterparts` was unquoted (SC2086). Switched to `printf '%s '` with a trim, same semantics without the shellcheck warning. Also fixed a comment that referred to `closingPullRequestsReferences` while the code used `closedByPullRequestsReferences`. Open intentionally: the Copilot comment flagging the Issue-side query falling back to a timelineItems/ConnectedEvent heuristic. We'll address that *if* we observe closedByPullRequestsReferences being unavailable in practice — no need to pre-emptively write the fallback.
Copilot findings on the Phase 1 PR, all legitimate: 1. reeval path's assign-and-request-review step gated only on bot-clean, not on promote == true. When the PR card Status was already "In progress" / "In review" / "Done," we'd still re-request review from the reviewer on every re-eval cycle — noise. Added the promote guard. 2. workflow_run fires for CI on every push to main too (CI triggers on both push:main and pull_request). A main-push CI run has no associated PR so the orchestrator would early-exit at PR-number resolution, but still spin up a runner. Added a job-level if to short-circuit when workflow_run's triggering event isn't pull_request. 3. board-state-sync's linked-counterpart queries capped at first: 10. Bumped to 50 to match set-linked-issues-status. 4. set-linked-issues-status declared github-token as required but the consolidated single-query version uses project-token for every read. Removed the dead input; callers drop the parameter. Plus a long-standing issue Eric raised — admin-approval staying red after a valid approval. Two root causes: - concurrency.cancel-in-progress: true was producing CANCELLED check runs when events raced. CANCELLED shows as not-passing in the ruleset gate, so a cancelled-mid-flight post-approval run would leave the check red even though the logic would have succeeded. Flipped to cancel-in-progress: false — runs queue and complete sequentially. - The reviews API has observable submit-event lag. Added a fast- path: when the triggering event is itself an admin APPROVED submit, trust the event payload directly (review.state + review.user.login) rather than polling the API. Eliminates the lag-induced false negative for the common case. Slow path (API poll) now retries 3× with 2-second backoff for events that aren't themselves a submit. Also merged origin/main (the #61 goreleaser bump) into the branch to clear the "out of date" merge block.
Four more findings, all legit:
1. board-state-sync's "Inspect event" step set PROJECT_TOKEN in env
but the script only reads $GITHUB_EVENT_PATH via jq. Removed the
unused env var.
2. Reviewer selection on the reeval path picked *all* non-author
admins, giving 2-assignee PRs whenever a non-admin authored. That
contradicts AGENTS.md §Task Board state machine which specifies
*one* reviewer-assignee per PR card. Switched to deterministic
single-pick:
- author Eric → reviewer Taite
- author Taite → reviewer Eric
- other author → alternate by PR-number parity (spreads load)
3. board-upsert-status description said project-token only needed
project:write, but the resource(url) GraphQL read also requires
repo:read on private repos. Updated description.
4. Same mismatch in board-state-sync.yml header comment — it called
out project:write only, but closingIssuesReferences /
closedByPullRequestsReferences GraphQL reads need repo:read too.
Updated comment.
Medium-severity findings from Claude's re-review, plus three unresolved Copilot threads tied to the same work. 1. board-state-sync.yml guard used \`exit 0\` on missing PROJECT_BOARD_TOKEN — which only exits the step shell, not the job. Subsequent steps would run with an empty GH_TOKEN and fail. Switched to the SKIP_BOARD=true env-var pattern used in project-orchestrator.yml (Claude Medium #1). 2. project-orchestrator.yml: when SKIP_BOARD is set, the pr-status step is skipped and its \`promote\` output is unset, which caused the Assign + request review step's \`promote == 'true'\` condition to silently fail — skipping reviewer assignment even though that operation uses GITHUB_TOKEN and doesn't need the board token. Extended the condition to also allow \`env.SKIP_BOARD == 'true'\` (Claude Medium #2). 3. dependabot-automerge.yml major-bump flow: reordered so the token guard + board-add + assign steps run before the comment, and the comment wording is now conditional on SKIP_BOARD — previously the comment claimed "Added to the Task Board" before the board-add even ran, and if the token was missing the claim was false (Claude Low #3 / Copilot R4 restated). 4. AGENTS.md §Review tooling reference table — three stale facts corrected to match the new implementation: - review-request channel IS used now (not "intentionally not") - re-eval trigger is workflow_run + bot COMMENTED reviews, not check_suite (documented the GITHUB_TOKEN suppression reason) - reviewer selection is single-pick parity-based, not "both admins" (Claude Low #4) 5. Copilot R4 remaining: board-state-sync comment referenced \`closingPullRequestsReferences\` on a line I hadn't fixed in an earlier round. Verified all references now align on \`closedByPullRequestsReferences\` (the actual field name). Also added a SKIP_BOARD guard to every subsequent step in board-state-sync.yml so the "not-configured" path produces a clean no-op instead of a cascade of failing gh calls.
…ic retry Two more findings, both correctness: 1. board-state-sync.yml Issue-side counterpart lookup swallowed all gh api errors via `2>/dev/null || true`. That masked real failures (bad token scope, API outage) as "schema field missing" and silently skipped mirroring. Refactored to capture the response, check .errors in the GraphQL payload, and only treat "closedByPullRequestsReferences doesn't exist" as the known-skip case. Real GraphQL errors now fail the workflow loudly. 2. admin-approval retry loop's break condition was `-n $approvers` — triggering on ANY approval, not just an admin's. If a non-admin approval was already on the PR (e.g., external contributor scenario), the loop would break on the first attempt even if a newly-submitted admin approval hadn't propagated through the reviews API yet. Changed the break condition to "an admin login appears in approvers" — retry until we see the actual admin, or until attempts run out.
Three more iterations on error handling — Copilot pointed out that several workflows were swallowing too much behind warnings, which could mask regressions we'd want to notice. 1. assign-and-request-review: split the request-reviewers API failure path. 422 (stale-approved-on-head-SHA) is an expected post-dismiss-stale outcome and stays a notice. Every other failure (rate limit, permissions regression, network) now prints the gh stderr verbatim so operators can debug. 2. admin-approval retry loop: distinguish 'API succeeded with no admin approval yet' (retry for propagation) from 'API failed' (surface error, fail the workflow after 3 attempts). The blanket `2>/dev/null || echo ""` previously made a broken API look like 'nobody approved.' 3. board-state-sync linked-counterpart skip: the previous 'let orchestrator add it later' comment was wishful — the orchestrator only runs on PR events, not on projects_v2_item: edited, so counterparts not on the board silently lost their mirror. Upgraded from plain echo to ::warning:: with debug guidance (re-trigger by bumping Status after manual add). We're not automating the add because we only have node IDs at this point, not URLs, and the "someone removed the card manually" case is probably intentional.
…acklog Copilot R6 flagged that my round-4 `promote`-based gate on the reeval Assign step had an inverse gap: if a PR card was already on the board with Status = In progress / In review / Done (e.g., someone manually moved it before the first bot-clean re-eval), then promote=false, and the Assign step would silently skip reviewer assignment AND review request — even though those should happen on first bot-clean regardless of the board state. Fix: remove the board-status gate from the orchestrator entirely and push idempotency into the composite action instead. The composite now: - queries current PR assignees + requested-reviewers - computes the subset of desired reviewers that are missing from BOTH lists - exits early with a log message if everyone's already present - otherwise adds/requests only the missing users That handles both directions cleanly: - PR already In progress with reviewer set → composite no-ops (no re-spam, same behavior as before) - PR already In progress without reviewer (the gap) → composite sets the missing reviewer No-op behavior on SKIP_BOARD also works — assignment doesn't depend on the board token at all. Didn't consolidate the CHANGELOG Unreleased section (Copilot R6 #1) — the duplicate ### Fixed / ### Changed headers are pre-existing from #55/#65/#71 accumulating, and full consolidation is release-cut scope. Replied to that thread deferring.
…up_by
Two Copilot R7 findings:
1. assign-and-request-review's idempotency check was 'missing
from both sides' (AND), which left the pair unable to repair
out-of-sync states — e.g., dismiss_stale_reviews_on_push
clears the review-request without touching the assignee, and
the old code would see the user as 'already assigned' and
skip restoring the review request. Fixed by splitting the
reconciliation per-side: MISSING_ASSIGNEES triggers a
gh pr edit --add-assignee on just that subset, and
MISSING_REVIEW_REQUESTS triggers a POST /requested_reviewers
on just that subset. Either side being clean skips only that
half. Both server APIs are idempotent so overlapping subsets
are free.
2. admin-approval reviews-API jq filter used group_by without an
explicit sort_by. jq's group_by sorts internally so the
filter worked correctly, but adding sort_by(.user.login)
ahead of group_by documents the invariant ('one group per
user login, ordered by login') and shields against any
future jq version change or environmental quirk that might
flip the default. Zero functional difference today.
Two findings, one security + one correctness.
1. actions/checkout on pull_request_target / workflow_run events
defaulted to the event SHA — which on PR events resolves to
the PR's merge/head commit. That means a PR modifying
.github/actions/**/action.yml could inject code executed with
PROJECT_BOARD_TOKEN + write-scoped GITHUB_TOKEN in scope (the
well-known pull_request_target attack). Pinned both the
orchestrator and dependabot-automerge checkouts to
github.event.repository.default_branch so composite actions
are ALWAYS read from trusted main code, regardless of event.
Also set persist-credentials: false to reduce token exposure
— we don't push from either workflow.
2. Orchestrator's pr-status (reeval guard) step didn't use
strict mode. A failed gh api graphql call (scope regression,
network) would yield an empty response; jq extracts empty
strings; the subsequent `[ -z "$current_option" ]` would
incorrectly evaluate true and set promote=true — promoting
a card we couldn't actually read. Added:
- set -euo pipefail at top of the step
- explicit check for .errors in the GraphQL response
- distinguishes "not on board" (OK to promote) from "on board
with empty Status" from "on board, other" so the branch
logic is unambiguous even under partial failures
Same strict-mode added to 'Determine action' and
'Check bot-clean' steps while I was there.
Still haven't consolidated CHANGELOG (tracked in #76) — will
stay open through round 9 if that's the only outstanding item.
…ywhere Four findings, theme is 'the logic that was supposed to be defensive was still silently collapsing real failures to no-op-like behavior.' 1. Reeval re-request spam (real behavioral gap): my R7/R8 'repair the pair' logic in the composite was re-requesting review on every re-eval cycle after a reviewer submitted CHANGES_REQUESTED, because GitHub clears a user's review-request when they submit. The composite saw 'missing review request' and re-fired. That re-notifies the reviewer before the author has explicitly re-requested, defeating the re_requested signal. Added request-review input (default true) to the composite. On the orchestrator's reeval path, only passes true when promote=true (first-time Ready transition); otherwise false (assign-only, gap-repair without re-notification). re_requested path always passes true. Dependabot major-bump always passes true. 2. board-upsert-status composite: no .errors or null-resource check. A successful HTTP response with GraphQL-level errors (bad scope, URL permissions) would leave item_id empty and trip the 'item not on board — adding' branch, masking the failure as a spurious re-add. Added explicit fail-fast on .errors / null resource. 3. set-linked-issues-status: same pattern. Added .errors check + '.data.repository.pullRequest != null' check up front. 4. board-state-sync counterpart lookup: same pattern. Added .errors check + '.data.node != null' check. The previous missing-counterpart skip is now specific to the 'node exists but isn't on our project' case, not collapsed with the 'query failed' case.
Previous CI run on b4ae257 had Integration Tests and E2E Tests fail pre-execution (no runner assigned, empty steps, ~2 second duration). Both reruns via gh run rerun --failed produced the same pre-execution failure. No ci.yml or test-file changes on this branch could cause this — appears to be a GitHub Actions infrastructure / runner-pool transient. Forcing a completely fresh workflow dispatch.
Completes the ARC runner migration for the new workflow added in this PR. PR #77 swapped every existing workflow's runs-on from ubuntu-latest to [wave-rf-runners]; the new board-state-sync.yml introduced here was branched off pre-#77 main and missed the sweep. Without this, the mirror job would queue indefinitely on the now-empty ubuntu-latest pool.
5f80fd8 to
00752f9
Compare
…ide GraphQL Two findings from Claude's re-review on the rebased branch: 1. [Medium] project-orchestrator.yml: `request-review` input on the reeval Assign step evaluated false on the SKIP_BOARD path because `pr-status` is skipped there and its `promote` output is empty. Extended with `|| env.SKIP_BOARD == 'true'` to match the Assign step's existing condition. The composite action is idempotent on already-set review requests, so re-spam risk after CHANGES_REQUESTED is bounded (and acceptable for the SKIP_BOARD environment, which has no board-state to differentiate first-bot-clean from re-eval anyway). 2. [Low] board-state-sync.yml: PR-side `closingIssuesReferences` query used inline `--jq`, which silently turns GraphQL `.errors` into empty output — taking the "no linked counterpart" branch and producing a silent no-op on real failures. Refactored to match the Issue-side pattern: capture full response, explicitly check `.errors`, fail loudly via `::error::` + exit 1.
|
Pushed [Medium] [Low] PR-side Verified locally with The re-review will fire automatically on this push's — Posted by Claude Code on behalf of @EricAndrechek |
|
Claude finished @EricAndrechek's task in 2m 7s —— View job Review —
|
| Path | promote |
SKIP_BOARD |
request-review |
|---|---|---|---|
| Normal, first bot-clean | 'true' |
'' |
true ✅ |
| Normal, re-eval (already In review) | 'false' |
'' |
false ✅ |
| SKIP_BOARD (no board token) | '' (step skipped) |
'true' |
true ✅ |
The "Compute reviewer" step (line 292) correctly omits SKIP_BOARD != 'true' from its condition, so steps.reviewers.outputs.reviewers is always populated on a bot-clean event. The "Assign + request review" step's if: (line 412) also omits the SKIP_BOARD gate, so the composite action runs on that path. The composite action's own idempotency check prevents re-spam when the reviewer already has an outstanding request.
The comment block (lines 427–439) is a thorough explanation of the failure mode and the trade-off. Good.
Fix #2 — PR-side GraphQL error handling (board-state-sync.yml:155–176)
pr_resp=$(gh api graphql -f query='...' -F id="$CONTENT_NODE_ID")
graphql_errors=$(echo "$pr_resp" | jq -c '.errors // empty')
if [ -n "$graphql_errors" ]; then
echo "::error::GraphQL error querying PR counterparts for $CONTENT_NODE_ID: $graphql_errors"
exit 1
fi
counterparts=$(echo "$pr_resp" | jq -r '.data.node.closingIssuesReferences.nodes[].id')Matches the Issue-side pattern exactly. The inline --jq is gone; a real auth/API/schema error now surfaces as a hard failure rather than silently taking the "no linked counterpart" path. The schema-version special-case (closedByPullRequestsReferences not exposed) isn't replicated on the PR side because closingIssuesReferences is a stable field — correct call.
One minor observation (not a blocker): if .data.node is null (e.g., a corrupted event payload with an invalid node ID), jq's null.foo.bar[].id outputs nothing and the step takes the "No linked counterpart(s)" no-op path. Both sides have this same gap, and in practice CONTENT_NODE_ID comes straight from the validated event payload, so the risk is negligible.
Verdict
LGTM. Both findings are correctly and completely fixed. The commit is scoped to the two targeted files (31 lines added/changed, 4 removed). No new issues introduced. This PR is ready to merge.
| Branch
Two unrelated tool-version regressions that surfaced today: 1. `make security` — golangci-lint v2 (we're on v2.11.4) removed the v1 `--out-format` flag in favor of `--output.<formatter>.path` knobs. The v2 default is already colored line-number style with colors enabled, so just dropping the flag is the right fix; no format change visible to the user. 2. `make size-tree` and `make size-treemap` — gsa (`go-size-analyzer`) shipped v1.12.5 on 2026-04-27 which adopted Go's experimental `encoding/json/v2` package. On Go 1.25+ that requires building with `GOEXPERIMENT=jsonv2`. Prepended the env to each `go run gsa@latest` invocation. The experiment becomes a no-op once jsonv2 is promoted out of experimental status. Both targets are dev-only and not in `make ci`'s path, so this doesn't affect CI gates. Kept the `@latest` + Makefile pattern rather than moving to `tool` directives — gsa pulls DWARF/ELF analysis deps that aren't worth bloating go.sum for an ad-hoc binary-inspection tool, and golangci-lint's upstream explicitly recommends against `go install` / tool-directive use.
Closes #74.
Phase 1 of the Task Board state-machine rebuild discussed 2026-04-23. Moves from the rotating-assignee model to set-once-per-card assignees (Issue = implementer, PR = reviewer) with mirrored statuses (PR Ready ↔ Issue In review; both Done on merge), extracts the shared board operations into composite actions, adds the missing "linked issue → In review" on the
reevalpath, surfaces Dependabot major bumps onto the board, and adds bidirectional sync for human manual card moves.Full spec is now in
AGENTS.md §Task Board state machineas source of truth.Changed
.github/actions/board-upsert-status/action.yml.github/actions/assign-and-request-review/action.yml.github/actions/set-linked-issues-status/action.yml.github/workflows/project-orchestrator.ymlreeval, extends bot-clean to new required checks..github/workflows/dependabot-automerge.yml.github/workflows/board-state-sync.ymlprojects_v2_item: edited, with loop-prevention guard.AGENTS.md§Task Board state machinerewritten as authoritative spec.CHANGELOG.mdTestability note
pull_request_targetruns the workflow from the PR's base branch. So this PR cannot smoke-test the new orchestrator against itself — the old one fires. The smoke test is the next PR after merge, where we should see:Fix-forward on regressions, not revert — the composite actions isolate most risk.
Risk
The workflow files in this PR are inert until merged to main (pull_request_target uses base-branch workflow, and our composite actions don't exist in main yet). So there's zero half-old/half-new state during this PR's lifecycle.
— Posted by Claude Code on behalf of @EricAndrechek