diff --git a/.github/scripts/check-agent-result.cjs b/.github/scripts/check-agent-result.cjs new file mode 100644 index 00000000..1c3e8e29 --- /dev/null +++ b/.github/scripts/check-agent-result.cjs @@ -0,0 +1,55 @@ +const { readFileSync } = require("node:fs"); + +const terminalTypes = new Set(["create_pull_request", "noop", "report_incomplete"]); + +function requireTerminalResult(content) { + const results = []; + for (const [index, line] of content.split(/\r?\n/).entries()) { + if (!line.trim()) { + continue; + } + + let item; + try { + item = JSON.parse(line); + } catch { + throw new Error(`Safe output line ${index + 1} is not valid JSON.`); + } + if ( + item === null || + typeof item !== "object" || + Array.isArray(item) || + typeof item.type !== "string" + ) { + throw new Error(`Safe output line ${index + 1} must be an object with a type.`); + } + + const type = item.type.replaceAll("-", "_"); + if (terminalTypes.has(type)) { + results.push(type); + } + } + + if (results.length !== 1) { + throw new Error( + `Expected exactly one terminal safe output, found ${results.length}. ` + + "Call create_pull_request, noop, or report_incomplete; a prose-only explanation is not a result.", + ); + } + return results[0]; +} + +if (require.main === module) { + try { + if (process.argv.length !== 3) { + throw new Error("Usage: node check-agent-result.cjs "); + } + const result = requireTerminalResult(readFileSync(process.argv[2], "utf8")); + console.log(`Verified terminal safe output: ${result}`); + } catch (error) { + console.error(`::error::${error instanceof Error ? error.message : String(error)}`); + process.exitCode = 1; + } +} + +module.exports = { requireTerminalResult }; diff --git a/.github/scripts/check-agent-result.test.cjs b/.github/scripts/check-agent-result.test.cjs new file mode 100644 index 00000000..fd877def --- /dev/null +++ b/.github/scripts/check-agent-result.test.cjs @@ -0,0 +1,127 @@ +const assert = require("node:assert/strict"); +const { spawnSync } = require("node:child_process"); +const { copyFileSync, mkdtempSync, rmdirSync, unlinkSync, writeFileSync } = require("node:fs"); +const { tmpdir } = require("node:os"); +const { join } = require("node:path"); +const { test } = require("node:test"); +const { requireTerminalResult } = require("./check-agent-result.cjs"); + +const script = join(__dirname, "check-agent-result.cjs"); +const encode = (items) => items.map((item) => JSON.stringify(item)).join("\n"); + +for (const item of [ + { type: "noop", message: "Completed review; no eligible change." }, + { type: "create_pull_request", title: "Fix behavior", body: "Verified fix.", branch: "fix-behavior" }, + { type: "report_incomplete", reason: "Duplicate evidence is blocked.", details: "No edits published." }, +]) { + test(`accepts one ${item.type} result`, () => { + assert.equal(requireTerminalResult(encode([item])), item.type); + }); +} + +test("matches gh-aw's hyphenated type normalization", () => { + assert.equal( + requireTerminalResult(encode([{ type: "report-incomplete", reason: "Blocked." }])), + "report_incomplete", + ); +}); + +test("allows other safe outputs alongside one terminal result", () => { + assert.equal( + requireTerminalResult(encode([{ type: "missing_data", reason: "Unavailable." }, { type: "report_incomplete" }])), + "report_incomplete", + ); +}); + +test("accepts blank lines and CRLF output", () => { + assert.equal(requireTerminalResult('\r\n{"type":"noop"}\r\n\r\n'), "noop"); +}); + +for (const content of ["", " \n\r\n", '{"type":"missing_data"}\n']) { + test(`rejects output without a terminal result: ${JSON.stringify(content)}`, () => { + assert.throws(() => requireTerminalResult(content), /exactly one terminal safe output, found 0/); + }); +} + +test("rejects the blocked-check prose instead of treating it as success", () => { + assert.throws( + () => requireTerminalResult("Unable to publish because the required duplicate check was incomplete."), + /not valid JSON/, + ); +}); + +test("rejects contradictory terminal results", () => { + assert.throws( + () => requireTerminalResult(encode([{ type: "noop" }, { type: "report_incomplete" }])), + /exactly one terminal safe output, found 2/, + ); +}); + +test("rejects repeated terminal calls", () => { + assert.throws( + () => requireTerminalResult(encode([{ type: "noop" }, { type: "noop" }])), + /exactly one terminal safe output, found 2/, + ); +}); + +for (const item of [null, [], 1, "noop", {}, { type: 1 }]) { + test(`rejects invalid safe-output records: ${JSON.stringify(item)}`, () => { + assert.throws(() => requireTerminalResult(encode([item])), /object with a type/); + }); +} + +test("does not ignore malformed lines after a valid result", () => { + assert.throws(() => requireTerminalResult('{"type":"noop"}\ninvalid'), /line 2 is not valid JSON/); +}); + +test("CLI fails a missing result file with an error annotation", (t) => { + const directory = mkdtempSync(join(tmpdir(), "agent-result-")); + t.after(() => rmdirSync(directory)); + const result = spawnSync(process.execPath, [script, join(directory, "missing.jsonl")], { encoding: "utf8" }); + assert.equal(result.status, 1); + assert.match(result.stderr, /::error::.*ENOENT/); +}); + +test("prepared validation is unaffected by later checkout edits", (t) => { + const directory = mkdtempSync(join(tmpdir(), "agent-result-")); + const checkoutScript = join(directory, "checkout.cjs"); + const preparedScript = join(directory, "prepared.cjs"); + const output = join(directory, "safe-outputs.jsonl"); + copyFileSync(script, checkoutScript); + copyFileSync(checkoutScript, preparedScript); + writeFileSync(checkoutScript, "process.exit(0);\n"); + writeFileSync(output, ""); + t.after(() => { + for (const file of [checkoutScript, preparedScript, output]) { + unlinkSync(file); + } + rmdirSync(directory); + }); + const result = spawnSync(process.execPath, [preparedScript, output], { encoding: "utf8" }); + assert.equal(result.status, 1); + assert.match(result.stderr, /::error::.*found 0/); +}); + +for (const [name, content, status] of [ + ["empty result", "", 1], + ["completed review", '{"type":"noop","message":"Reviewed."}\n', 0], + ["reported blocker", '{"type":"report_incomplete","reason":"Evidence is blocked."}\n', 0], +]) { + test(`CLI handles ${name}`, (t) => { + const directory = mkdtempSync(join(tmpdir(), "agent-result-")); + const file = join(directory, "safe-outputs.jsonl"); + writeFileSync(file, content); + t.after(() => { + unlinkSync(file); + rmdirSync(directory); + }); + const result = spawnSync(process.execPath, [script, file], { encoding: "utf8" }); + assert.equal(result.status, status, result.stderr); + if (status === 0) { + assert.match(result.stdout, /Verified terminal safe output/); + assert.equal(result.stderr, ""); + } else { + assert.match(result.stderr, /::error::.*found 0/); + } + }); +} diff --git a/.github/workflows/dotnet-port-api-nightly.lock.yml b/.github/workflows/dotnet-port-api-nightly.lock.yml index 13e2a4a7..e4457f17 100644 --- a/.github/workflows/dotnet-port-api-nightly.lock.yml +++ b/.github/workflows/dotnet-port-api-nightly.lock.yml @@ -1,4 +1,4 @@ -# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"4ee5b9d96468f649c9b769d0d386673c69071b685fa6a2045deb6df16f523cb6","body_hash":"7a6f000b96ed302c8d568203c70afe6806dab40373bb6eabe2261da82abaab96","compiler_version":"v0.89.21","strict":true,"agent_id":"copilot","agent_model":"gpt-5.6","engine_versions":{"copilot":"1.0.87"}} +# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"08c67a6866d3119b28b5f07c93d98aef3ff856d85a6c12769ea67f3d0bc6e94d","body_hash":"c80d5fa0c33d596f3e9cfd8cc5ec3c89611018b3f52514e36ca60364955268da","compiler_version":"v0.89.21","strict":true,"agent_id":"copilot","agent_model":"gpt-5.6","engine_versions":{"copilot":"1.0.87"}} # gh-aw-manifest: {"version":1,"secrets":["GHMANAGER_GITHUBAPP_MICROSOFT_AGENT_FRAMEWORK_FOR_GO_PRIVATE_KEY_PEM","GH_AW_CI_TRIGGER_TOKEN","GH_AW_DEFAULT_OTLP_ENDPOINT","GH_AW_DEFAULT_OTLP_HEADERS","GH_AW_GITHUB_MCP_SERVER_TOKEN","GH_AW_GITHUB_TOKEN","GITHUB_TOKEN"],"actions":[{"repo":"actions/checkout","sha":"3d3c42e5aac5ba805825da76410c181273ba90b1","version":"v7.0.1"},{"repo":"actions/create-github-app-token","sha":"bcd2ba49218906704ab6c1aa796996da409d3eb1","version":"v3.2.0"},{"repo":"actions/download-artifact","sha":"3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c","version":"v8.0.1"},{"repo":"actions/github-script","sha":"3a2844b7e9c422d3c10d287c895573f7108da1b3","version":"v9.0.0"},{"repo":"actions/setup-node","sha":"820762786026740c76f36085b0efc47a31fe5020","version":"v7.0.0"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"},{"repo":"github/gh-aw-actions/setup","sha":"924af5fdc64061cfbf66fb584c8b07e2ac230c60","version":"v0.89.21"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.28.25","digest":"sha256:25fbbefb92b690d4e6ef34de8df057c0349e6adf2b7486c0cc56954a8e9a3cd4","pinned_image":"ghcr.io/github/gh-aw-firewall/agent:0.28.25@sha256:25fbbefb92b690d4e6ef34de8df057c0349e6adf2b7486c0cc56954a8e9a3cd4"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.28.25","digest":"sha256:c3c7082e73ba83052c580097e5a9c9c40a11e6f6b6fa0a4710e6859f83a62854","pinned_image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.28.25@sha256:c3c7082e73ba83052c580097e5a9c9c40a11e6f6b6fa0a4710e6859f83a62854"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.28.25","digest":"sha256:94cac14372280dbf4a08d021d7b5810a9fd7d73c88802e04a9e05551b1cbcd09","pinned_image":"ghcr.io/github/gh-aw-firewall/squid:0.28.25@sha256:94cac14372280dbf4a08d021d7b5810a9fd7d73c88802e04a9e05551b1cbcd09"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.4.25","digest":"sha256:9be0a86220e807a0ecc89e53d7453468f7a53fbc6b3d1efd2299025ffe01d086","pinned_image":"ghcr.io/github/gh-aw-mcpg:v0.4.25@sha256:9be0a86220e807a0ecc89e53d7453468f7a53fbc6b3d1efd2299025ffe01d086"},{"image":"ghcr.io/github/gh-aw-node","digest":"sha256:a8082161d7dceda14b68f32eb39d0eaa96b825d07f5895b096afab9d9e0c7748","pinned_image":"ghcr.io/github/gh-aw-node@sha256:a8082161d7dceda14b68f32eb39d0eaa96b825d07f5895b096afab9d9e0c7748"},{"image":"ghcr.io/github/github-mcp-server:v1.12.2","digest":"sha256:508a0857ec762b1ab1cece29193345b501fab1dd9d1228a7b617062954cecac6","pinned_image":"ghcr.io/github/github-mcp-server:v1.12.2@sha256:508a0857ec762b1ab1cece29193345b501fab1dd9d1228a7b617062954cecac6"}],"mcp_servers":[{"name":"github","tools":["get_commit","get_file_contents","get_latest_release","get_me","get_pull_request","get_pull_request_comments","get_pull_request_diff","get_pull_request_files","get_pull_request_review_comments","get_pull_request_reviews","get_pull_request_status","get_release_by_tag","get_tag","issue_read","list_branches","list_commits","list_issue_types","list_issues","list_pull_requests","list_releases","list_starred_repositories","list_tags","pull_request_read","search_code","search_issues","search_pull_requests","search_repositories"]},{"name":"safeoutputs","tools":["create_pull_request","missing_data","missing_tool","noop"]}]} # This file was automatically generated by gh-aw (v0.89.21). DO NOT EDIT. To debug this workflow, load the skill at https://github.com/github/gh-aw/blob/main/debug.md # @@ -489,6 +489,8 @@ jobs: run: "set -euo pipefail\ngo -C _catalog mod download\ngit diff --exit-code -- _catalog/go.mod _catalog/go.sum\ndata=/tmp/gh-aw/agent/dotnet-port-api\nmkdir -p \"$data\"\ngo -C _catalog run ./cmd/symbolmap gaps -limit 0 > \"$data/catalog-gaps.json\"\nprintf 'DOTNET_GAPS_FILE=%s\\n' \"$data/catalog-gaps.json\" >> \"$GITHUB_ENV\"\n" shell: bash working-directory: ${{ github.workspace }} + - name: Prepare terminal result validation + run: cp .github/scripts/check-agent-result.cjs "$RUNNER_TEMP/gh-aw/check-agent-result.cjs" - name: Configure Git credentials env: @@ -1144,6 +1146,12 @@ jobs: if [ ! -f /tmp/gh-aw/agent_output.json ]; then echo '{"items":[]}' > /tmp/gh-aw/agent_output.json fi + - env: + GH_AW_SAFE_OUTPUTS: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS }} + if: success() + name: Require a terminal agent result + run: node "$RUNNER_TEMP/gh-aw/check-agent-result.cjs" "$GH_AW_SAFE_OUTPUTS" + # Small dedicated copy of the agent output so safe-output processing # survives a failed or timed-out upload of the larger agent artifact - name: Upload agent output fallback artifact diff --git a/.github/workflows/dotnet-port-api-nightly.md b/.github/workflows/dotnet-port-api-nightly.md index f19110d4..975e6055 100644 --- a/.github/workflows/dotnet-port-api-nightly.md +++ b/.github/workflows/dotnet-port-api-nightly.md @@ -51,6 +51,14 @@ steps: mkdir -p "$data" go -C _catalog run ./cmd/symbolmap gaps -limit 0 > "$data/catalog-gaps.json" printf 'DOTNET_GAPS_FILE=%s\n' "$data/catalog-gaps.json" >> "$GITHUB_ENV" + - name: Prepare terminal result validation + run: cp .github/scripts/check-agent-result.cjs "$RUNNER_TEMP/gh-aw/check-agent-result.cjs" +post-steps: + - name: Require a terminal agent result + if: success() + env: + GH_AW_SAFE_OUTPUTS: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS }} + run: node "$RUNNER_TEMP/gh-aw/check-agent-result.cjs" "$GH_AW_SAFE_OUTPUTS" permissions: contents: read pull-requests: read @@ -144,8 +152,12 @@ For `search_issues` and `search_pull_requests`, always include `repo:microsoft/a Searches discover references; direct reads resolve them. Track references by repository, resource type (`issue` or `pr`), and number, with their candidate/query and any filtering or failure. Read relevant bodies, comments, diffs, and outcomes directly through the matching tool; never include bodies in search results or re-search to rediscover a known reference. Reuse resolved evidence across candidate checks. Skip only work that implements, actively claims, or explicitly rejects the exact missing behavior, including older fallback tracking issues. A matching title, base feature port, internal refactor, or preservation test is not enough; a closed PR is not necessarily merged. +Current Go code is the source of truth for implemented coverage. Identify the actual declarations, callers, and tests that satisfy the selected contract; a proposed patch or a PR body's description is not checkout evidence. When citing a PR as implemented work, use `pull_request_read` with `method: get` and record its `state`, `merged`, and `merged_at`. Require `merged: true` and a non-null merge timestamp, then verify the behavior still exists in the current checkout. Search/list results and `state: closed` alone do not establish a merge. A closed, unmerged PR may document a maintainer's explicit rejection, but record that as a rejected proposal, never as implemented coverage; without such a decision, reassess the candidate against current code. + A filtered search item is not an explicit denial of a direct read. For each potentially relevant filtered reference, attempt one matching `issue_read` or `pull_request_read` through the same guarded GitHub MCP, unless that direct read was already denied. If allowed, assess its actual relevance; if denied or required evidence remains unavailable, keep that candidate blocked during selection and final recheck. Do not retry an explicitly denied read, lower integrity policy, change transports, call a trapped guard, or use sandbox `gh` or unconfigured downloads. Narrower empty searches, an accessible partial diff, passing tests, or disclosing the filter do not clear unresolved evidence. +Filtering that hides issue/PR identifiers is also unresolved evidence, not an empty result. Record the query and filtered count without inventing references or searching around the policy. If required duplicate evidence still blocks an otherwise eligible candidate and no fully verified candidate can be selected within the budget, call `report_incomplete`; do not stop with a prose-only explanation or substitute `noop`. Source-backed exclusions remain valid, and a blocked alternative does not invalidate a fully verified selection. + ## Review and selection Verify `DOTNET_UPSTREAM_SHA` and screen every row of `DOTNET_GAPS_FILE`, reading remaining ranges if a preview is cut off. Record the screened count and `page.total`; an unreadable or incompletely screened report is incomplete selection, not proof of no work. Keep selection read-only: group related catalog leaves into coherent candidates and inspect at most three promising groups in depth. @@ -168,6 +180,8 @@ Before editing, confirm the selected catalog leaves, the pinned .NET contract an Call one terminal safe-output tool and inspect its result before final prose; a prose summary is not a tool call: +Post-execution validation requires exactly one `create_pull_request`, `noop`, or `report_incomplete` result. Exit code zero does not waive this requirement. For every claim of already-implemented coverage, include current Go file/symbol evidence and the verified merge status of any PR cited as implementation evidence. + - `create_pull_request`: one verified, tested gap closure with a clear final duplicate check. Send only `title`, `body`, and `branch`; omit `temporary_id` and other optional arguments. Draft status, base branch, and title prefix are configured already; this single PR needs no temporary ID. Use a concrete title and sections **Summary**, **Ported .NET PRs**, **Breaking Changes**, **Tests and Examples**, and **Notes**. Include inventoried package versions, pinned SHA, exact catalog leaves and expected assessment changes, relevant upstream commits/PRs (or `None` if no specific PR was ported), immutable evidence, experimental and duplicate checks, and actual validation. Keep prose concise without hard-wrapping paragraphs. Report publication as queued, not a confirmed PR; never push, merge, approve, or open PRs directly. - `noop`: completed bounded gap review with no eligible unclaimed change and no potentially eligible candidate left blocked. Source-backed exclusions, including experimental, fix-only, already covered, and too broad, are completed reviews, not blockers. State the pinned SHA, screened/total gap counts, actual leaves/groups inspected, exclusion or already-satisfied reasons, and existing-work links. Do not claim a full catalog audit from screening rows and inspecting a few candidates; an empty diff alone does not establish completion. - `report_incomplete`: no fully verified selection is possible because required selection/source/duplicate evidence remains unresolved, or the selected port's validation fails, or execution reaches a runtime limit. Send only `reason` and `details`. Include the operation/error, recovery attempts, SHA, screened/total gap counts, blocked candidates, and remaining work. Leave partial edits unpublished; do not substitute `noop` or report a working-but-denied tool as missing. The three-group selection limit alone is not a failure. diff --git a/.github/workflows/dotnet-port-fixes-nightly.lock.yml b/.github/workflows/dotnet-port-fixes-nightly.lock.yml index 402ffd52..d7e1263b 100644 --- a/.github/workflows/dotnet-port-fixes-nightly.lock.yml +++ b/.github/workflows/dotnet-port-fixes-nightly.lock.yml @@ -1,4 +1,4 @@ -# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"41a7282a7691fa2fc91d621c6cee01c005b4e0550c95a406b87c743e48a32f60","body_hash":"7213d602bcc057b745bfa70eb51d2e08ca981d5dd77ffb7858dd2ebdcdd582ab","compiler_version":"v0.89.21","strict":true,"agent_id":"copilot","agent_model":"gpt-5.6","engine_versions":{"copilot":"1.0.87"}} +# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"c977ec921192dbeed076d2e667d7eed0ebf9fcbfd8f6c94300a7d2ef39680610","body_hash":"6305d89bed6c7974b275a85411ef8098449b9a276ffa8ab60ae1c1ab13d21d6d","compiler_version":"v0.89.21","strict":true,"agent_id":"copilot","agent_model":"gpt-5.6","engine_versions":{"copilot":"1.0.87"}} # gh-aw-manifest: {"version":1,"secrets":["GHMANAGER_GITHUBAPP_MICROSOFT_AGENT_FRAMEWORK_FOR_GO_PRIVATE_KEY_PEM","GH_AW_CI_TRIGGER_TOKEN","GH_AW_DEFAULT_OTLP_ENDPOINT","GH_AW_DEFAULT_OTLP_HEADERS","GH_AW_GITHUB_MCP_SERVER_TOKEN","GH_AW_GITHUB_TOKEN","GITHUB_TOKEN"],"actions":[{"repo":"actions/checkout","sha":"3d3c42e5aac5ba805825da76410c181273ba90b1","version":"v7.0.1"},{"repo":"actions/create-github-app-token","sha":"bcd2ba49218906704ab6c1aa796996da409d3eb1","version":"v3.2.0"},{"repo":"actions/download-artifact","sha":"3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c","version":"v8.0.1"},{"repo":"actions/github-script","sha":"3a2844b7e9c422d3c10d287c895573f7108da1b3","version":"v9.0.0"},{"repo":"actions/setup-node","sha":"820762786026740c76f36085b0efc47a31fe5020","version":"v7.0.0"},{"repo":"actions/upload-artifact","sha":"043fb46d1a93c77aae656e7c1c64a875d1fc6a0a","version":"v7.0.1"},{"repo":"github/gh-aw-actions/setup","sha":"924af5fdc64061cfbf66fb584c8b07e2ac230c60","version":"v0.89.21"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.28.25","digest":"sha256:25fbbefb92b690d4e6ef34de8df057c0349e6adf2b7486c0cc56954a8e9a3cd4","pinned_image":"ghcr.io/github/gh-aw-firewall/agent:0.28.25@sha256:25fbbefb92b690d4e6ef34de8df057c0349e6adf2b7486c0cc56954a8e9a3cd4"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.28.25","digest":"sha256:c3c7082e73ba83052c580097e5a9c9c40a11e6f6b6fa0a4710e6859f83a62854","pinned_image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.28.25@sha256:c3c7082e73ba83052c580097e5a9c9c40a11e6f6b6fa0a4710e6859f83a62854"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.28.25","digest":"sha256:94cac14372280dbf4a08d021d7b5810a9fd7d73c88802e04a9e05551b1cbcd09","pinned_image":"ghcr.io/github/gh-aw-firewall/squid:0.28.25@sha256:94cac14372280dbf4a08d021d7b5810a9fd7d73c88802e04a9e05551b1cbcd09"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.4.25","digest":"sha256:9be0a86220e807a0ecc89e53d7453468f7a53fbc6b3d1efd2299025ffe01d086","pinned_image":"ghcr.io/github/gh-aw-mcpg:v0.4.25@sha256:9be0a86220e807a0ecc89e53d7453468f7a53fbc6b3d1efd2299025ffe01d086"},{"image":"ghcr.io/github/gh-aw-node","digest":"sha256:a8082161d7dceda14b68f32eb39d0eaa96b825d07f5895b096afab9d9e0c7748","pinned_image":"ghcr.io/github/gh-aw-node@sha256:a8082161d7dceda14b68f32eb39d0eaa96b825d07f5895b096afab9d9e0c7748"},{"image":"ghcr.io/github/github-mcp-server:v1.12.2","digest":"sha256:508a0857ec762b1ab1cece29193345b501fab1dd9d1228a7b617062954cecac6","pinned_image":"ghcr.io/github/github-mcp-server:v1.12.2@sha256:508a0857ec762b1ab1cece29193345b501fab1dd9d1228a7b617062954cecac6"}],"mcp_servers":[{"name":"github","tools":["get_commit","get_file_contents","get_latest_release","get_me","get_pull_request","get_pull_request_comments","get_pull_request_diff","get_pull_request_files","get_pull_request_review_comments","get_pull_request_reviews","get_pull_request_status","get_release_by_tag","get_tag","issue_read","list_branches","list_commits","list_issue_types","list_issues","list_pull_requests","list_releases","list_starred_repositories","list_tags","pull_request_read","search_code","search_issues","search_pull_requests","search_repositories"]},{"name":"safeoutputs","tools":["create_pull_request","missing_data","missing_tool","noop"]}]} # This file was automatically generated by gh-aw (v0.89.21). DO NOT EDIT. To debug this workflow, load the skill at https://github.com/github/gh-aw/blob/main/debug.md # @@ -489,6 +489,8 @@ jobs: run: "set -euo pipefail\ngo -C _catalog mod download\ngit diff --exit-code -- _catalog/go.mod _catalog/go.sum\n" shell: bash working-directory: ${{ github.workspace }} + - name: Prepare terminal result validation + run: cp .github/scripts/check-agent-result.cjs "$RUNNER_TEMP/gh-aw/check-agent-result.cjs" - name: Configure Git credentials env: @@ -1144,6 +1146,12 @@ jobs: if [ ! -f /tmp/gh-aw/agent_output.json ]; then echo '{"items":[]}' > /tmp/gh-aw/agent_output.json fi + - env: + GH_AW_SAFE_OUTPUTS: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS }} + if: success() + name: Require a terminal agent result + run: node "$RUNNER_TEMP/gh-aw/check-agent-result.cjs" "$GH_AW_SAFE_OUTPUTS" + # Small dedicated copy of the agent output so safe-output processing # survives a failed or timed-out upload of the larger agent artifact - name: Upload agent output fallback artifact diff --git a/.github/workflows/dotnet-port-fixes-nightly.md b/.github/workflows/dotnet-port-fixes-nightly.md index 656ad8f7..215622e3 100644 --- a/.github/workflows/dotnet-port-fixes-nightly.md +++ b/.github/workflows/dotnet-port-fixes-nightly.md @@ -41,6 +41,14 @@ steps: set -euo pipefail go -C _catalog mod download git diff --exit-code -- _catalog/go.mod _catalog/go.sum + - name: Prepare terminal result validation + run: cp .github/scripts/check-agent-result.cjs "$RUNNER_TEMP/gh-aw/check-agent-result.cjs" +post-steps: + - name: Require a terminal agent result + if: success() + env: + GH_AW_SAFE_OUTPUTS: ${{ steps.set-runtime-paths.outputs.GH_AW_SAFE_OUTPUTS }} + run: node "$RUNNER_TEMP/gh-aw/check-agent-result.cjs" "$GH_AW_SAFE_OUTPUTS" permissions: contents: read pull-requests: read @@ -132,8 +140,12 @@ For `search_issues` and `search_pull_requests`, always include `repo:microsoft/a Searches discover references; direct reads resolve them. Track references by repository, resource type (`issue` or `pr`), and number, with their candidate/query and any filtering or failure. Read relevant bodies, comments, diffs, and outcomes directly through the matching tool; never include bodies in search results or re-search to rediscover a known reference. Reuse resolved evidence across candidate checks. Skip only work that implements, actively claims, or explicitly rejects the exact missing behavior, including older fallback tracking issues. A matching title, base feature port, shared package, internal refactor, or preservation test is not enough; a closed PR is not necessarily merged. +Current Go code is the source of truth for implemented coverage. Identify the actual declarations, callers, and tests that satisfy the selected contract; a proposed patch or a PR body's description is not checkout evidence. When citing a PR as implemented work, use `pull_request_read` with `method: get` and record its `state`, `merged`, and `merged_at`. Require `merged: true` and a non-null merge timestamp, then verify the behavior still exists in the current checkout. Search/list results and `state: closed` alone do not establish a merge. A closed, unmerged PR may document a maintainer's explicit rejection, but record that as a rejected proposal, never as implemented coverage; without such a decision, reassess the candidate against current code. + A filtered search item is not an explicit denial of a direct read. For each potentially relevant filtered reference, attempt one matching `issue_read` or `pull_request_read` through the same guarded GitHub MCP, unless that direct read was already denied. If allowed, assess its actual relevance; if denied or required evidence remains unavailable, keep that candidate blocked during selection and final recheck. Do not retry an explicitly denied read, disable TLS verification, weaken integrity/firewall policy, change transports, call a trapped/unavailable guard, or use shell `gh`/`curl`/Python downloads. Narrower empty searches, an accessible partial diff, passing tests, or disclosing the filter do not clear unresolved evidence. +Filtering that hides issue/PR identifiers is also unresolved evidence, not an empty result. Record the query and filtered count without inventing references or searching around the policy. If required duplicate evidence still blocks an otherwise eligible candidate and no fully verified candidate can be selected within the budget, call `report_incomplete`; do not stop with a prose-only explanation or substitute `noop`. Source-backed exclusions remain valid, and a blocked alternative does not invalidate a fully verified selection. + If local source independently excludes a candidate under Scope or the counterpart check, record that exclusion separately from any visibility limits already encountered; those limits do not make the excluded candidate eligible or require further duplicate reads. This does not clear duplicate evidence for a candidate that remains eligible. ## Review and selection @@ -159,6 +171,8 @@ Before editing, confirm that the selected group is fix-only and all required sou Call one terminal safe-output tool and inspect its result before final prose; a prose summary is not a tool call: +Post-execution validation requires exactly one `create_pull_request`, `noop`, or `report_incomplete` result. Exit code zero does not waive this requirement. For every claim of already-implemented coverage, include current Go file/symbol evidence and the verified merge status of any PR cited as implementation evidence. + - `create_pull_request`: one verified, tested fix/test change with a clear final duplicate check. Send only `title`, `body`, and `branch`; omit `temporary_id` and optional arguments. Draft status, base branch, and title prefix are configured. Use **Summary**, **Ported .NET PRs**, **Breaking Changes**, **Tests and Examples**, and **Notes**. State `No` for breaking changes and explain the corrected behavior, not a new contract. Include pinned SHA, actual inspected range, complete-change classification, immutable source/test links, duplicates/exclusions, and actual validation. For a source-verified fallback with no particular .NET PR, write `None` and explain it. Keep paragraphs concise without hard wrapping. Report publication as queued, never confirmed; do not push, merge, approve, or open PRs directly. - `noop`: completed bounded review with no eligible unclaimed fix/test and no potentially eligible candidate left blocked. Source-backed exclusions/API deferrals are completed reviews, not blockers. Report the pinned SHA, actual inspected range/groups, fallback area and outcome, and existing-work links. Do not claim a full parity audit. - `report_incomplete`: required source/duplicate evidence remains unresolved, validation fails, or execution reaches a runtime limit. Send only `reason` and `details`, with the operation/error, recovery attempts, SHA, blocked candidates, and remaining work. Leave partial edits unpublished. Do not substitute `noop` or report a working-but-denied tool as missing. The three-group selection limit alone is not a failure. \ No newline at end of file diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 55bd3035..78da9b25 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -45,6 +45,25 @@ jobs: - name: Run catalog tooling tests run: go -C _catalog test -v ${{ matrix.target.name != 'windows-arm64' && '-race' || '' }} -shuffle=on ./... + workflow-tests: + name: Workflow safeguards + runs-on: ubuntu-latest + permissions: + contents: read + steps: + - name: Checkout code + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 + with: + persist-credentials: false + + - name: Set up Node.js + uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0 + with: + node-version: 22 + + - name: Test workflow safeguards + run: node --test .github/scripts/check-agent-result.test.cjs + copilot-integration: name: GitHub Copilot integration if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository @@ -80,7 +99,7 @@ jobs: run: go test -count=1 -v ./provider/copilotprovider -run '^TestE2E_' conclusion: - needs: [test, copilot-integration] + needs: [test, workflow-tests, copilot-integration] runs-on: ubuntu-latest if: ${{ always() }} steps: