Skip to content

fix(workflows): target activation labels via gh-aw temporary IDs - #1965

Merged
bradygaster merged 3 commits into
devfrom
bradygaster-plan-activate-temporary-ids
Aug 31, 2026
Merged

fix(workflows): target activation labels via gh-aw temporary IDs#1965
bradygaster merged 3 commits into
devfrom
bradygaster-plan-activate-temporary-ids

Conversation

@bradygaster

@bradygaster bradygaster commented Aug 31, 2026

Copy link
Copy Markdown
Owner

Closes #1962
Parent #1957

Working as Procedures (Prompt Engineer) — issue carries squad:procedures.

Problem

The /squad plan activate prose instructed the model to read a real GitHub issue number back from each create-issue call, then target add_labels at that number:

After EVERY create-issue call: verify returned issue number, stop on failure, NEVER predict issue numbers.

gh-aw never returns an issue number to the agent. Issue creation is deferred to the post-agent safe-output job, so the agent only ever receives a success acknowledgement. The instruction was therefore unsatisfiable, and it left two failure modes:

  1. The model either stalls waiting for a number that never arrives, or invents one.
  2. An add_labels call whose item_number cannot be resolved does not error — it silently applies the labels to the triggering intent issue, branding the user's own request with an activated item's owner label.

Fix

Switched the full activation path to gh-aw's supported temporary_id linkage.

Area Before After
Frontmatter create-issue accepted calls without a temp ID require-temporary-id: true — a call without one is rejected
Hallucination Guard "verify returned issue number" States plainly that no number is returned; forbids predicting, inferring, or waiting
add_labels targeted a "verified" real number item_number = the minting create-issue call's temporary_id; a dedup-matched epic uses its verified real number
Task parent implied the epic's real number epic's #aw_epic{K} when minted this run; verified real number when 2b reused a deduped prior-phase epic
Dependency edges (Step 3) blockedBy via write API with a real number blocked_by declared on the create-issue call, using temporary IDs
Sub-issue fallback body ref literal Parent: #{issue_number} Parent: #aw_epic{K} when minted this run; verified real number otherwise
2d report_incomplete "last verified issue number" last task's temporary ID

New Temporary-ID Contract section covers:

  • Form — the real gh-aw pattern ^#?aw_[A-Za-z0-9_]{3,12}$.
  • Minting — derive, never invent#aw_epic{K} (1-based epic position), #aw_task{N} (task's own # cell, non-alphanumerics → _, so 2.3#aw_task2_3). Overflow drops the epic/task word rather than truncating the number.
  • Uniqueness is the prompt's responsibility — verified: gh-aw does not reject a duplicate temporary_id; temporaryIdMap.set() silently overwrites, so the last writer owns the mapping and earlier references resolve to the wrong issue. No warning is emitted.
  • Explicit targeting is mandatory — names the silent-fallback hazard above.
  • Existing and reused issues — targeted only by verified real numbers, since temporary IDs map only issues this run created.

Reused/deduped epics (review finding, fixed)

Step 2b dedups epics by title against prior phases. A reused epic was never minted in this run, so it has no entry in gh-aw's temporary-ID map — passing #aw_epic{K} for it would be an unresolvable reference. Both places that reference an epic now branch on provenance:

  • Step 2c parent — temporary ID only when 2b minted the epic this run; verified real number for a dedup-by-title/prior-phase match. The prose also explicitly forbids passing a temporary ID that was not minted this run.
  • Sub-issue Fallback body reference — same carve-out, and it matters more here: gh-aw leaves an unresolved #aw_… body reference verbatim rather than stripping it, so a temporary ID it never minted would ship to the user as a literal #aw_epic3.

Preserved unchanged: roster correspondence (per-task Agent cell, never inherited from the epic or carried forward), base squad, @copilotsquad:copilot, multi-owner epic behavior, non-roster recording, and origin-issue safety.

Contract verification (pinned gh-aw v0.87.10)

Checked against the binary's embedded schema and upstream source, not assumed:

  • create_issue.temporary_id — optional string, ^#?aw_[A-Za-z0-9_]{3,12}$
  • add_labels.item_numbernumber | string, ^(\d+|#?aw_[A-Za-z0-9_]{3,12})$
  • create_issue.parent / create_issue.blocked_by — accept temporary IDs
  • require-temporary-id: true compiles to "require_temporary_id":true, required_field_additions.create_issue: ["temporary_id"], and the agent-facing constraint string temporary_id is required.
  • Ordering is safe: the topological sort inspects create_issue.blocked_by; an add_labels hitting an unresolved temp ID returns deferred and gets one retry pass — so create-issue then add_labels is the supported order.
  • replaceTemporaryIdReferences() leaves an unresolved #aw_… body reference intact — the basis for the fallback carve-out above.

⚠️ Two things reviewers should confirm

1. require-temporary-id is workflow-global, not per-skill.

It is enforced in safe_outputs_handlers.cjs at the MCP tool layer from the single config.create_issue block. workflows/squad.md has a second create_issue caller — the squad-plan-accept fast path (#1959's scope) — whose calls would be rejected outright once the flag is on. I enabled the flag (an explicit #1962 success criterion) plus the smallest possible compliance edit to the fast path: mint a temporary ID and use it for parent. I deliberately did not add add_labels or create-if-missing there — that remains #1959, and an existing test asserts their absence in that region (still passing).

2. The Activation bindings: JSON block still references real issue numbers — and cannot be fixed here.

replaceTemporaryIdReferences() replaces an #aw_task1 token with `#${number}` — the # is retained for same-repo refs. Embedding #aw_ refs in that JSON code block would emit "issue": #42, which is invalid JSON. There is no substitution form that yields a bare number. Per the instruction to report rather than invent a workaround, I left the block unchanged and am flagging it as a blocker/decision for #1963 rather than papering over it. This means #1962's "no operation relies on an inferred issue number" is satisfied for every targeting operation but not yet for that reporting block.

Viable avenue for #1963: gh-aw exposes the resolved map as steps.process_safe_outputs.outputs.temporary_id_map (JSON { "aw_id": {"repo":..., "number":N} }) and as artifact /tmp/gh-aw/temporary-id-map.json.

Validation

  • Strict compile: all 4 workflows (squad, squad-implement-worker, squad-review, squad-deps-worker) compile with --strict, all 4 lock files emitted, FAILED=0. Remaining warnings are pre-existing on dev (concurrency discriminator, slash_command+bots).
  • Targeted tests: 296 passed across gh-aw-activation-temporary-ids, gh-aw-activation-label-provisioning, gh-aw-agent-binding-correspondence, gh-aw-activate-roster-binding, gh-aw-plan-lifecycle, gh-aw-quality.
  • Build: npm run build passes.
  • Full suite: 8127 passed. The 12 failures in cli-packaging-smoke / acceptance / external-state CLI tests are pre-existing — reproduced identically on the baseline with my changes stashed.

Test changes

Updated test/gh-aw-activation-label-provisioning.test.ts — one test asserted the prose must contain "never call add_labels before [create-issue] has returned a real issue number". That sentence encoded the exact invalid premise this issue corrects, so the test now asserts the inverse contract (the old rule is gone; the prose tells the model not to wait; create-issue returns no number).

Added test/gh-aw-activation-temporary-ids.test.ts (18 tests) — narrowly scoped to #1962: temporary-ID declaration on every activation create-issue, the documented gh-aw pattern, deterministic minting, the uniqueness mandate, mandatory explicit item_number, add_labelstemporary_id wiring, no-prediction rules, real numbers for reused issues, 2b/2c temp-ID fields, epic-as-parent, reused-epic parent targeting, sub-issue fallback provenance branching, blocked_by on create-issue, plus compiled-lock assertions that require_temporary_id actually reaches the handler config. Fails closed on a missing gh aw per the repo's #1834 convention.

Prose was tightened to stay under the existing 160 KB source-growth guard rather than raising it.

Out of scope (untouched)

/squad activate fast-path provisioning (#1959, beyond the minimal compliance edit above), activation reporting redesign (#1963), capacity safeguards (#1961), broad behavioral suite (#1960), E4 (#1958), checker distribution.

No changeset: no packages/*/src/ or templates/ changes, so the changelog-gate path regex does not match.

Note: #1964 conflicts at Step 2d. Not merged or rebased into this branch — the parent coordinator is sequencing integration.

`/squad plan activate` told the model to read a real issue number back from
each `create-issue` call and then target `add_labels` at it. gh-aw never
returns one — issue creation is deferred to the post-agent safe-output job,
so the agent only ever sees a success acknowledgement. Every operation that
depended on a "returned" number was therefore unsatisfiable, and an
`add_labels` call with no resolvable `item_number` does not error: it
silently labels the triggering intent issue.

Switch the full activation path to gh-aw's supported `temporary_id` linkage:

- Enable `require-temporary-id: true` on the `create-issue` safe output, so
  a call that omits a temporary ID is rejected rather than silently
  mis-targeted.
- Add a Temporary-ID Contract section covering the real gh-aw pattern
  (`^#?aw_[A-Za-z0-9_]{3,12}$`), a deterministic derive-don't-invent minting
  scheme (`#aw_epic{K}` / `#aw_task{N}`), and an explicit uniqueness rule —
  gh-aw does not reject duplicates, it silently lets the last writer own the
  mapping.
- Rewrite the Hallucination Guard to state that no issue number is returned,
  and forbid predicting, inferring, or waiting for one.
- Target `add_labels.item_number`, `create_issue.parent`, and
  `create_issue.blocked_by` at temporary IDs; keep verified real numbers for
  pre-existing/reused issues and the triggering issue.

Roster correspondence, base `squad`, @copilot mapping, multi-owner and
non-roster behavior, and origin-issue safety are unchanged.

`require-temporary-id` is workflow-global (enforced at the MCP tool layer
from the single create_issue config), so the `squad-plan-accept` fast path
needed a minimal compliance edit to mint a temporary ID. Its label
provisioning remains out of scope — that is #1959.

Verified against the pinned gh-aw v0.87.10.

Closes #1962
Parent #1957

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot AI lite review requested due to automatic review settings August 31, 2026 22:03
@github-actions

github-actions Bot commented Aug 31, 2026

Copy link
Copy Markdown
Contributor

🛫 PR Readiness Check

ℹ️ This comment updates on each push. Last checked: commit c95d43a

PR Scope: 🔧 Infrastructure

⚠️ 2 item(s) to address before review

Status Check Details
Single commit 3 commits — consider squashing before review
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 1 active Copilot thread(s) resolved (1 outdated skipped)
CI passing All checks passing

Files Changed (3 files, +436 −22)

File +/−
test/gh-aw-activation-label-provisioning.test.ts +20 −3
test/gh-aw-activation-temporary-ids.test.ts +357 −0
workflows/squad.md +59 −19

Total: +436 −22


This check runs automatically on every push. Fix any ❌ items and push again.
See CONTRIBUTING.md and PR Requirements for details.

@github-actions

Copy link
Copy Markdown
Contributor

🟡 Impact Analysis — PR #1965

Risk tier: 🟡 MEDIUM

📊 Summary

Metric Count
Files changed 3
Files added 1
Files modified 2
Files deleted 0
Modules touched 2

🎯 Risk Factors

  • 3 files changed (≤5 → LOW)
  • 2 modules touched (2-4 → MEDIUM)

📦 Modules Affected

root (1 file)
  • workflows/squad.md
tests (2 files)
  • test/gh-aw-activation-label-provisioning.test.ts
  • test/gh-aw-activation-temporary-ids.test.ts

This report is generated automatically for every PR. See #733 for details.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🟡 Changes recommended

The updated activation flow now applies labels via add_labels, but nearby workflow prose still contains inconsistent label-reporting and self-validation wording that can mislead the agent.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Review tier: Lite
Findings: 2 Low severity

New issues introduced by this change (2)
Severity Finding
Low severity workflows/​squad.md — Step 2d uses “requested/recognized” as the count being compared, but then tells the agent to call…
Low severity workflows/​squad.md — This section now correctly instructs label application via add_labels targeting temporary_id,…
What changed in this PR

Updates the squad-plan-activate workflow contract to use gh-aw temporary_id linkage (instead of unsatisfiable “returned issue numbers”) so add_labels can deterministically target the intended created issues and avoid silent fallback labeling of the triggering intent issue.

Changes:

  • Enforces require-temporary-id: true for create-issue safe outputs and documents a Temporary-ID Contract + hallucination guard updates.
  • Rewrites activation label application instructions to target add_labels.item_number at the minting create-issue.temporary_id.
  • Adds a focused contract test suite asserting both the prose contract and the compiled lock’s require_temporary_id wiring; updates the prior label-provisioning prose test accordingly.
File Description
workflows/​squad.md Requires temporary IDs for create-issue, updates activation contract to use temp IDs for parent/blocked_by/add_labels targeting, and refreshes hallucination guidance.
test/​gh-aw-activation-temporary-ids.test.ts New tests asserting the temporary-ID contract in prose plus compiled-lock verification of require_temporary_id.
test/​gh-aw-activation-label-provisioning.test.ts Updates an assertion to forbid the old “wait for returned issue number” contract and require the new “do not wait” language.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread workflows/squad.md Outdated
Comment thread workflows/squad.md
…t refs

Review finding: Step 2b dedups epics by title against prior phases, so a
reused epic was never minted in this run and has no entry in gh-aw's
temporary-ID map. Passing `#aw_epic{K}` for it was an unresolvable parent
reference.

- Step 2c parent targeting now branches on provenance: `#aw_epic{K}` only
  when 2b minted the epic this run; the verified real number when 2b matched
  a pre-existing epic by title.
- Sub-issue Fallback body reference gets the same carve-out. This matters
  more there: gh-aw leaves an unresolved `#aw_…` body reference VERBATIM
  rather than stripping it, so a temporary ID it never minted would ship to
  the user as a literal "#aw_epic3".
- Adds two focused tests covering reused-epic parent targeting and the
  fallback provenance branch.

Prose trimmed elsewhere to stay under the existing 160 KB source-growth
guard rather than raising it.

Refs #1962
Parent #1957

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
bradygaster added a commit that referenced this pull request Aug 31, 2026
Independent review found three factual defects in the prose and tests
shipped by the first commit. All three are corrected against the pinned
gh-aw v0.87.10 sources rather than gh-aw's own injected descriptions.

1. `report_incomplete` does NOT make the run conclude non-successfully.
   `report_incomplete_handler.cjs` emits `core.warning` only, and
   `handle_agent_failure.cjs` contains zero `setFailed`/`process.exit`
   calls in 4453 lines. What it actually does is open or update a durable
   `[aw] {workflow} reported incomplete result` tracking issue. Step 2e
   now states that, says the run still reports success, and tells the
   agent never to rely on a red run to carry the signal. gh-aw's own tool
   description ("treated as a failure signal even when the agent exits
   successfully") is misleading; trusting it was exactly the injected-prose
   mistake #1961 exists to prevent.

   Limitation, stated honestly: no narrow mechanism in the pinned runtime
   makes an incomplete activation conclude red. Forcing one would require
   a custom safe-job, which is outside this issue's scope. The durable
   tracking record plus the "never report a clean activation you did not
   perform" rule are what carry the signal.

2. Cap enforcement is dual, not collection-only. Per Safe Outputs MCE4,
   `enforcePerTypeMax` in `safe_outputs_handlers.cjs` throws JSON-RPC
   `E002: {type} limit reached` at invocation time, which the agent DOES
   see; the collector then drops surplus items with a warning. Removed the
   claim that an over-limit item "never appears as an error to the agent".
   Reconciliation still keys on absence-of-success, not presence-of-error,
   because a call can also simply never be made.

3. The old `max: 80` did not cause runtime truncation — 50 calls never
   reached it. The real hazard is gh-aw's injected "Maximum 80 label(s)
   can be added" wording against a 100-label worst case, which invites
   agent self-truncation. 110 is justified by that hazard plus bounded
   margin, and the docblock now explicitly records what is NOT claimed.

Also, per the PR #1965 review: Step 2e no longer demands a real issue
number for items created during the run. Issue creation is deferred to
the post-agent job, so it names the stable temporary ID plus title and
intended labels, and quotes a real number only for a reused issue —
avoiding reintroduction of #1962's invalid-number assumption.

Tests: renamed the two overclaiming tests and gave each a negative guard
that fails if the retracted wording returns; added coverage for the
temporary-ID rule. Trimmed prose to stay under the existing 160 KB
source-growth guard rather than raising another test's budget.

Verified: 221/221 across the three gh-aw suites; mutation-checked (cap
-> 80 fails 5 tests incl. the compiled-runtime one, reintroducing either
retracted claim fails its guard); strict compile clean on all four
workflows; npm run build passes.

Closes #1961
Parent #1957

Working as FIDO (Quality Owner)

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 2471face-3b27-4419-bd48-0becd2642d7f
bradygaster added a commit that referenced this pull request Aug 31, 2026
Resolves the single workflows/squad.md conflict deliberately, keeping both
sides rather than accepting either wholesale:

- `- Temporary ID:` — takes #1965's byte-tightened wording ("be unique in
  this run", "gh-aw silently lets a duplicate's last writer own the
  mapping"). That commit trimmed prose specifically to stay under the
  source-growth guard; discarding it would undo that.
- `- Labels:` — keeps #1959's corrected rule: the inline `@copilot` →
  `squad:copilot` mapping in the primary computation, and the non-roster
  omit-and-record contract. #1965's side of this line predates both fixes.

The scoped uniqueness assertion in the fast-path suite now accepts either
phrasing — `/(must not repeat within|be unique in) this run/` — and gains a
second assertion requiring the silent-duplicate hazard that motivates it.
The invariant is enforced more tightly than before, not relaxed: verified by
deleting the clause from squad.md and confirming the test fails.

Source-growth guard stays at 170 KB. #1965's trims bought room back but not
enough — combined source measures 164.8 KB after the merge, still over 160.
Comment updated with the post-merge number.

Tests: 487 gh-aw tests pass (14 files). All four workflows strict-compile
(FAILED=0). `npm run build` passes.

Refs #1959
Refs #1957

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
bradygaster added a commit that referenced this pull request Aug 31, 2026
Stacks this branch explicitly on `bradygaster-plan-activate-temporary-ids`
(PR #1965 head 528ba9b) so #1964 reviews as a clean delta on top of the
temporary-ID contract instead of colliding with it at merge time.

Conflict resolution (workflows/squad.md, Step 2d — the only conflict):
- Took #1965's Self-Validation sentence wholesale. It is strictly stronger
  than the version on this branch ("requested/recognized", "the last task's
  temporary ID", "never substitute a guessed issue number").
- Reapplied only this branch's minimal trailing clause, which is #1961's
  concern and not addressed by #1965: never surface a safe-output cap as a
  guessed reason for a partial run — name a cap only when Step 2e observed
  one actually being reached.

Step 2e concretized against the now-present mechanism:
- The report identifier for an item created this run is the `temporary_id`
  minted under #1965's Temporary-ID Contract, not a GitHub issue number,
  because creation is deferred to the safe-output job.
- A real number may be quoted only where independently verified (dedup-by-title
  match, or Step 1's idempotent-rerun path). This mirrors #1965's own "Existing
  and reused issues" rule rather than restating a parallel contract.

Source-growth guard raised 160 -> 168 KB (test/gh-aw-quality.test.ts), per that
guard's own documented criterion rather than to turn a red test green:
- Ambient prompt (the canonical budget) measures 32.0 KB against 40 KB.
- All growth is inside the `squad-plan-activate` inline skill, which the
  extractor strips from the ambient prompt and loads on demand.
- Neither PR was individually over: #1965 alone 163812 B, #1961 alone 163819 B,
  against the old 163840 B ceiling — 28 and 21 bytes of headroom. Two
  independently-compliant PRs could not coexist under a threshold that tight.

Validation: 239/239 across the four gh-aw suites; three mutation probes
(cap 110->80, Step 2e temp-ID rule, retracted dual-enforcement claim) each fail
their intended tests; all four workflows compile with `--strict` showing only
pre-existing warnings; `npm run build` passes. Compiled lock confirms both
contracts coexist: add_labels max 110, create_issue max 75 + require_temporary_id.

Refs #1961, #1965. Parent #1957.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 2471face-3b27-4419-bd48-0becd2642d7f
Review finding: 2d compared a "requested/recognized" count but then told the
agent to report it as `created={N}`, leaving {N} undefined.

- Defines the quantity once as the **created count** — the number of
  `create-issue` calls this run emitted — and uses that one term throughout.
- Binds both parameters explicitly: `created={N}` set to that created count,
  `expected={M}` set to the declared total.
- Adds a focused test that locks the single term and both parameter bindings,
  and rejects a regression to mixed terminology. The assertion bounds its
  slice at the next heading so it cannot reach into Step 4, whose separate
  binding wording is #1963's scope.

Step 4's `Activation bindings:` block is deliberately untouched; its
reporting inconsistency is tracked in #1963.

Prose trimmed elsewhere to stay under the existing 160 KB source-growth guard.

Refs #1962
Parent #1957

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
bradygaster added a commit that referenced this pull request Aug 31, 2026
Keeps the stack on `bradygaster-plan-activate-temporary-ids` current after
#1965 advanced past 528ba9b. GitHub had flagged the PR CONFLICTING/DIRTY.

One conflict, again at Step 2d, resolved by the same rule used for 528ba9b:
take #1965's sentence wholesale and reapply only this branch's trailing clause.

#1965 c95d43a tightens 2d by defining the created count explicitly ("the number
of `create-issue` calls this run emitted") and binding `created={N}`/`expected={M}`
to it. That definition is adopted verbatim. This branch's delta — never surface a
safe-output cap as a *guessed* reason for a partial run; name one only when Step 2e
observed it being reached — is reapplied to the trailing sentence. Step 2e is
unchanged and still follows 2d.

#1965's other two hunks in this commit (Uniqueness wording, pre-existing-epic
parent rule) auto-merged and are present verbatim.

Validation: 240/240 across the four gh-aw suites (count rises from 239 because
c95d43a adds a test to #1965's own suite, which passes untouched); all four
workflows compile with `--strict` showing only pre-existing warnings;
`npm run build` passes.

Refs #1961, #1965. Parent #1957.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 2471face-3b27-4419-bd48-0becd2642d7f
@bradygaster
bradygaster merged commit fa5cc26 into dev Aug 31, 2026
18 checks passed
bradygaster added a commit that referenced this pull request Sep 1, 2026
Resolves #1964 against dev head 9e3f6b3, which now carries both
#1965 (temporary-ID contract, fa5cc26) and #1966 (fast-path label
provisioning, squash 9e3f6b3).

workflows/squad.md — auto-merged, no conflict. #1966's edits land in
the `/squad activate` fast path and Step 4; #1961's are in
squad-plan-activate Step 2d/2e. Different regions, so the recurring
Step 2d conflict did not recur this round. Verified by anchor grep
that all three contracts survive unduplicated: #1966's Fast-Path Label
Provisioning / non-roster reporting, #1965's Temporary-ID Contract and
created-count wording, and #1961's capacity budget / max 110 / Step 2e.

test/gh-aw-quality.test.ts — SOURCE_GROWTH_BUDGET_KB conflict resolved
to 172 (was 168 here, 170 on dev). Measured, not guessed: combined
source on the resolved branch is 173241 B (169.2 KB).

  - 168 now FAILS by 1209 B, so keeping it would land red.
  - 170 passes by only 839 B. That is the same stale-threshold failure
    this guard already hit once at 160, where 21-28 bytes of headroom
    meant two independently-compliant PRs could not coexist.
  - 172 leaves 2887 B (~2.8 KB) of real margin, so the guard still
    bites on genuine growth.

dev's #1959/#1962 rationale comment is preserved verbatim; the #1961
rationale is appended beneath it rather than replacing it.

Guard precondition confirmed: the canonical ambient prompt budget is
still 32.0 KB (32795 B) against 40 KB, unchanged by either PR, because
all growth sits inside inline `## skill:` blocks that gh-aw strips from
the always-loaded prompt. That is this guard's own documented condition
for a legitimate raise.

Validation: 320/320 across 9 targeted suites including #1966's new
fast-path suite; all four workflows `gh aw compile --strict` clean
(pre-existing discriminator/slash_command warnings only); npm run build
exit 0.

Closes #1961
Parent #1957

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 2471face-3b27-4419-bd48-0becd2642d7f
bradygaster added a commit that referenced this pull request Sep 1, 2026
…cit (#1961) (#1964)

* fix(gh-aw): prevent silent truncation of activation label operations

Working as FIDO (Quality Owner).

A 50-issue activation could lose `add_labels` operations while the run still
reported success. Two causes:

1. `add-labels: max: 80` was below the worst case under the reading gh-aw's own
   injected prose invites. The compiler emits "Maximum {max} label(s) can be
   added", which reads as a budget of label NAMES; a full 50-issue activation
   applying `squad` + `squad:{agent}` needs 100. An agent taking that phrasing
   literally could conclude it had overrun and stop labeling early or batch
   issues together.
2. gh-aw v0.87.10 drops an over-limit item rather than failing. Its collector
   (`collect_ndjson_output.cjs`) rejects the item and `continue`s, pushing a
   string into `errors`; those are emitted with `core.warning`, never
   `core.setFailed`. The run finishes green with label operations missing and
   nothing announces it.

Verified against the pinned runtime rather than inferred: `max` caps safe-output
ITEMS (tool calls) per type, not label names inside a call. A two-label call
costs one item.

Capacity: largest supported activation is 50 issues (`enterprise` profile
`max_issues: 50`, the highest documented profile limit, and the same threshold
`squad-plan-activate` uses to force phased activation). Worst case at that size
is 50 `create-issue` items, 50 `add_labels` calls, <=2 labels per call, 100
label names across the run.

Changes:
- `add-labels: max` 80 -> 110, sized to cover the worst case under BOTH readings
  (50 calls, 100 names) so no interpretation of the cap can justify dropping a
  label operation. `create-issue` stays at 75.
- Record the derivation and the item-vs-label-name semantics in the activation
  skill, plus the fact that an over-limit item is dropped, not failed.
- New Step 2e Label-Operation Reconciliation: track `activated` vs `labeled`,
  count a never-made/rejected/errored call as unlabeled, and on shortfall call
  `report_incomplete` naming the affected work items. gh-aw treats that as a
  failure signal even on successful exit, so a truncated activation can no
  longer be recorded as clean. Cap exhaustion becomes a nameable cause when
  observed; #1683's rule against guessing at caps is preserved and narrowed.

Tests: `test/gh-aw-activation-capacity.test.ts` (27) covers the derived maximum,
capacity under both readings, at-maximum and one-over boundaries, and the
reconciliation contract. Compiled-artifact assertions prefer runtime evidence
over prose: the declared cap reaching `GH_AW_SAFE_OUTPUTS_CONFIG`, the injected
constraint carrying the same number, `report_incomplete` being present (and so
callable), and agent-job permissions staying `issues: read`. Fails closed if
`gh aw` is absent, per #1834. Mutation-checked: reverting the cap to 80 fails 4.

No changes to temporary-ID linkage (#1962), fast-path parity (#1959), label
result reporting (#1963), broad contract coverage (#1960), or E4 (#1958).
Step 2e is deliberately neutral about how `add_labels` identifies its target so
it does not conflict with #1962.

Closes #1961
Parent #1957

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

* fix(gh-aw): correct runtime claims in activation capacity safeguards

Independent review found three factual defects in the prose and tests
shipped by the first commit. All three are corrected against the pinned
gh-aw v0.87.10 sources rather than gh-aw's own injected descriptions.

1. `report_incomplete` does NOT make the run conclude non-successfully.
   `report_incomplete_handler.cjs` emits `core.warning` only, and
   `handle_agent_failure.cjs` contains zero `setFailed`/`process.exit`
   calls in 4453 lines. What it actually does is open or update a durable
   `[aw] {workflow} reported incomplete result` tracking issue. Step 2e
   now states that, says the run still reports success, and tells the
   agent never to rely on a red run to carry the signal. gh-aw's own tool
   description ("treated as a failure signal even when the agent exits
   successfully") is misleading; trusting it was exactly the injected-prose
   mistake #1961 exists to prevent.

   Limitation, stated honestly: no narrow mechanism in the pinned runtime
   makes an incomplete activation conclude red. Forcing one would require
   a custom safe-job, which is outside this issue's scope. The durable
   tracking record plus the "never report a clean activation you did not
   perform" rule are what carry the signal.

2. Cap enforcement is dual, not collection-only. Per Safe Outputs MCE4,
   `enforcePerTypeMax` in `safe_outputs_handlers.cjs` throws JSON-RPC
   `E002: {type} limit reached` at invocation time, which the agent DOES
   see; the collector then drops surplus items with a warning. Removed the
   claim that an over-limit item "never appears as an error to the agent".
   Reconciliation still keys on absence-of-success, not presence-of-error,
   because a call can also simply never be made.

3. The old `max: 80` did not cause runtime truncation — 50 calls never
   reached it. The real hazard is gh-aw's injected "Maximum 80 label(s)
   can be added" wording against a 100-label worst case, which invites
   agent self-truncation. 110 is justified by that hazard plus bounded
   margin, and the docblock now explicitly records what is NOT claimed.

Also, per the PR #1965 review: Step 2e no longer demands a real issue
number for items created during the run. Issue creation is deferred to
the post-agent job, so it names the stable temporary ID plus title and
intended labels, and quotes a real number only for a reused issue —
avoiding reintroduction of #1962's invalid-number assumption.

Tests: renamed the two overclaiming tests and gave each a negative guard
that fails if the retracted wording returns; added coverage for the
temporary-ID rule. Trimmed prose to stay under the existing 160 KB
source-growth guard rather than raising another test's budget.

Verified: 221/221 across the three gh-aw suites; mutation-checked (cap
-> 80 fails 5 tests incl. the compiled-runtime one, reintroducing either
retracted claim fails its guard); strict compile clean on all four
workflows; npm run build passes.

Closes #1961
Parent #1957

Working as FIDO (Quality Owner)

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 2471face-3b27-4419-bd48-0becd2642d7f

* test(gh-aw): use explicit .js extension on helper import

Addresses Copilot review on PR #1964. The reviewer is correct on both counts.

`tsc --module nodenext --moduleResolution nodenext` reports exactly one error
on this file, and TypeScript names the fix itself:

  test/gh-aw-activation-capacity.test.ts(94,46): error TS2835: Relative import
  paths need explicit file extensions in ECMAScript imports when
  '--moduleResolution' is 'node16' or 'nodenext'. Did you mean
  './helpers/gh-aw-lock.js'?

The repo is `"type": "module"` with `module`/`moduleResolution: NodeNext` in the
root tsconfig, and the four other suites importing this helper already use the
`.js` specifier. This file was the lone outlier; it now matches.

Verified: 293/293 across all eight suites that import the helper, all four
workflows compile with `--strict` (pre-existing warnings only), `npm run build`
passes, and the NodeNext type-check on this file is now clean.

Refs #1961. Parent #1957.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 2471face-3b27-4419-bd48-0becd2642d7f

* fix(workflows): state Step 2e counts as accepted operations, not applied labels

Copilot review flagged that Step 2e's reconciliation claimed issues
"received their labels". #1963 (now merged to dev) settled the
vocabulary: an accepted safe output is queued this turn and applied by
the post-agent job, so the agent has evidence only that a call was
accepted for a specific target -- never that a label reached GitHub.
Step 2e was the remaining place still asserting application, which
conflicts with the accepted-vs-applied rule the same skill now states.

- `labeled` is defined as issues whose add_labels call was accepted.
- The report_incomplete reason reads "had a label operation accepted".
- "proof that every label landed" -> "every label operation was accepted".
- Added an explicit statement that the counts track label operations,
  not labels present on GitHub.

Tests: new assertion guards the positive and both retired over-claim
phrasings; verified by reverting the prose (1 failed, 28 passed) rather
than assuming. Tightened the sibling assertion to match the exact count
definitions -- the previous `.*`-joined form matched a distant
"was accepted" elsewhere in the flattened skill and so failed to detect
a reverted definition.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 0515ad5d-4c5b-48bb-92a6-4e1296594cab

* test(gh-aw): anchor the false-success assertion, which was vacuous

Copilot review caught that the regex ended in an `|activated.*artifact`
alternative. Alternation binds loosest, so the whole pattern collapsed to
that branch and matched any mention of the artifact -- the test would
have passed with the `labeled < activated` condition deleted from the
prose, which is the entire invariant it names.

Anchored to the full sentence and verified by mutation: removing the
condition from Step 2e now fails (1 failed / 28 passed) where it
previously passed.

Second instance of this defect class in this suite; the sibling count
assertions were tightened in 4ec89dd for the same reason. Audited the
remaining alternations here -- `/finish green|still succeeds/` is
legitimate (two accepted phrasings, both specific).

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 0515ad5d-4c5b-48bb-92a6-4e1296594cab

---------

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 2471face-3b27-4419-bd48-0becd2642d7f
Copilot-Session: 0515ad5d-4c5b-48bb-92a6-4e1296594cab
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.

Use gh-aw temporary IDs for plan activation label targeting

2 participants