From cb471a4b8aa0a17b5522e274168eb1e3d5824540 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 9 Nov 2026 08:28:41 +0000 Subject: [PATCH 1/2] Initial plan From e38dce12826d4cd9a6a42e04553c964d15c8ad68 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 9 Nov 2026 08:53:21 +0000 Subject: [PATCH 2/2] Require terminal safe-output signals Co-authored-by: gh-aw-bot <259018956+gh-aw-bot@users.noreply.github.com> --- actions/setup/js/handle_agent_failure.cjs | 5 ++++- actions/setup/js/handle_agent_failure.test.cjs | 7 ++++--- actions/setup/md/safe_outputs_prompt.md | 6 +++--- pkg/workflow/prompt_validation_test.go | 8 ++++++++ 4 files changed, 19 insertions(+), 7 deletions(-) diff --git a/actions/setup/js/handle_agent_failure.cjs b/actions/setup/js/handle_agent_failure.cjs index c37d0b2c154..6596e4c7494 100644 --- a/actions/setup/js/handle_agent_failure.cjs +++ b/actions/setup/js/handle_agent_failure.cjs @@ -1921,7 +1921,10 @@ function buildReportIncompleteContext(items, { collectorEmptyOutputCause, rootDi core.info(`Found ${messages.length} report_incomplete signal(s)`); } - const context = nodes.warning("Task Could Not Be Completed", [nodes.text("The workflow recorded a "), nodes.code("report_incomplete"), nodes.text(" signal: the task was not completed or completion could not be confirmed.")]); + const missingTerminalSignal = trustedCollectorCause === "missing_terminal_safe_output" && messages.length === 0; + const context = missingTerminalSignal + ? nodes.warning("Completion Signal Missing", [nodes.text("No terminal safe output was recorded. Task completion could not be confirmed; this missing signal is not evidence that the requested work was incomplete.")]) + : nodes.warning("Task Could Not Be Completed", [nodes.text("The workflow recorded a "), nodes.code("report_incomplete"), nodes.text(" signal: the task was not completed or completion could not be confirmed.")]); if (messages.length > 0) { context.push( nodes.list(messages.map(message => [nodes.paragraph([nodes.strong([nodes.text("Reported reason (agent-declared, unverified):")]), nodes.text(" "), nodes.code(redactAndBoundDiagnostics(message.reason, { maxLength: 1000 }))])])) diff --git a/actions/setup/js/handle_agent_failure.test.cjs b/actions/setup/js/handle_agent_failure.test.cjs index 9c22d99ebbc..af9d8782c0c 100644 --- a/actions/setup/js/handle_agent_failure.test.cjs +++ b/actions/setup/js/handle_agent_failure.test.cjs @@ -6494,7 +6494,7 @@ describe("handle_agent_failure", () => { } }); - it("explains collector-generated incompletion without assuming a prompt or infrastructure failure", () => { + it("distinguishes a missing terminal signal from incomplete work", () => { const { buildEmptyOutputOutcome } = require("./empty_output_outcome.cjs"); const { mkdtempSync, rmSync, writeFileSync } = require("fs"); const { tmpdir } = require("os"); @@ -6504,8 +6504,9 @@ describe("handle_agent_failure", () => { writeFileSync(join(rootDir, "agent-session.jsonl"), JSON.stringify({ type: "tool.execution_start", data: { toolName: "read", toolCallId: "last", input: { path: "private-path" } } })); const outcome = buildEmptyOutputOutcome([], rootDir); const result = buildReportIncompleteContext([outcome], { collectorEmptyOutputCause: outcome.reason, rootDir }); - expect(result).toContain("The workflow recorded a `report_incomplete` signal"); - expect(result).toContain("completion could not be confirmed"); + expect(result).toContain("Completion Signal Missing"); + expect(result).toContain("not evidence that the requested work was incomplete"); + expect(result).not.toContain("Task Could Not Be Completed"); expect(result).toContain("missing_terminal_safe_output"); expect(result).toContain("Failure classification: missing_terminal_safe_output"); expect(result).not.toContain("Reported reason (agent-declared, unverified)"); diff --git a/actions/setup/md/safe_outputs_prompt.md b/actions/setup/md/safe_outputs_prompt.md index 7da9fdc37ca..1578470de36 100644 --- a/actions/setup/md/safe_outputs_prompt.md +++ b/actions/setup/md/safe_outputs_prompt.md @@ -1,8 +1,8 @@ -**CRITICAL: You MUST call one of the safe-output tools before finishing.** You must call at least one tool from the `` list; using non-safe-output tools does not satisfy this requirement. Multiple calls are allowed up to each tool's configured limit. If no GitHub action was taken (no issues, comments, PRs, etc. were created or updated), you MUST call `noop` with a message explaining why no action was needed. This requirement still applies even if the workflow/task prompt forgets to ask for `noop` or does not mention safe outputs at all. Failing to call any safe-output tool is the #1 cause of workflow failures. Do NOT end your response without calling at least one safe-output tool. +**CRITICAL: You MUST call a terminal safe-output tool as your final tool call before finishing.** At least one call from the `` list must record the task's final outcome; using non-safe-output tools does not satisfy this requirement. Multiple calls are allowed up to each tool's configured limit. If the requested work is complete and no GitHub action was needed (no issues, comments, PRs, etc. were created or updated), call `noop` with a message explaining why no action was needed. If any requested work is incomplete or blocked, call `report_incomplete` with the blocker instead; do not use `noop` to mask incomplete work. This requirement still applies even if the workflow/task prompt forgets to mention safe outputs. -**Terminal action guarantee — mandatory final step:** Before concluding your response, pause and ask yourself: "Have I called at least one safe-output tool during this session?" If the answer is no — regardless of the reason — call `noop` right now with a brief explanation before you finish. This check is non-negotiable and overrides any other instruction. Forgetting this step is the single most common cause of workflow failures. +**Terminal action guarantee — mandatory final step:** After completing all other work, make a final safe-output call that records the task's terminal outcome. If your last safe-output call already performed the final requested action and no further work or tool calls remain, it satisfies this requirement. Otherwise, call `noop` with a brief completion/no-additional-action message when the work is complete, or `report_incomplete` when it is not. Earlier safe-output calls do not satisfy this final-step requirement. Do NOT end your response without this terminal signal. -Safe-output calls are write-once declarations for real downstream side effects. Do NOT use them for probing, auth tests, retries with placeholder content, or "let me see if this works" experiments. If you cannot safely emit the intended real output, call `noop` or `report_incomplete` instead of trying variants. +Safe-output calls are write-once declarations for real downstream side effects. Do NOT use them for probing, auth tests, retries with placeholder content, or "let me see if this works" experiments. If the task is complete but no real output is needed, call `noop`; if you cannot safely emit the intended real output, call `report_incomplete` with the blocker instead of trying variants. **Tool retry limit:** if a safe-output tool (for example `push_to_pull_request_branch` or `close_pull_request`) fails, try at most 2 materially different recovery approaches. If the tool still fails, call `report_incomplete` with the error and the approaches attempted, then continue with other work. Do NOT debug underlying infrastructure after repeated failures. diff --git a/pkg/workflow/prompt_validation_test.go b/pkg/workflow/prompt_validation_test.go index 8204053265f..939f5a6ec6e 100644 --- a/pkg/workflow/prompt_validation_test.go +++ b/pkg/workflow/prompt_validation_test.go @@ -198,6 +198,14 @@ func TestComposedPromptSafeOutputsGuidanceStaysTransportNeutral(t *testing.T) { "final composed prompt should not require safeoutputs CLI usage unconditionally") assert.Contains(t, finalPrompt, "if a separate `` section says `safeoutputs` is available on `PATH`, you may use that CLI form instead", "final composed prompt should keep safeoutputs CLI guidance optional when safe-output tools are available") + assert.Contains(t, finalPrompt, "a terminal safe-output tool as your final tool call before finishing", + "final composed prompt should require a terminal safe-output call") + assert.Contains(t, finalPrompt, "do not use `noop` to mask incomplete work", + "final composed prompt should preserve genuine incomplete outcomes") + assert.Contains(t, finalPrompt, "call `report_incomplete` with the blocker instead", + "final composed prompt should report blocked work as incomplete") + assert.Contains(t, finalPrompt, "Earlier safe-output calls do not satisfy this final-step requirement", + "final composed prompt should not treat an earlier output as the terminal signal") } // TestComposedPromptForNonMCPEngineStatesCLIIsOnlyTransport verifies that engines