diff --git a/.github/workflows/aw-failure-investigator.lock.yml b/.github/workflows/aw-failure-investigator.lock.yml index 88615e8de18..d86f2b4d3d3 100644 --- a/.github/workflows/aw-failure-investigator.lock.yml +++ b/.github/workflows/aw-failure-investigator.lock.yml @@ -763,6 +763,14 @@ jobs: "type": "string", "maxLength": 256 }, + "state_reason": { + "type": "string", + "enum": [ + "completed", + "not_planned", + "duplicate" + ] + }, "suggest": { "type": "boolean" } diff --git a/.github/workflows/daily-squid-image-scan.lock.yml b/.github/workflows/daily-squid-image-scan.lock.yml index a6745970ce0..b3c9d95872b 100644 --- a/.github/workflows/daily-squid-image-scan.lock.yml +++ b/.github/workflows/daily-squid-image-scan.lock.yml @@ -681,6 +681,14 @@ jobs: "type": "string", "maxLength": 256 }, + "state_reason": { + "type": "string", + "enum": [ + "completed", + "not_planned", + "duplicate" + ] + }, "suggest": { "type": "boolean" } diff --git a/.github/workflows/dependabot-go-checker.lock.yml b/.github/workflows/dependabot-go-checker.lock.yml index 22f1cb6daf8..4ada4affb52 100644 --- a/.github/workflows/dependabot-go-checker.lock.yml +++ b/.github/workflows/dependabot-go-checker.lock.yml @@ -702,6 +702,14 @@ jobs: "type": "string", "maxLength": 256 }, + "state_reason": { + "type": "string", + "enum": [ + "completed", + "not_planned", + "duplicate" + ] + }, "suggest": { "type": "boolean" } diff --git a/.github/workflows/engine-conformance-agy.lock.yml b/.github/workflows/engine-conformance-agy.lock.yml index f78e912f753..c6c3dfc6ff4 100644 --- a/.github/workflows/engine-conformance-agy.lock.yml +++ b/.github/workflows/engine-conformance-agy.lock.yml @@ -1,4 +1,4 @@ -# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"660aeba6a2c3db50dbeeb09df77d4e69d06f1c514201efa0a41385a06b57688f","body_hash":"f2584351afda1dfb431018da1d9231c04af8b4049e421f24c54d7878fb93e782","strict":true,"agent_id":"agy","agent_model":"gemini-3.8-flash-medium","engine_versions":{"agy":"1.3.1"}} +# gh-aw-metadata: {"schema_version":"v4","frontmatter_hash":"897ac885ad93306bb8425cfdc99e936a507dd6636cc137babd06ce380357d284","body_hash":"f2584351afda1dfb431018da1d9231c04af8b4049e421f24c54d7878fb93e782","strict":true,"agent_id":"agy","agent_model":"gemini-3.8-flash-medium","engine_versions":{"agy":"1.3.1"}} # gh-aw-manifest: {"version":1,"secrets":["COPILOT_GITHUB_TOKEN","GEMINI_API_KEY","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/download-artifact","sha":"9000827ccba6bdab643e8b6fd33ac0654aef8333","version":"v8.0.2"},{"repo":"actions/github-script","sha":"3a2844b7e9c422d3c10d287c895573f7108da1b3","version":"v9.0.0"},{"repo":"actions/setup-node","sha":"949feb2413d6458794dcd2491c4babbbce0c15c1","version":"v7.1.0"},{"repo":"actions/upload-artifact","sha":"cf430e030ddbb5b0abf93d22962f4752f3646cd9","version":"v7.0.2"}],"containers":[{"image":"ghcr.io/github/gh-aw-firewall/agent:0.28.50","digest":"sha256:91fe4af8373c723d4d2e0706a0c35d310c2eeadaea5e0738c725b3861042f620","pinned_image":"ghcr.io/github/gh-aw-firewall/agent:0.28.50@sha256:91fe4af8373c723d4d2e0706a0c35d310c2eeadaea5e0738c725b3861042f620"},{"image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.28.50","digest":"sha256:11f12bcd3dde377ba822df7c670d6b937e9e0cebc46218946d4edc972d7fd965","pinned_image":"ghcr.io/github/gh-aw-firewall/api-proxy:0.28.50@sha256:11f12bcd3dde377ba822df7c670d6b937e9e0cebc46218946d4edc972d7fd965"},{"image":"ghcr.io/github/gh-aw-firewall/squid:0.28.50","digest":"sha256:cc9b6b979edf4aafbb3811c2bdc4891039d5346473afa88e304aa692167004f9","pinned_image":"ghcr.io/github/gh-aw-firewall/squid:0.28.50@sha256:cc9b6b979edf4aafbb3811c2bdc4891039d5346473afa88e304aa692167004f9"},{"image":"ghcr.io/github/gh-aw-mcpg:v0.4.30","digest":"sha256:ab5a436a1490438db473e4e3d4c973cb1d75e3cb233fb08b73d31b42d7d18fba","pinned_image":"ghcr.io/github/gh-aw-mcpg:v0.4.30@sha256:ab5a436a1490438db473e4e3d4c973cb1d75e3cb233fb08b73d31b42d7d18fba"},{"image":"ghcr.io/github/gh-aw-node","digest":"sha256:11c2c544876cdbaffefe0f7f1283b74838c209654b4934a78e23ed11f750490f","pinned_image":"ghcr.io/github/gh-aw-node@sha256:11c2c544876cdbaffefe0f7f1283b74838c209654b4934a78e23ed11f750490f"}],"mcp_servers":[{"name":"agy-native","tools":["native_challenge"]},{"name":"mcpscripts","tools":["conformance-challenge","native-challenge","native-infrastructure-challenge"]},{"name":"safeoutputs","tools":["create_issue","missing_data","missing_tool","noop","report_incomplete"]},{"name":"work-queue","tools":["work_queue_claim_finish","work_queue_dispatch_next","work_queue_explain","work_queue_read","work_queue_submit"]}],"threat_detection":{"mode":"disabled"}} # This file was automatically generated by gh-aw. DO NOT EDIT. To debug this workflow, load the skill at https://github.com/github/gh-aw/blob/main/debug.md # @@ -297,7 +297,7 @@ jobs: uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: GH_AW_PROMPT: ${{ runner.temp }}/gh-aw/aw-prompts/prompt.txt - GH_AW_MCP_CLI_SERVERS_LIST: "- `agy-native` — run `agy-native --help` to see available tools\n- `mcpscripts` — run `mcpscripts --help` to see available tools\n- `safeoutputs` — run `safeoutputs --help` to see available tools\n- `work-queue` — run `work-queue --help` to see available tools" + GH_AW_MCP_CLI_SERVERS_LIST: "- `mcpscripts` — run `mcpscripts --help` to see available tools\n- `safeoutputs` — run `safeoutputs --help` to see available tools" with: script: | const path = require('path'); @@ -928,7 +928,6 @@ jobs: export GH_AW_ENGINE="agy" export GH_AW_MCP_CONFIG_ADAPTER="convert_gateway_config_agy.cjs" - export GH_AW_MCP_CLI_SERVERS='["agy-native","mcpscripts","safeoutputs","work-queue"]' MCP_GATEWAY_UID=$(id -u 2>/dev/null || echo '0') MCP_GATEWAY_GID=$(id -g 2>/dev/null || echo '0') source "${RUNNER_TEMP}/gh-aw/actions/resolve_docker_socket_gid.sh" diff --git a/.github/workflows/engine-conformance-agy.md b/.github/workflows/engine-conformance-agy.md index 22061299966..5a4de4070a9 100644 --- a/.github/workflows/engine-conformance-agy.md +++ b/.github/workflows/engine-conformance-agy.md @@ -21,6 +21,7 @@ engine: env: ENGINE_CONFORMANCE_SENTINEL: conformance-agy tools: + cli-proxy: false work-queue: worker: true require-assignment: true diff --git a/.github/workflows/eslint-monster.lock.yml b/.github/workflows/eslint-monster.lock.yml index 27f9a4d89e6..ac775c04c95 100644 --- a/.github/workflows/eslint-monster.lock.yml +++ b/.github/workflows/eslint-monster.lock.yml @@ -730,6 +730,14 @@ jobs: "type": "string", "maxLength": 256 }, + "state_reason": { + "type": "string", + "enum": [ + "completed", + "not_planned", + "duplicate" + ] + }, "suggest": { "type": "boolean" } diff --git a/.github/workflows/lint-monster.lock.yml b/.github/workflows/lint-monster.lock.yml index 53cc8525aae..8935620f1c9 100644 --- a/.github/workflows/lint-monster.lock.yml +++ b/.github/workflows/lint-monster.lock.yml @@ -692,6 +692,14 @@ jobs: "type": "string", "maxLength": 256 }, + "state_reason": { + "type": "string", + "enum": [ + "completed", + "not_planned", + "duplicate" + ] + }, "suggest": { "type": "boolean" } diff --git a/.github/workflows/objective-impact-report.lock.yml b/.github/workflows/objective-impact-report.lock.yml index cf42fbc1968..2e3cdcc9402 100644 --- a/.github/workflows/objective-impact-report.lock.yml +++ b/.github/workflows/objective-impact-report.lock.yml @@ -662,6 +662,14 @@ jobs: "type": "string", "maxLength": 256 }, + "state_reason": { + "type": "string", + "enum": [ + "completed", + "not_planned", + "duplicate" + ] + }, "suggest": { "type": "boolean" } diff --git a/.github/workflows/pr-sous-chef.lock.yml b/.github/workflows/pr-sous-chef.lock.yml index 18a66652c12..44a6d18ee41 100644 --- a/.github/workflows/pr-sous-chef.lock.yml +++ b/.github/workflows/pr-sous-chef.lock.yml @@ -1087,6 +1087,14 @@ jobs: "type": "string", "maxLength": 256 }, + "state_reason": { + "type": "string", + "enum": [ + "completed", + "not_planned", + "duplicate" + ] + }, "suggest": { "type": "boolean" } diff --git a/.github/workflows/semantic-function-refactor.lock.yml b/.github/workflows/semantic-function-refactor.lock.yml index ee53415b399..c3f1287bad1 100644 --- a/.github/workflows/semantic-function-refactor.lock.yml +++ b/.github/workflows/semantic-function-refactor.lock.yml @@ -667,6 +667,14 @@ jobs: "type": "string", "maxLength": 256 }, + "state_reason": { + "type": "string", + "enum": [ + "completed", + "not_planned", + "duplicate" + ] + }, "suggest": { "type": "boolean" } diff --git a/actions/setup/js/safe_output_type_validator.test.cjs b/actions/setup/js/safe_output_type_validator.test.cjs index 7d228805668..d16201ed21c 100644 --- a/actions/setup/js/safe_output_type_validator.test.cjs +++ b/actions/setup/js/safe_output_type_validator.test.cjs @@ -141,6 +141,8 @@ const SAMPLE_VALIDATION_CONFIG = { defaultMax: 1, fields: { issue_number: { optionalPositiveInteger: true }, + duplicate_of: { issueOrPRNumber: true }, + state_reason: { type: "string", enum: ["completed", "not_planned", "duplicate"] }, }, }, dismiss_pull_request_review: { @@ -313,6 +315,15 @@ describe("safe_output_type_validator", () => { }); describe("validateItem", () => { + it("preserves an agent-selected duplicate state reason in API casing", async () => { + const { validateItem } = await import("./safe_output_type_validator.cjs"); + + const result = validateItem({ type: "close_issue", issue_number: 2, duplicate_of: 1, state_reason: "DUPLICATE" }, "close_issue", 1); + + expect(result.isValid).toBe(true); + expect(result.normalizedItem).toEqual({ type: "close_issue", issue_number: 2, duplicate_of: 1, state_reason: "duplicate" }); + }); + it("preserves explicit JSON null for built-in ledger values", async () => { const { validateItem, resetValidationConfigCache } = await import("./safe_output_type_validator.cjs"); process.env.GH_AW_VALIDATION_CONFIG = JSON.stringify({ @@ -1601,7 +1612,6 @@ describe("safe_output_type_validator", () => { { itemType: "upload_asset", item: { type: "upload_asset", path: "image.png", targetFileName: "../../.git/config" }, fieldName: "targetFileName" }, { itemType: "create_issue", item: { type: "create_issue", title: "Test", body: "Detailed issue body text.", assignees: ["octocat"] }, fieldName: "assignees" }, { itemType: "create_discussion", item: { type: "create_discussion", title: "Test", body: "This discussion body is intentionally long enough for validation.", labels: ["security"] }, fieldName: "labels" }, - { itemType: "close_issue", item: { type: "close_issue", issue_number: 1, state_reason: "not_planned" }, fieldName: "state_reason" }, { itemType: "push_to_pull_request_branch", item: { type: "push_to_pull_request_branch", message: "Apply changes", diff_size: 0 }, fieldName: "diff_size" }, { itemType: "create_pull_request", item: { type: "create_pull_request", title: "Fix bug", body: "Fixes the thing", branch: "fix/bug", base_commit: "abc123deadbeef" }, fieldName: "base_commit" }, ])("should strip undeclared $itemType.$fieldName", async ({ itemType, item, fieldName }) => { diff --git a/pkg/workflow/agy_engine_test.go b/pkg/workflow/agy_engine_test.go index 71ce07c5199..503ff6a73f4 100644 --- a/pkg/workflow/agy_engine_test.go +++ b/pkg/workflow/agy_engine_test.go @@ -345,7 +345,7 @@ func TestAgyProductionConformancePermissionsAreBounded(t *testing.T) { assert.Contains(t, string(lock), `GH_AW_SAFE_OUTPUTS_STAGED: "true"`) assert.Contains(t, string(lock), `"threat_detection":{"mode":"disabled"}`) assert.NotContains(t, callee.Jobs, "detection") - assertAgyConformanceProbes(t, callee, entry.id == "engine-conformance-agy") + assertAgyConformanceProbes(t, callee, false) }) } } @@ -356,6 +356,7 @@ func TestAgyConformanceEntryPointsShareConfiguration(t *testing.T) { assert.Equal(t, map[string]any{"workflow_dispatch": nil}, canonical["on"]) assert.Equal(t, []any{"shared/agy-conformance.md", "shared/engine-conformance-worker.md"}, canonical["imports"]) assert.Equal(t, map[string]any{ + "cli-proxy": false, "work-queue": map[string]any{"worker": true, "require-assignment": true}, }, canonical["tools"]) canonical["imports"] = []any{"shared/agy-conformance.md"} @@ -425,7 +426,7 @@ func TestAgySmokeLabelCommandIsCentrallyRouted(t *testing.T) { assert.NotContains(t, string(lock), "remove_trigger_label") } -func assertAgyConformanceProbes(t *testing.T, compiled agyConformanceWorkflow, workQueueWorker bool) { +func assertAgyConformanceProbes(t *testing.T, compiled agyConformanceWorkflow, workQueueCLIServer bool) { t.Helper() steps, environment, commands := agyConformanceStepContent(compiled) execution := steps["Execute experimental Agy CLI"] @@ -455,7 +456,7 @@ func assertAgyConformanceProbes(t *testing.T, compiled agyConformanceWorkflow, w for _, expected := range []string{`"agy-native"`, `"native-challenge"`, "--exclude-env GEMINI_API_KEY"} { assert.Contains(t, commands, expected, "compiled commands must retain %s", expected) } - if workQueueWorker { + if workQueueCLIServer { assert.Contains(t, commands, `export GH_AW_MCP_CLI_SERVERS='["agy-native","mcpscripts","safeoutputs","work-queue"]'`) } else { assert.NotContains(t, commands, "export GH_AW_MCP_CLI_SERVERS=") diff --git a/pkg/workflow/safe_output_validation_config_test.go b/pkg/workflow/safe_output_validation_config_test.go index 1d85a931257..cb4a795afd1 100644 --- a/pkg/workflow/safe_output_validation_config_test.go +++ b/pkg/workflow/safe_output_validation_config_test.go @@ -643,6 +643,33 @@ func TestValidationConfigCoversToolInputSchemas(t *testing.T) { } } +func TestValidationConfigCoversInjectedToolProperties(t *testing.T) { + configs := map[string]*SafeOutputsConfig{ + "omitted state-reason": { + CloseIssues: &CloseIssuesConfig{}, + }, + "list state-reason and allowed-events": { + CloseIssues: &CloseIssuesConfig{AllowedStateReason: []string{"completed", "duplicate"}}, + SubmitPullRequestReview: &SubmitPullRequestReviewConfig{ + AllowedEvents: []string{"APPROVE", "COMMENT"}, + }, + }, + } + + for name, safeOutputs := range configs { + t.Run(name, func(t *testing.T) { + injections := computePropertyInjections(safeOutputs) + for toolName, properties := range injections { + for propertyName := range properties { + if _, ok := ValidationConfig[toolName].Fields[propertyName]; !ok { + t.Errorf("%s injected property %q is missing from ValidationConfig", toolName, propertyName) + } + } + } + }) + } +} + func TestCreateDiscussionBodyMinLength(t *testing.T) { config, ok := ValidationConfig["create_discussion"] if !ok { diff --git a/pkg/workflow/safe_outputs_validation_config.go b/pkg/workflow/safe_outputs_validation_config.go index 43b014648c7..f9fd6ff2c3e 100644 --- a/pkg/workflow/safe_outputs_validation_config.go +++ b/pkg/workflow/safe_outputs_validation_config.go @@ -433,6 +433,7 @@ var ValidationConfig = map[string]TypeValidationConfig{ "body": {Type: "string", Sanitize: true, MaxLength: MaxBodyLength}, "issue_number": {OptionalPositiveInteger: true}, "duplicate_of": {IssueOrPRNumber: true}, + "state_reason": {Type: "string", Enum: closeIssueStateReasonValues}, "rationale": {Type: "string", Sanitize: true, MaxLength: 280, StripOnError: true}, "confidence": {Type: "string", Enum: []string{"LOW", "MEDIUM", "HIGH"}, StripOnError: true}, "suggest": {Type: "boolean"},