fix: append block findings to the issue body so a re-queued ticket can see them #436
No reviewers
Labels
No labels
Agent
Compat/Breaking
Kind/Bug
Kind/Documentation
Kind/Enhancement
Kind/Feature
Kind/Security
Kind/Testing
Priority
Critical
Priority
High
Priority
Low
Priority
Medium
Reviewed
Confirmed
Reviewed
Duplicate
Reviewed
Invalid
Reviewed
Won't Fix
Status
Abandoned
Status
Blocked
Status
Need More Info
No milestone
No assignees
2 participants
Notifications
Total time spent: 8 minutes 30 seconds
Due date
igor
8 minutes 30 seconds
No due date set.
Dependencies
No dependencies set
Reference
joshtronic/igor!436
Loading…
Reference in a new issue
No description provided.
Delete branch "agent/434-security-block-findings-are-written-to-a-comment"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
What this PR does
forgejo_append_issue_body(lib/forgejo.sh): fetches the issue's current body, appends the reason under a## Blocked (<date>)heading, and PATCHes it back -- best-effort, returns 1 without touching anything on a fetch/PATCH failure.bin/agent-block.shnow calls it before commenting, and the comment gets a trailing note that matches the real mechanism ("Appended to the issue description above...") instead of the old comment-only instruction that never actually reached the next run's prompt.agent-block.shcall site (security gate, scope cap, vacuous tests, off-limits files, repeated noop, etc.), not just the security-review one in the original report, since they all funnel through the same helper.Test plan
bin/test-agent-block.sh-- runs the realagent-block.shas a subprocess (onlycurlstubbed at the transport layer;lib/forgejo.sh's real_fjexecutes) and replaysbin/tick.sh's exactISSUE_BODY/USER_MSGconstruction to prove the blocked findings land in what the next run's prompt would actually contain, not just "somewhere in Forgejo". Verified this test fails against the pre-fixagent-block.sh(reverted it locally, confirmed 5 checks fail for the right reason, restored the fix).forgejo_append_issue_bodyinbin/test-forgejo.sh(success path preserves + appends, fetch-failure path never PATCHes).make testpasses (allbin/test-*.sh, including the two new/extended ones).make lintpasses (shellcheck + mdl clean).Closes #434
🤖 Review —
REQUEST_CHANGES(automated)CI for
29502c99: successAdds
forgejo_append_issue_bodyand wires it intoagent-block.shso block reasons land in the issue body (which is whattick.shfeeds the next run) instead of only a comment. Scope is tight (~200 lines, one issue), the checklist matches the diff item-for-item, and CI is green. One correctness defect blocks: the "best-effort, never touches anything on failure" guarantee isn't actually enforced, and the failure mode destroys the issue description.Blocking
lib/forgejo.sh:160-165— a failed/error fetch can silently overwrite the issue body with only the block note.Two ways this guard doesn't hold:
$?for a pipeline is the last command's status (jq), notforgejo_get_issue's, unlesspipefailis active in the shell that sources the lib.bin/test-forgejo.shevidently haspipefailon (that's the only reason theGET) return 22stub produces the assertedrc 1/ "never PATCHes" — the stub emits no output, andjq -r '.body // empty'on empty stdin exits 0). The diff doesn't showagent-block.sh'ssetline, so I can't confirm the production caller has the same option set. The test therefore proves the guard under the test harness's options, not the caller's.pipefail, a Forgejo error body ({"message":"...","url":"..."}on 403/404/500) is well-formed JSON andcurlexits 0 unless_fjuses--fail(not visible here)..body // empty→ empty string, exit 0 → the function proceeds toPATCH {"body": "\n\n---\n## Blocked (…)\n\n<reason>\n"}, replacing the entire issue description with just the block note. That's precisely the data the fix exists to preserve, and it's the path most likely to fire in the real world (auth blip, rate limit, renumbered issue).Fixed looks like: validate the fetch result before writing — e.g. capture the raw response, require
jq -e '.number'(orhas("body")) to succeed, andreturn 1otherwise; don't rely on the pipeline's exit status. A regression test that stubs the GET as an HTTP-error JSON payload (not just a non-zero return with no output) and asserts no PATCH occurs would lock this in.Non-blocking
bin/test-agent-block.shonly exercises the happy path through the real script. The interesting branch — append fails, soNOTEstays empty, the warning goes to stderr, and the comment/label/unassign still run — is never executed end-to-end. Worth a second case where the stubbedcurlfails the GET.bin/test-forgejo.sh:390-393— the check labelled "PATCHes the issues endpoint (not labels/comments)" doesn't test the endpoint at all; the_fjstub drops$2, so nothing asserts the path is/repos/acme/x/issues/42. Capture$2and assert on it, or rename the check.bin/test-forgejo.shredefines_fjglobally and never restores it. Safe today because these blocks are last in the file, but it's a trap for whoever appends the next test.## Blocked (<date>)section. After a few rounds the issue body is mostly stale findings, and the date-only heading means same-day repeats produce duplicate headings. Consider replacing a prior## Blockedsection rather than always appending, or at least noting the growth as accepted.Independent review by the harness on
claude-opus-5(effort: high). The human reviewer is requested once Igor has reviewed; a human still merges.🔧 Rework — round 1 (automated)
Addressed the review on
claude-opus-5at effort high — 1 new commit(s).The "best-effort, never touches anything on failure" guarantee in forgejo_append_issue_body wasn't enforced, and the failure mode overwrote the whole issue description with just the block note -- exactly the data the helper exists to preserve. Two holes, both now closed: 1. `current=$(forgejo_get_issue ... | jq -r '.body // empty')` takes the PIPELINE's status, i.e. jq's, unless the caller has pipefail set -- and jq on empty stdin exits 0. The fetch is now captured on its own line, so no caller shell option can change what the guard sees. 2. Even with pipefail, a 2xx payload that isn't an issue is still well-formed JSON whose `.body // empty` is "" with exit 0. The payload must now look like an issue (`has("number")`, `has("body")`) before anything is written. Also from review: - test-forgejo.sh asserts the PATCH path is /repos/acme/x/issues/42 (the old check named the endpoint but only grepped the payload), and restores the real _fj after the stub block instead of leaving it redefined for whatever gets appended next. - test-agent-block.sh covers the other end-to-end branch: the append fails, so the note stays off the comment, the warning goes to stderr, and the label + unassign still land. - agent-block.sh's heading carries the time, so a ticket blocked twice in one day doesn't grow two identical `## Blocked (date)` headings. Growth itself stays accepted and is documented as such -- prior attempts are context a re-queued run wants. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>🤖 Review —
COMMENT(automated)CI for
760b5646: successAdds
forgejo_append_issue_bodytolib/forgejo.sh, wires it intobin/agent-block.shso block reasons land in the issue body (which is what the next tick's prompt is built from), plus new/extended tests. The core fix is small, correct-looking, and the description matches the diff. CI is green. Two things I can't verify from the diff and a few design notes keep me off APPROVE.Can't verify from the diff
bin/test-agent-block.shactually executed by CI? The Makefile isn't in the diff. Ifmake testglobsbin/test-*.shthe new file is picked up automatically and this is a non-issue; if it enumerates test scripts explicitly, the new 160-line test never runs and the green CI signal says nothing about it. Please confirm (or add the Makefile entry). This is the difference between "tested" and "test file exists".forgejo_append_issue_bodynow injects---+## Blocked (<ts>)sections into human-authored descriptions. If any other harness path greps the body for markers (task checklists,Closes #, scope hints), this could interact with it. Nothing in the diff touches that, but the diff also doesn't show me the consumers.Coverage caveat (the "acceptance test" doesn't test
tick.sh)Both
bin/test-agent-block.sh:118-127andbin/test-forgejo.sh(therebuilt tick.sh promptblock) re-implement tick.sh's extraction by hand:That is a copy of the logic, not an invocation of it. If
tick.shchanged to build the prompt from something other than.body, both of these checks would still pass while the regression they guard against silently returned. The structural greps intest-forgejo.sh(agent-block.sh calls forgejo_append_issue_body) are the more honest guard here. Consider greppingtick.shfor the.bodyextraction shape too, so the assumption this whole fix rests on is pinned. Also: the same acceptance assertion is duplicated across both new test blocks — one of them is redundant.Design notes (non-blocking, but worth a human eye)
lib/forgejo.shsays growth is "accepted" — fine as a decision, but it's a decision about prompt size and issue readability that the human should knowingly own. No cap, no dedupe of identical consecutive reasons.What I checked and found fine
forgejo_append_issue_body(separate assignment so callerpipefailcan't mask the fetch status, plushas("number") and has("body")shape check withjq -e) correctly avoids the destructive "PATCH the block note as the whole description" failure mode, and both branches are tested.agent-block.shonly emits the "Appended to the issue description above" note when the append actually succeeded, and falls through to comment + label + unassign on failure — the honest-message path is tested (append failure -> the comment does not claim the body was updated).Happy to re-review once the
make testwiring question is answered.Independent review by the harness on
claude-opus-5(effort: high). The human reviewer is requested once Igor has reviewed; a human still merges.