diff --git a/internal/scaffold/fullsend-repo/agents/triage.md b/internal/scaffold/fullsend-repo/agents/triage.md index 58cc303e01..8de2eb0702 100644 --- a/internal/scaffold/fullsend-repo/agents/triage.md +++ b/internal/scaffold/fullsend-repo/agents/triage.md @@ -52,10 +52,14 @@ Also look for **blocking relationships** — open issues or PRs that must be res - The issue describes a feature that depends on infrastructure or API changes tracked in another issue - The issue references an upstream library, service, or repository that has a known open bug - A PR is already in flight that would conflict with or must land before work on this issue -- An open PR already addresses this issue, even partially — the work is already in progress - The issue's fix requires a design decision that is being discussed in another issue -**Existing PR gate (HARD CONSTRAINT):** If an open PR already addresses this issue — even partially — treat it as a prerequisite. Use `action: "prerequisites"` with the PR URL in the `existing` array. Do not emit `action: "sufficient"` when an open PR covers the reported problem; dispatching a second implementation would create duplicates. Only skip this rule if the PR is closed without merging (the work was abandoned) or if the PR is clearly unrelated despite mentioning the issue number. +**Existing PR gate (HARD CONSTRAINT):** If an open PR already addresses this issue, do not emit `action: "sufficient"` — dispatching a second implementation would create duplicates. Instead, distinguish between two cases: + +1. **PR fixes this issue** — the PR directly resolves the reported problem. Use `action: "in-progress"` with the PR URL in the `pull_requests` array. This signals that work is already underway, not that the issue is blocked. +2. **PR is a true prerequisite** — the PR does not fix this issue but must land before work on this issue can start (e.g., infrastructure changes, API additions, dependency upgrades). Use `action: "prerequisites"` with the PR URL in the `existing` array. + +Only skip this rule if the PR is closed without merging (the work was abandoned) or if the PR is clearly unrelated despite mentioning the issue number. If the issue mentions other repositories, libraries, or upstream projects, search those too: @@ -238,6 +242,23 @@ At least one of the two arrays must have entries. } ``` +### Action: `in-progress` + +An open pull request already addresses this issue — the work is underway. Use this action when a PR directly fixes or resolves the reported problem. This is distinct from `prerequisites`, which is for PRs that must land *before* work on this issue can start. + +**HARD CONSTRAINT:** Never emit `sufficient` when an open PR already fixes this issue. Use `in-progress` instead — dispatching a second implementation would create duplicates. + +```json +{ + "action": "in-progress", + "reasoning": "Brief explanation of how the PR addresses this issue", + "pull_requests": [ + { "url": "https://github.com/org/repo/pull/123" } + ], + "comment": "A professional comment explaining that this issue is already being addressed by an existing PR. Link to the PR(s) and briefly describe how they resolve the reported problem. Use 'addressed by' or 'fixed by' framing — not 'blocked by'." +} +``` + ### Action: `sufficient` Information is sufficient for a developer to investigate and fix. diff --git a/internal/scaffold/fullsend-repo/schemas/triage-result.schema.json b/internal/scaffold/fullsend-repo/schemas/triage-result.schema.json index 73616cab7b..77cbde0b11 100644 --- a/internal/scaffold/fullsend-repo/schemas/triage-result.schema.json +++ b/internal/scaffold/fullsend-repo/schemas/triage-result.schema.json @@ -9,7 +9,7 @@ "properties": { "action": { "type": "string", - "enum": ["insufficient", "duplicate", "sufficient", "prerequisites", "question"] + "enum": ["insufficient", "duplicate", "sufficient", "prerequisites", "question", "in-progress"] }, "reasoning": { "type": "string", @@ -73,6 +73,21 @@ }, "additionalProperties": false }, + "pull_requests": { + "type": "array", + "minItems": 1, + "items": { + "type": "object", + "required": ["url"], + "properties": { + "url": { + "type": "string", + "pattern": "^https://github\\.com/[a-zA-Z0-9._-]+/[a-zA-Z0-9._-]+/pull/[0-9]+$" + } + }, + "additionalProperties": false + } + }, "label_actions": { "$ref": "#/$defs/label_actions" } @@ -103,6 +118,10 @@ } } } + }, + { + "if": { "properties": { "action": { "const": "in-progress" } }, "required": ["action"] }, + "then": { "required": ["pull_requests"] } } ], "$defs": { diff --git a/internal/scaffold/fullsend-repo/scripts/post-triage-test.sh b/internal/scaffold/fullsend-repo/scripts/post-triage-test.sh index fd4f4d8f45..5d0486120f 100755 --- a/internal/scaffold/fullsend-repo/scripts/post-triage-test.sh +++ b/internal/scaffold/fullsend-repo/scripts/post-triage-test.sh @@ -33,6 +33,12 @@ if [[ "\$1" == "issue" ]] && [[ "\$2" == "create" ]]; then echo "https://github.com/mock-org/mock-repo/issues/999" exit 0 fi +# Capture body content piped via --body-file - so tests can verify comment bodies. +if [[ "\$1" == "issue" ]] && [[ "\$2" == "comment" ]] && [[ "\$*" == *"--body-file -"* ]]; then + BODY=\$(cat) + echo "gh \$* <>" >> "${GH_LOG}" + exit 0 +fi echo "gh \$*" >> "${GH_LOG}" MOCKEOF chmod +x "${MOCK_BIN}/gh" @@ -445,6 +451,39 @@ run_test "ready-to-code-applied-without-label-actions" \ '{"action":"sufficient","reasoning":"all clear","clarity_scores":{"symptom":0.9,"cause":0.85,"reproduction":0.9,"impact":0.8,"overall":0.87},"triage_summary":{"title":"Fix crash","severity":"high","category":"bug","problem":"Crash","root_cause_hypothesis":"Buffer overflow","reproduction_steps":["step 1"],"environment":"Linux","impact":"All users","recommended_fix":"Fix buffer","proposed_test_case":"test_crash"},"comment":"## Triage Summary\n\nReady."}' \ "gh api repos/test-org/test-repo/issues/42/labels -f labels[]=ready-to-code --silent" +run_test "in-progress-posts-comment" \ + '{"action":"in-progress","reasoning":"PR fixes the reported bug","pull_requests":[{"url":"https://github.com/test-org/test-repo/pull/100"}],"comment":"This issue is already being addressed by an existing pull request."}' \ + "gh issue comment 42 --repo test-org/test-repo --body-file -" + +run_test "in-progress-applies-pr-open-label" \ + '{"action":"in-progress","reasoning":"PR fixes the reported bug","pull_requests":[{"url":"https://github.com/test-org/test-repo/pull/100"}],"comment":"This issue is already being addressed by an existing pull request."}' \ + "gh api repos/test-org/test-repo/issues/42/labels -f labels[]=pr-open --silent" + +run_test "in-progress-removes-blocked-label" \ + '{"action":"in-progress","reasoning":"PR fixes the reported bug","pull_requests":[{"url":"https://github.com/test-org/test-repo/pull/100"}],"comment":"This issue is already being addressed by an existing pull request."}' \ + "gh api repos/test-org/test-repo/issues/42/labels/blocked -X DELETE --silent" + +run_test "in-progress-removes-ready-to-code-label" \ + '{"action":"in-progress","reasoning":"PR fixes the reported bug","pull_requests":[{"url":"https://github.com/test-org/test-repo/pull/100"}],"comment":"This issue is already being addressed by an existing pull request."}' \ + "gh api repos/test-org/test-repo/issues/42/labels/ready-to-code -X DELETE --silent" + +run_test "in-progress-removes-needs-info-label" \ + '{"action":"in-progress","reasoning":"PR fixes the reported bug","pull_requests":[{"url":"https://github.com/test-org/test-repo/pull/100"}],"comment":"This issue is already being addressed by an existing pull request."}' \ + "gh api repos/test-org/test-repo/issues/42/labels/needs-info -X DELETE --silent" + +run_test "in-progress-missing-comment-fails" \ + '{"action":"in-progress","reasoning":"PR fixes the reported bug","pull_requests":[{"url":"https://github.com/test-org/test-repo/pull/100"}]}' \ + "" \ + "true" + +run_test "in-progress-appends-addressed-by" \ + '{"action":"in-progress","reasoning":"PR fixes the reported bug","pull_requests":[{"url":"https://github.com/test-org/test-repo/pull/100"}],"comment":"This issue is already being addressed."}' \ + "Addressed by:" + +run_test_stdout "in-progress-control-label-refused" \ + '{"action":"in-progress","reasoning":"PR fixes the reported bug","pull_requests":[{"url":"https://github.com/test-org/test-repo/pull/100"}],"comment":"This issue is addressed.","label_actions":{"reason":"Tried to set pr-open label.","actions":[{"action":"add","label":"pr-open"}]}}' \ + "::warning::Refused to add control label 'pr-open' -- control labels are managed by the triage pipeline" + # --- Summary --- echo "" diff --git a/internal/scaffold/fullsend-repo/scripts/post-triage.sh b/internal/scaffold/fullsend-repo/scripts/post-triage.sh index 94cedb01b2..ca2e5bd344 100755 --- a/internal/scaffold/fullsend-repo/scripts/post-triage.sh +++ b/internal/scaffold/fullsend-repo/scripts/post-triage.sh @@ -79,7 +79,7 @@ remove_label() { # add or remove these via label_actions. This list covers labels that the # pipeline itself applies (pre-triage.sh resets the first five; the action # handlers apply blocked/triaged/feature). -CONTROL_LABELS=("needs-info" "ready-to-code" "duplicate" "feature" "blocked" "triaged" "question") +CONTROL_LABELS=("needs-info" "ready-to-code" "duplicate" "feature" "blocked" "triaged" "question" "pr-open") is_control_label() { local label="$1" @@ -122,6 +122,38 @@ case "${ACTION}" in add_label "duplicate" ;; + in-progress) + if [[ -z "${COMMENT}" ]]; then + echo "ERROR: action is 'in-progress' but no comment provided" >&2 + exit 1 + fi + + # Collect PR URLs from the pull_requests array. + PR_COUNT=$(jq '.pull_requests // [] | length' "${RESULT_FILE}") + PR_URLS="" + for i in $(seq 0 $((PR_COUNT - 1))); do + URL=$(jq -r ".pull_requests[${i}].url" "${RESULT_FILE}") + PR_URLS="${PR_URLS} ${URL}" + done + PR_URLS=$(echo "${PR_URLS}" | xargs) # trim whitespace + + if [[ -n "${PR_URLS}" ]]; then + PR_LIST="" + for url in ${PR_URLS}; do + PR_LIST="${PR_LIST} +- ${url}" + done + COMMENT="${COMMENT} + +**Addressed by:**${PR_LIST}" + fi + + remove_label "ready-to-code" + remove_label "needs-info" + remove_label "blocked" + add_label "pr-open" + ;; + prerequisites) if [[ -z "${COMMENT}" ]]; then echo "ERROR: action is 'prerequisites' but no comment provided" >&2