fix(gh-aw): bind squad:{agent} labels to each issue's own plan row - #1863
Conversation
Activation minted a certified-but-wrong label and then reported labels it
never applied. Both survived every existing guard because the guards check
membership, and these were failures of correspondence.
Team Guard TG-2 certifies the vocabulary of names that may become a
`squad:{agent}` label; validation Check 10 asserts every plan `Agent` is a
member of it. Neither asks whether the label on task N is the agent the plan
assigned to task N. A label can be simultaneously certified and wrong.
Measured on octodemo/aspiregregator-squad-e2e (run 32778953402, TG-2 green,
Check 10 green, 12/12 values certified):
#1859 Task 6 is assigned to McManus; issue #17 was created with squad:kint,
the owner of its parent epic. 11 of 12 bindings were correct, and the
twelfth was invisible because `kint` is a valid roster name.
#1860 The summary reported `squad:kint` on epic #6 and
`squad:kint / squad:mcmanus` on epic #7. Both carry only `squad`.
The multi-owner epic was also never recorded under `Non-roster agent
values`, so a run that dropped two bindings read as clean.
The cause was ambiguity, not disobedience. Both create-issue steps said
`Labels: squad, squad:{agent}` without ever binding `{agent}` to a source --
unlike the `plan implement` path at L928, which qualifies it. Reading tasks
grouped under an epic, inheriting the epic's agent is the natural resolution.
- Task rule now names the task's own `Agent` cell, keyed by the plan row
whose `#` matches, and prohibits epic inheritance and carry-forward by name.
- Epic rule now derives from the epic's own tasks: one distinct agent mints
that label, two or more resolve to bare `squad` plus a `Non-roster agent
values` entry. An epic has no Agent column, so any other reading is a guess.
- Label Pre-flight gains a correspondence step stating that membership across
the run is not evidence, and a reporting step requiring the summary to name
a label only after that issue's create-issue call returned carrying it.
Tests assert prompt text, which is weaker than the executed-shell assertions in
gh-aw-activate-roster-binding.test.ts; deterministic post-activation enforcement
remains #1801. They are mutation-checked: restoring either rule to its previous
wording fails two of them.
Closes #1859
Closes #1860
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 20afe6d2-444e-414e-8a39-e67ab67ca6df
🟡 Impact Analysis — PR #1863Risk tier: 🟡 MEDIUM 📊 Summary
🎯 Risk Factors
📦 Modules Affectedroot (1 file)
tests (1 file)
This report is generated automatically for every PR. See #733 for details. |
🛫 PR Readiness Check
PR Scope: 🔧 Infrastructure
|
| Status | Check | Details |
|---|---|---|
| ✅ | Single commit | 1 commit — clean history |
| ✅ | Not in draft | Ready for review |
| ✅ | Branch up to date | Up to date with dev |
| ❌ | Copilot review | No Copilot review yet — it may still be processing |
| ✅ | Changeset present | No source files changed — changeset not required |
| ✅ | Scope clean | No .squad/ or docs/proposals/ files |
| ✅ | No merge conflicts | No merge conflicts |
| ✅ | Copilot threads resolved | No Copilot review threads |
| ❌ | CI passing | 1 check(s) still running |
Files Changed (2 files, +199 −2)
| File | +/− |
|---|---|
test/gh-aw-agent-binding-correspondence.test.ts |
+184 −0 |
workflows/squad.md |
+15 −2 |
Total: +199 −2
This check runs automatically on every push. Fix any ❌ items and push again.
See CONTRIBUTING.md and PR Requirements for details.
There was a problem hiding this comment.
🟡 Changes recommended
Normalize and trim epic agent values before distinct-counting and label generation.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Pull request overview
Updates gh-aw activation rules so issue labels correspond to their assigned plan agents and reporting reflects labels actually applied.
Changes:
- Adds per-task and per-epic label binding rules.
- Handles multi-owner epics and non-roster values.
- Adds seven prompt-contract tests.
File summaries
| File | Description |
|---|---|
workflows/squad.md |
Updates activation label binding and reporting rules. |
test/gh-aw-agent-binding-correspondence.test.ts |
Tests the new correspondence requirements. |
Review details
Suppressed comments (6)
workflows/squad.md:1338
- This reporting condition cannot be satisfied for idempotent reruns: Step 2b says to deduplicate by an existing issue title, so no
create-issuecall returns for that issue. The summary would then omit labels that the existing issue actually carries. Permit either the create response or an existing-issue lookup to return the issue's actual labels before reporting them.
`squad:{agent}` label for an issue only after that issue's `create-issue` call returned
successfully carrying it. Never state a label that was skipped, omitted, deferred, or
workflows/squad.md:1376
- The accepted
Agentvalue@copilotis allowed by the pre-flight rule, but interpolating and lowercasing it here yieldssquad:@copilot. The repository's routing and label-sync contracts usesquad:copilot(.squad/templates/routing.md:24,.github/workflows/sync-squad-labels.yml:175-181), so a Copilot-owned task would not receive the label that downstream automation recognizes. Add an explicit@copilot→squad:copilotmapping.
- Labels: `squad` (0075ca), `squad:{agent}` (e4e669) where `{agent}` is **this task's own `Agent` cell**, lowercased — read from the implementation-plan row whose `#` matches this task. Never inherit the parent epic's agent, and never carry the previous task's value forward: re-read the `Agent` cell for every task, because consecutive tasks under one epic routinely have different agents. No `size:*` labels unless policy says so.
workflows/squad.md:1362
@copilotis an explicitly permitted Agent value, but this singleton branch interpolates it assquad:@copilot. The repository's routing and heartbeat workflows consumesquad:copilot(the triage mapper special-cases@copilot), and nosquad:@copilotlabel is provisioned, so a copilot-owned epic will be created without a routable agent label. Handle the@copilot→squad:copilotmapping here and in the shared gate before interpolating the label.
- Labels: `squad` (0075ca), `squad:{agent}` (e4e669) where `{agent}` is **derived from this epic's own tasks**: collect the `Agent` values of every implementation-plan row whose `Epic` cell names this epic. Exactly one distinct value → mint `squad:{that agent}`. Two or more → multi-owner epic: apply only `squad` and record it under `Non-roster agent values`. Never mint a single agent label for a multi-owner epic, and never choose one of several.
workflows/squad.md:1341
- An unavailable label is not a non-roster value: the Agent can be a certified
ROSTER_MEMBER, with only the label resource missing. Requiring theNon-roster agent valuesheading for that case falsely classifies a valid roster member and conflicts with the next paragraph's prerequisite-gap reporting. Keep unavailable-label diagnostics in the prerequisite-gap section and reserve this heading for derived multi-owner or uncertified values.
assumed. Whenever an `Agent` value did not become a label — multi-owner epic, uncertified
name, unavailable label — the `Non-roster agent values` heading is **required**, and must
name the value and the issue it applied to. Omitting the heading while omitting the label
workflows/squad.md:1362
- The new suite exercises epic-source selection and the multi-owner refusal, but it never pins the singleton branch (
Exactly one distinct value → mint squad:{that agent}). A regression that omits or downgrades labels for every single-owner epic would leave all seven tests green, while the existing completeness rule only requires one agent label somewhere in the run. Add a positive contract assertion for the exactly-one case.
- Labels: `squad` (0075ca), `squad:{agent}` (e4e669) where `{agent}` is **derived from this epic's own tasks**: collect the `Agent` values of every implementation-plan row whose `Epic` cell names this epic. Exactly one distinct value → mint `squad:{that agent}`. Two or more → multi-owner epic: apply only `squad` and record it under `Non-roster agent values`. Never mint a single agent label for a multi-owner epic, and never choose one of several.
workflows/squad.md:1376
- This lookup now requires a
#key, but the producer's own Step 4 structure atsquad.md:1123describes phase tables asTitle|Size|Depends On|Agent|Epicand omits#. Although the imported ontology supplies that column, the local producer contract is contradictory; a model following the listed phase-table columns can emit no key, making the required per-task lookup impossible and reopening the drift this change targets. Make the producer contract explicitly include#or define a stable fallback before relying on it here.
- Labels: `squad` (0075ca), `squad:{agent}` (e4e669) where `{agent}` is **this task's own `Agent` cell**, lowercased — read from the implementation-plan row whose `#` matches this task. Never inherit the parent epic's agent, and never carry the previous task's value forward: re-read the `Agent` cell for every task, because consecutive tasks under one epic routinely have different agents. No `size:*` labels unless policy says so.
- Files reviewed: 2/2 changed files
- Comments generated: 1
- Review effort level: Lite
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| **2b. Create Epic Issues:** `create-issue` per epic (dedup by title `[Epic] {name}` if already exists from prior phase). | ||
| - Title: `[Epic] {name}` | ||
| - Labels: `squad` (0075ca), `squad:{agent}` (e4e669) | ||
| - Labels: `squad` (0075ca), `squad:{agent}` (e4e669) where `{agent}` is **derived from this epic's own tasks**: collect the `Agent` values of every implementation-plan row whose `Epic` cell names this epic. Exactly one distinct value → mint `squad:{that agent}`. Two or more → multi-owner epic: apply only `squad` and record it under `Non-roster agent values`. Never mint a single agent label for a multi-owner epic, and never choose one of several. |
Closes #1859
Closes #1860
The gap these two defects exposed
squad plan activateminted a certified but wrong label, then reported labels it never applied. Both slipped past every guard the workflow already had, because those guards verify membership and these were failures of correspondence.squad:{agent}label.Agentvalue belongs to that set.A label can be simultaneously certified and wrong.
Measured evidence
From the first full E4 run on
octodemo/aspiregregator-squad-e2e(run32778953402— TG-2 green, Check 10 green, 12/12 values certified):squad:kint— the owner of its parent epic. 11 of 12 bindings were correct; the twelfth was invisible becausekintis a perfectly valid roster name.— squad:kinton epic #6 andsquad:kint / squad:mcmanuson epic #7. Both issues carry onlysquad. The multi-owner epic was also never recorded underNon-roster agent values, so a run that silently dropped two bindings read as clean.Root cause: ambiguity, not disobedience
Both
create-issuesteps readLabels: squad, squad:{agent}and never bound{agent}to a source — unlike theplan implementpath at L928, which does qualify it ("{owner}is theOwnerlowercased"). Reading tasks grouped beneath an epic, inheriting the epic's agent is the natural resolution of an unbound placeholder.Changes to
workflows/squad.md(17 lines)Agentcell, keyed by the plan row whose#matches, and prohibits epic inheritance and carry-forward by name — a positive instruction alone was already present and did not prevent gh-aw:plan activatebinds a task'ssquad:{agent}label from its epic owner, not the task'sAgentcell #1859.squadplus aNon-roster agent valuesentry. An epic has noAgentcolumn of its own, so any other reading is a guess.create-issuecall returned carrying it; theNon-roster agent valuesheading is required whenever a value did not become a label).Tests
test/gh-aw-agent-binding-correspondence.test.ts— 7 tests, all passing.These assert prompt text, which is a weaker instrument than the executed-shell assertions in
gh-aw-activate-roster-binding.test.ts. Deterministic post-activation enforcement is #1801 and remains open. What these buy is that the specific ambiguity behind #1859 cannot silently return.They are mutation-checked: restoring the task rule to its previous wording fails 2 of the 7. A prose assertion that cannot fail is decoration.
Validation
npx vitest run test/gh-aw-agent-binding-correspondence.test.ts— 7/7gh-aw-quality,template-sync,gh-aw-activate-roster-binding,gh-aw-plan-lifecycle— 404 passing, no regressionsnpm run lintclean; eslint cleanpackages/*/src/Not addressed here
bindingsarray in theactivatedartifact.workflows/shared/squad-planning-ontology.md§3.6's pre-filled✅checks table.