From 5ca4f239bdde38ba720af876a2d3bfbf198dc7d3 Mon Sep 17 00:00:00 2001 From: brady gaster Date: Thu, 20 Aug 2026 00:58:02 -0700 Subject: [PATCH 1/2] Prevent dispatch workflow schema probes Document dispatch_workflow as write-once in the implement worker prompt so schema discovery probes cannot consume the single safe-output slot before the real continuation payload. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 26891f18-3f78-4cda-801b-ee668fecae33 --- test/gh-aw-quality.test.ts | 23 +++++++++++++++++++++++ workflows/squad-implement-worker.md | 18 +++++++++++++----- 2 files changed, 36 insertions(+), 5 deletions(-) diff --git a/test/gh-aw-quality.test.ts b/test/gh-aw-quality.test.ts index 111c0fa4b..63c3e35c8 100644 --- a/test/gh-aw-quality.test.ts +++ b/test/gh-aw-quality.test.ts @@ -1066,6 +1066,29 @@ describe('gh-aw: merge continuation dispatch contract', () => { } }); + it('worker continuation warns dispatch_workflow is write-once and not schema-probed', () => { + const continuation = workerContent.match( + /## Continue Parent Epic After Merge([\s\S]*?)The remaining instructions apply only to `workflow_dispatch`/ + )?.[1] ?? ''; + + expect(continuation, 'continuation should name the dispatch_workflow tool').toMatch(/dispatch_workflow/); + expect(continuation, 'dispatch_workflow should be called once only with the final payload').toMatch( + /exactly once[\s\S]*complete payload/i + ); + expect(continuation, 'empty or placeholder schema probes should be forbidden').toMatch( + /NEVER[\s\S]*empty[\s\S]*(?:placeholder|partial)[\s\S]*(?:probe|discover)[\s\S]*schema/i + ); + expect(continuation, 'the prompt should say the schema is already supplied').toMatch( + /full schema[\s\S]*already given[\s\S]*nothing to discover/i + ); + expect(continuation, 'noop should be named as the alternative when not dispatching').toMatch( + /not ready to dispatch[\s\S]*noop/i + ); + expect(continuation, 'the consequence of a probe should be explicit').toMatch( + /FIRST[\s\S]*wins[\s\S]*later calls[\s\S]*discarded[\s\S]*probe destroys/i + ); + }); + it('worker continuation comments on the parent epic instead of auto-targeting the merged PR', () => { const continuation = workerContent.match( /## Continue Parent Epic After Merge([\s\S]*?)The remaining instructions apply only to `workflow_dispatch`/ diff --git a/workflows/squad-implement-worker.md b/workflows/squad-implement-worker.md index 9a4616a57..da5eeb778 100644 --- a/workflows/squad-implement-worker.md +++ b/workflows/squad-implement-worker.md @@ -223,8 +223,15 @@ For a merged pull request: relationship, falling back to its `Parent: #N` body line. 3. If no parent epic exists, comment on the merged pull request saying its issue is standalone and that no further work was queued, then stop. -4. Call the prompt-listed `dispatch_workflow` safe-output tool exactly once with - the workflow inputs nested under `inputs`: +4. WRITE-ONCE: call the prompt-listed `dispatch_workflow` safe-output tool + exactly once, and only when the complete payload is ready. NEVER call + `dispatch_workflow` with empty, partial, or placeholder arguments to probe or + discover its schema. The full schema is already given in this prompt; there + is nothing to discover. If you are not ready to dispatch, or there is no next + wave to dispatch, call `noop` instead of `dispatch_workflow`. The FIRST + `dispatch_workflow` call wins and all later calls are silently discarded, so + a probe destroys the real dispatch. When dispatching, nest the workflow + inputs under `inputs`: ```json { @@ -250,9 +257,10 @@ comment — never a silent exit. Cover both terminal cases: - No parent epic → state that the pull request's issue is standalone and that nothing further was queued. -Never emit `noop` for a merge continuation. `noop` is not reported as a comment, -so it strands a merged pull request with no signal about what happens next — the -exact failure this procedure exists to prevent. +Never emit `noop` for a merge continuation as a substitute for the visible +continuation comment. `noop` is not reported as a comment, so it strands a +merged pull request with no signal about what happens next — the exact failure +this procedure exists to prevent. The remaining instructions apply only to `workflow_dispatch`. From 5ba613bf22d61bfe94ada76bcce56fd72134b1ad Mon Sep 17 00:00:00 2001 From: brady gaster Date: Thu, 20 Aug 2026 00:59:54 -0700 Subject: [PATCH 2/2] Loosen dispatch prompt contract assertion Keep the test focused on the stable contract: dispatch_workflow must not be probed with empty or placeholder arguments, and noop is the alternative when there is nothing to dispatch. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 26891f18-3f78-4cda-801b-ee668fecae33 --- test/gh-aw-quality.test.ts | 13 ++----------- 1 file changed, 2 insertions(+), 11 deletions(-) diff --git a/test/gh-aw-quality.test.ts b/test/gh-aw-quality.test.ts index 63c3e35c8..977df8759 100644 --- a/test/gh-aw-quality.test.ts +++ b/test/gh-aw-quality.test.ts @@ -1072,20 +1072,11 @@ describe('gh-aw: merge continuation dispatch contract', () => { )?.[1] ?? ''; expect(continuation, 'continuation should name the dispatch_workflow tool').toMatch(/dispatch_workflow/); - expect(continuation, 'dispatch_workflow should be called once only with the final payload').toMatch( - /exactly once[\s\S]*complete payload/i - ); expect(continuation, 'empty or placeholder schema probes should be forbidden').toMatch( /NEVER[\s\S]*empty[\s\S]*(?:placeholder|partial)[\s\S]*(?:probe|discover)[\s\S]*schema/i ); - expect(continuation, 'the prompt should say the schema is already supplied').toMatch( - /full schema[\s\S]*already given[\s\S]*nothing to discover/i - ); - expect(continuation, 'noop should be named as the alternative when not dispatching').toMatch( - /not ready to dispatch[\s\S]*noop/i - ); - expect(continuation, 'the consequence of a probe should be explicit').toMatch( - /FIRST[\s\S]*wins[\s\S]*later calls[\s\S]*discarded[\s\S]*probe destroys/i + expect(continuation, 'noop should be named as the alternative when there is nothing to dispatch').toMatch( + /(?:nothing|no next wave)[\s\S]{0,200}dispatch[\s\S]{0,200}`?noop`?|`?noop`?[\s\S]{0,200}(?:nothing|no next wave)[\s\S]{0,200}dispatch/i ); });