From 5fc0e94d4c1cdd96dc2fe5f9676c5d8123e51014 Mon Sep 17 00:00:00 2001 From: Brady Gaster Date: Tue, 1 Sep 2026 12:08:14 -0700 Subject: [PATCH 1/3] feat: close direct cast issues on merge Closes #1974 Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: d20ee964-dcbd-4e17-921d-e624f8bc0eb3 --- test/gh-aw-quality.test.ts | 73 ++++++++++++++++++++++++++++++++++++++ workflows/squad.md | 2 +- 2 files changed, 74 insertions(+), 1 deletion(-) diff --git a/test/gh-aw-quality.test.ts b/test/gh-aw-quality.test.ts index df5442013..c475768df 100644 --- a/test/gh-aw-quality.test.ts +++ b/test/gh-aw-quality.test.ts @@ -2574,6 +2574,79 @@ describe('gh-aw: Cast replaces disposable bootstrap state (#1909)', () => { }); }); +// --------------------------------------------------------------------------- +// gh-aw: Cast PR closes its originating issue on merge (#1974) +// +// A direct `/squad cast` command opens a PR that scaffolds the team. Prior to +// this change, that PR never referenced the invoking issue with a closing +// keyword, so merging it left the issue open. The Auto-Cast Pivot (TG-3) is a +// different code path that opens a Cast PR as a *side effect* of another +// command (e.g. `/squad plan`) finding no team yet -- that PR must NOT close +// the underlying work issue, since only the team was cast, not the requested +// work. Step 8 must therefore add the closing keyword only for the direct +// invocation, while TG-3's existing prohibition (tested above) stays intact. +// --------------------------------------------------------------------------- +describe('gh-aw: Cast PR closes its originating issue on merge (#1974)', () => { + const squadContent = readText(SQUAD_WORKFLOW); + const cast = squadContent.match( + /## skill: `squad-cast`\n[\s\S]*?(?=\n## skill:|$)/, + )?.[0] ?? ''; + const step8 = cast.match(/##### Step 8: Open PR\n[\s\S]*?(?=\n##### Step 9)/)?.[0] ?? ''; + + it('Step 8 exists and configures the branch/title used by direct Cast', () => { + expect(step8.length, 'Step 8: Open PR section should be found in the cast skill').toBeGreaterThan(0); + expect(step8).toContain('`create-pull-request`'); + expect(step8).toContain('branch `squad/cast-{repo}`'); + expect(step8).toContain('title `[squad] Cast your Squad — {description}`'); + }); + + it('Step 8 appends a literal `Closes #{issue_number}` line for a direct Cast invocation', () => { + expect(step8).toMatch(/append a standalone final body line:\s*`Closes #\{issue_number\}`/); + expect(step8).toMatch(/merging this PR automatically closes the issue that invoked `\/squad cast`/); + }); + + it('the closing keyword is scoped to the resolved target issue, never a different issue', () => { + expect(step8).toMatch(/resolved target issue from Trigger Context/i); + expect(step8).toMatch(/never a different issue/i); + }); + + it('explicitly excludes the Auto-Cast Pivot (TG-3), which forbids closing keywords', () => { + expect(step8).toMatch(/not the Auto-Cast Pivot in TG-3, which forbids closing keywords/i); + // TG-3's own prohibition (asserted elsewhere) must still be present and unweakened. + expect(cast === squadContent ? squadContent : squadContent).toMatch( + /MUST NOT contain.*Fixes.*Closes.*Resolves/i, + ); + }); + + it('instructs writing the literal keyword without surrounding backticks in the real PR body', () => { + expect(step8).toMatch(/no surrounding backticks in the actual PR body/i); + }); + + it('preserves existing Step 8 PR content: team summary and the post-merge rerun instruction', () => { + expect(step8).toContain('body with team summary'); + expect(step8).toMatch( + /return to the originating issue and rerun `\{canonical_command\}` to resume your work/, + ); + }); + + it('preserves the Step 6 payload allowlist enumeration instruction', () => { + expect(step8).toMatch( + /Enumerate every concrete path from the validated Step 6 payload allowlist as the file request/, + ); + expect(step8).toMatch(/do not add any other path/); + }); + + it('does not weaken auto-close-issue: false, which still guards Connect/Adopt/Cast-Member/Auto-Pivot', () => { + // The literal Closes # line (not gh-aw's built-in auto-close-issue) is what closes the + // issue for direct Cast, so the shared safe-output config can stay false for every other + // create-pull-request call in this workflow (Connect, Adopt, Cast-Member, Auto-Cast Pivot). + const frontmatter = extractFrontmatter(SQUAD_WORKFLOW); + const safeOutputs = extractSafeOutputs(frontmatter); + const pr = safeOutputs['create-pull-request'] as Record; + expect(pr['auto-close-issue']).toBe(false); + }); +}); + // --------------------------------------------------------------------------- // gh-aw: Threat detection taxonomy regression (#1701) // diff --git a/workflows/squad.md b/workflows/squad.md index f0ed0f80e..6079264cc 100644 --- a/workflows/squad.md +++ b/workflows/squad.md @@ -838,7 +838,7 @@ not describe this as a post-agent or independently fail-closed gate. ##### Step 8: Open PR -`create-pull-request`: branch `squad/cast-{repo}`, title `[squad] Cast your Squad — {description}`, body with team summary. Append to the PR body: "After merging, return to the originating issue and rerun `{canonical_command}` to resume your work." Enumerate every concrete path from the validated Step 6 payload allowlist as the file request; do not add any other path. +`create-pull-request`: branch `squad/cast-{repo}`, title `[squad] Cast your Squad — {description}`, body with team summary. Append to the PR body: "After merging, return to the originating issue and rerun `{canonical_command}` to resume your work." When this Cast run was invoked directly on an issue (not the Auto-Cast Pivot in TG-3, which forbids closing keywords per its own rule above), append a standalone final body line: `Closes #{issue_number}`, using the resolved target issue from Trigger Context — never a different issue — so merging this PR automatically closes the issue that invoked `/squad cast`. Write the literal text with no surrounding backticks in the actual PR body (the backticks here mark the literal string to reproduce). Enumerate every concrete path from the validated Step 6 payload allowlist as the file request; do not add any other path. ##### Step 9: Post Completion From 7d94a0d756250e04e0966261202b92d4dd68feac Mon Sep 17 00:00:00 2001 From: Brady Gaster Date: Tue, 1 Sep 2026 15:24:56 -0700 Subject: [PATCH 2/3] test: simplify cast pivot assertion Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: d20ee964-dcbd-4e17-921d-e624f8bc0eb3 --- test/gh-aw-quality.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/gh-aw-quality.test.ts b/test/gh-aw-quality.test.ts index 379297351..128f9a428 100644 --- a/test/gh-aw-quality.test.ts +++ b/test/gh-aw-quality.test.ts @@ -2625,7 +2625,7 @@ describe('gh-aw: Cast PR closes its originating issue on merge (#1974)', () => { it('explicitly excludes the Auto-Cast Pivot (TG-3), which forbids closing keywords', () => { expect(step8).toMatch(/not the Auto-Cast Pivot in TG-3, which forbids closing keywords/i); // TG-3's own prohibition (asserted elsewhere) must still be present and unweakened. - expect(cast === squadContent ? squadContent : squadContent).toMatch( + expect(squadContent).toMatch( /MUST NOT contain.*Fixes.*Closes.*Resolves/i, ); }); From 9bbb12dad7158098602d5e549eebfc1fabb81fe5 Mon Sep 17 00:00:00 2001 From: Brady Gaster Date: Tue, 1 Sep 2026 15:36:17 -0700 Subject: [PATCH 3/3] fix: clarify cast issue substitution Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: d20ee964-dcbd-4e17-921d-e624f8bc0eb3 --- test/gh-aw-quality.test.ts | 14 ++++++++------ workflows/squad.md | 2 +- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/test/gh-aw-quality.test.ts b/test/gh-aw-quality.test.ts index 128f9a428..2fbd5fab1 100644 --- a/test/gh-aw-quality.test.ts +++ b/test/gh-aw-quality.test.ts @@ -2612,14 +2612,15 @@ describe('gh-aw: Cast PR closes its originating issue on merge (#1974)', () => { expect(step8).toContain('title `[squad] Cast your Squad — {description}`'); }); - it('Step 8 appends a literal `Closes #{issue_number}` line for a direct Cast invocation', () => { - expect(step8).toMatch(/append a standalone final body line:\s*`Closes #\{issue_number\}`/); + it('Step 8 templates a closing line with the resolved issue number for a direct Cast invocation', () => { + expect(step8).toMatch(/append a standalone final body line in the form `Closes #\{issue_number\}`/); expect(step8).toMatch(/merging this PR automatically closes the issue that invoked `\/squad cast`/); + expect(step8).toMatch(/Replace `\{issue_number\}` with the resolved numeric target issue number/); }); it('the closing keyword is scoped to the resolved target issue, never a different issue', () => { - expect(step8).toMatch(/resolved target issue from Trigger Context/i); - expect(step8).toMatch(/never a different issue/i); + expect(step8).toMatch(/resolved numeric target issue number from Trigger Context/i); + expect(step8).toMatch(/never .*reference a different issue/i); }); it('explicitly excludes the Auto-Cast Pivot (TG-3), which forbids closing keywords', () => { @@ -2630,8 +2631,9 @@ describe('gh-aw: Cast PR closes its originating issue on merge (#1974)', () => { ); }); - it('instructs writing the literal keyword without surrounding backticks in the real PR body', () => { - expect(step8).toMatch(/no surrounding backticks in the actual PR body/i); + it('instructs omitting placeholder braces and documentation backticks from the real PR body', () => { + expect(step8).toMatch(/never emit the braces/i); + expect(step8).toMatch(/omit the documentation backticks from the actual PR body/i); }); it('preserves existing Step 8 PR content: team summary and the post-merge rerun instruction', () => { diff --git a/workflows/squad.md b/workflows/squad.md index 7bae87a42..9354b21de 100644 --- a/workflows/squad.md +++ b/workflows/squad.md @@ -839,7 +839,7 @@ not describe this as a post-agent or independently fail-closed gate. ##### Step 8: Open PR -`create-pull-request`: branch `squad/cast-{repo}`, title `[squad] Cast your Squad — {description}`, body with team summary. Append to the PR body: "After merging, return to the originating issue and rerun `{canonical_command}` to resume your work." When this Cast run was invoked directly on an issue (not the Auto-Cast Pivot in TG-3, which forbids closing keywords per its own rule above), append a standalone final body line: `Closes #{issue_number}`, using the resolved target issue from Trigger Context — never a different issue — so merging this PR automatically closes the issue that invoked `/squad cast`. Write the literal text with no surrounding backticks in the actual PR body (the backticks here mark the literal string to reproduce). Enumerate every concrete path from the validated Step 6 payload allowlist as the file request; do not add any other path. +`create-pull-request`: branch `squad/cast-{repo}`, title `[squad] Cast your Squad — {description}`, body with team summary. Append to the PR body: "After merging, return to the originating issue and rerun `{canonical_command}` to resume your work." When this Cast run was invoked directly on an issue (not the Auto-Cast Pivot in TG-3, which forbids closing keywords per its own rule above), append a standalone final body line in the form `Closes #{issue_number}` so merging this PR automatically closes the issue that invoked `/squad cast`. Replace `{issue_number}` with the resolved numeric target issue number from Trigger Context — never emit the braces or reference a different issue — and omit the documentation backticks from the actual PR body. Enumerate every concrete path from the validated Step 6 payload allowlist as the file request; do not add any other path. ##### Step 9: Post Completion