#!/usr/bin/env bash # lib/attention.sh — the `attention` target invariants (#232, epic #229). # # Both reconcilers source this file. Pure decisions sit above the divider; # the impure orchestrator below reads the current label episode and comments # through the sourcing script's run()/log(). The machine diagnoses only: it # never sets, clears, retargets or assigns anything on the strength of these # checks (#229 D2). ATTENTION_MARKER_PREFIX='\n' "$ATTENTION_MARKER_PREFIX" "$1" } # --------------------------------------------------------------------------- # The impure orchestrator. Called only behind a has-attention gate. # Needs REPO; uses the caller's run() and log(). # --------------------------------------------------------------------------- reconcile_attention() { # $1 item, $2 pr|issue, $3 assignees, $4 suppression local n="$1" surface="$2" assignees="$3" suppression="${4:-}" local target comment labeled_events labeled_at marker comments body timeline : "${REPO:?reconcile_attention: REPO is required}" target="$(attention_target_decision "$surface" "$assignees")" comment="$(attention_comment_decision "$target" "$suppression")" [ "$comment" != KEEP ] || return 0 if [ "$comment" = SUPPRESS ]; then log "#$n: malformed attention detected; comment suppressed by $suppression precedence" return 0 fi # forge_timeline projects both forges into the GitHub event shape # (.event / .label.name) — Forgejo's raw timeline carries neither. Capture # its status BEFORE jq: a pipeline's status is the last command's, so # `forge_timeline | jq` would collapse an unreadable timeline into an empty # one, and those are the two states this function exists to tell apart # (#188, and lib/ruling.sh does the identical dance). if ! timeline="$(forge_timeline "$n" 2>/dev/null)"; then log "#$n: attention timeline unreadable — no verdict invented this pass" return 0 fi labeled_events="$(jq -r '.[] | select(.event == "labeled" and .label.name == "attention") | .created_at' <<<"$timeline")" if [ -z "$labeled_events" ]; then log "#$n: attention flag has no visible labeled event — no verdict invented this pass" return 0 fi labeled_at="$(attention_newest_flag <<<"$labeled_events")" marker="$(attention_episode_marker "$labeled_at")" if ! comments="$(forge_api --paginate "repos/$REPO/issues/$n/comments" \ --jq '.[].body // ""' 2>/dev/null)"; then log "#$n: attention comments unreadable — no verdict invented this pass" return 0 fi grep -qF "$marker" <<<"$comments" && return 0 case "$target" in MALFORMED_PR) body="$marker This pull request carries \`attention\`, but that label is issue-only. Put the label on the assigned issue that owns the claim. The sweep cannot infer which issue that is, so it reports the malformed target without removing or retargeting the label." ;; MALFORMED_UNASSIGNED) body="$marker This issue carries \`attention\` but has no assignee to receive the demand. Assign the intended builder or remove the flag. The sweep reports the board bug without assigning anyone or changing the label." ;; esac run forge_issue_comment "$n" "$body" >/dev/null log "#$n: malformed attention ($surface) — commented; no label or assignee changed" }