Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ script-test:
$(call run-timed,bash scripts/pre-review-test.sh)
$(call run-timed,bash scripts/post-review-test.sh)
$(call run-timed,bash scripts/risk-tier1-test.sh)
$(call run-timed,bash scripts/pre-fix-test.sh)
$(call run-timed,bash scripts/post-fix-test.sh)
$(call run-timed,bash scripts/post-retro-test.sh)
$(call run-timed,bash scripts/pre-scribe-test.sh)
Expand Down
20 changes: 18 additions & 2 deletions agents/fix.md
Original file line number Diff line number Diff line change
Expand Up @@ -183,8 +183,24 @@ Bot-triggered runs (from the review agent) are capped at `ITERATION_CAP`
(default: 5). When the iteration count approaches this cap, the `needs-human`
label is added and the autonomous loop stops on the next attempt. A human can
then direct the agent with `/fs-fix` commands up to `ITERATION_CAP_HUMAN`
(default: 10) total iterations (bot + human combined). This ensures humans
are never locked out of the agent after a bot loop exhausts its budget.
(default: 10) total iterations (bot + human combined). This ensures humans are
never locked out of the agent after a bot loop exhausts its budget.

A maintainer can tighten the *autonomous* loop for a single PR with a
`fullsend-fix-budget/N` label (N a positive integer). If present, the smallest
valid label lowers the bot cap to N, so the review→fix loop escalates to a human
sooner on a risky or expensive PR. It applies to the bot cap only: the human
`/fs-fix` cap is never tightened, so the label can never lock a human out. The
label can only *tighten* the bot cap, never raise it: a value at or above the
bot cap has no effect, and malformed values (non-integer, zero, negative, or
absurdly large) are ignored so a bad label cannot silently block or widen the
loop. pre-fix enforces the tightened cap and post-fix reports against the same
effective cap (summary and the `needs-human` warning).

> **Not yet active.** The label is recognized and enforced, but the review→fix
> workflow does not yet forward PR labels to the fix agent (`PR_LABELS` is unset
> in the delivery path), so applying the label currently has no effect. See
> `docs/fix.md` and the wiring follow-up before relying on it.

## Validation retry behavior

Expand Down
9 changes: 9 additions & 0 deletions docs/fix.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ Remove the label or use `/fs-fix` to re-engage.
|-------|---------|
| `fullsend-no-fix` | Prevents automatic fix runs on this PR. Applied by `/fs-fix-stop`. Manual `/fs-fix` commands are unaffected. |
| `needs-human` | The fix agent is approaching its iteration cap and needs human direction. Applied automatically when an automatic fix iteration reaches the warning threshold. |
| `fullsend-fix-budget/N` | **Reserved — not yet active** (requires `PR_LABELS` wiring in the review→fix workflow; see [Iteration limits](#iteration-limits)). Once active: tightens the *autonomous* review→fix loop for this PR to `N` iterations (`N` a positive integer), so the bot escalates to a human sooner. Applied by a maintainer. Lowers the bot cap only (never the human `/fs-fix` cap) and can only tighten it, never raise it; malformed values are ignored. |

## Configuration

Expand Down Expand Up @@ -156,6 +157,14 @@ The fix agent enforces iteration caps to prevent infinite review-fix loops:
`needs-human` label.
- Each `/fs-fix` comment cancels any in-flight fix run for the same PR and
starts a new one.
- **Per-PR override (reserved, not yet active):** a `fullsend-fix-budget/N`
label is intended to lower the *automatic* cap for a single PR (bot only; the
manual `/fs-fix` cap is never tightened, so a human is never locked out). The
parser and enforcement ship in pre-fix/post-fix, but the review→fix workflow
does not yet forward PR labels to the fix agent (`PR_LABELS` is unset in the
delivery path), so the label currently has no effect. Wiring it requires the
workflow to pass the PR's labels through `PR_LABELS`; until then the caps
behave exactly as above.

## Multi-forge support

Expand Down
9 changes: 9 additions & 0 deletions harness/fix.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,15 @@ env:
TRIGGER_SOURCE: "${TRIGGER_SOURCE}"
HUMAN_INSTRUCTION: "${HUMAN_INSTRUCTION}"
FIX_ITERATION: "${FIX_ITERATION}"
# NOTE: a `fullsend-fix-budget/N` PR label can tighten the autonomous (bot)
# fix cap, and pre-fix/post-fix already consume PR_LABELS to enforce it
# (the human /fs-fix cap is never tightened). It is omitted
# here on purpose: the reusable-fix workflow does not yet forward the PR
# labels, and referencing an unset host var fails harness env validation
# (fail-closed) on every run. To activate, re-add
# PR_LABELS: "${PR_LABELS}"

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[MEDIUM] Activation recipe prescribes an env.runner entry that pre-/post-fix do not need, and that would fail closed on GitLab

This comment tells the follow-up author to activate the label by re-adding PR_LABELS: "${PR_LABELS}" to the shared env.runner block together with a reusable-fix.yml change. Verified against fullsend-ai/fullsend main (7495cdc5):

  1. The entry is redundant for enforcement. childScriptEnv() in internal/cli/run.go:3586-3587 builds the pre-/post-script environment from os.Environ() first and only then overlays RunnerEnv. A step-level env: PR_LABELS on the "Run fix agent" step (reusable-fix.yml:361-385) therefore already reaches pre-fix and post-fix — the only two consumers — with no env.runner entry at all.
  2. Following the recipe breaks GitLab. harness/fix.yaml is forge-shared. The GitLab agent template (internal/scaffold/fullsend-repo-gitlab/.gitlab/ci/fullsend-agent.yml) sets every other key in this shared block (TARGET_BRANCH, HUMAN_INSTRUCTION, REVIEW_BODY_FILE, PRE_AGENT_HEAD, PUSH_TOKEN, ...) but has zero references to PR_LABELS. Adding the entry as written reproduces, on every GitLab fix run, the exact ValidateRunnerEnvWith fail-closed abort (harness.go:734-737, env.runner[PR_LABELS]: host variable PR_LABELS is not set) that the earlier thread on this file caught for GitHub.
  3. The forge-scoped blocks already existforge.github.env.runner (~line 119) and forge.gitlab.env.runner (~line 136) are the correct home for any forge-specific entry, and the recipe does not mention them.

This refines the suggestion in the earlier CRITICAL thread (which proposed re-adding the env.runner line in the wiring PR): that step is unnecessary, and unsafe in the shared block.

Suggestion. Rewrite the comment to state that the workflow step env alone is sufficient for pre-/post-fix (childScriptEnv inherits the process environment), so the GitHub wiring is just PR_LABELS: ${{ join(github.event.pull_request.labels.*.name, ',') }} on the "Run fix agent" step. If an env.runner entry is ever wanted, say it must go in forge.github.env.runner, and in forge.gitlab.env.runner only after the GitLab agent template exports a set-possibly-empty PR_LABELS — never in the shared block.

# together with the reusable-fix.yml change that sets PR_LABELS (labels are
# available upstream as a comma-joined string).
REVIEW_BODY_FILE: "${REVIEW_BODY_FILE}"
PRE_AGENT_HEAD: "${PRE_AGENT_HEAD}"
PUSH_TOKEN: "${PUSH_TOKEN}"
Expand Down
52 changes: 52 additions & 0 deletions scripts/lib/fix-budget.lib.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
#!/usr/bin/env bash

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Action required

6. Protected scripts require human approval 📜 Skill insight § Compliance

The PR modifies multiple files under the protected scripts/ path, so it must receive human review
and must not be auto-approved. The feature rationale provides context, but there is no linked issue
authorizing these governance/infrastructure changes.
Agent Prompt
## Issue description
This PR changes protected `scripts/` infrastructure and cannot be auto-approved.

## Issue Context
Route the PR for human approval and link the authorizing issue for the protected-path changes before merge.

## Fix Focus Areas
- scripts/lib/fix-budget.lib.sh[1-42]
- scripts/pre-fix.src.sh[20-29]
- scripts/pre-fix.src.sh[114-119]
- scripts/pre-fix-test.sh[1-59]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

# shellcheck shell=bash
# fix-budget.lib.sh — parse a per-PR fix-loop budget from PR labels.
#
# A label of the form `fullsend-fix-budget/N` (N a positive integer) lets a
# maintainer cap the review->fix loop for a single PR below the global
# iteration cap. The label can only TIGHTEN the cap, never raise it:
# enforcement lives in pre-fix, which applies min(label_budget, cap).
#
# Bundled into pre-fix.sh via bundle-sh.sh.
#
# Expected env vars (optional):
# PR_LABELS — PR label names separated by commas and/or newlines. Absent/empty
# is fine: parse_fix_budget then returns nothing and the cap is
# unchanged. (The upstream dispatcher comma-joins labels; a
# newline-joined value is also accepted.)

[[ -n "${FIX_BUDGET_SH_LOADED:-}" ]] && return 0
FIX_BUDGET_SH_LOADED=1

FIX_BUDGET_LABEL_PREFIX="fullsend-fix-budget/"

# parse_fix_budget [labels]
# Reads label names (arg 1, or PR_LABELS env when omitted) separated by commas
# and/or newlines. Echoes the smallest valid budget found, or nothing when no
# valid label is present. A malformed value (non-integer, zero, negative) is
# ignored, not fatal — a bad label must not silently drop the existing cap.
parse_fix_budget() {
local labels="${1-${PR_LABELS:-}}"
local best="" label n
# Accept comma-joined labels (the upstream dispatcher format) as well as
# newline-joined: normalize commas to newlines before splitting.
labels="${labels//,/$'\n'}"
while IFS= read -r label; do
Comment on lines +28 to +34

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remediation recommended

5. Feature lacks linked authorization 📜 Skill insight § Compliance

This PR adds a new parser, runtime guard, generated bundle changes, and tests well beyond the rule's
20-line threshold, but the PR metadata contains no linked authorizing issue. The non-trivial feature
therefore lacks the required explicit authorization.
Agent Prompt
## Issue description
The non-trivial feature change has no linked issue authorizing its scope.

## Issue Context
Link an issue that explicitly authorizes the per-PR fix-budget feature and confirms the intended producer wiring and enforcement scope.

## Fix Focus Areas
- scripts/lib/fix-budget.lib.sh[1-42]
- scripts/pre-fix.src.sh[114-119]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

# Trim surrounding whitespace so " fullsend-fix-budget/3 " still matches.
label="${label#"${label%%[![:space:]]*}"}"
label="${label%"${label##*[![:space:]]}"}"
[[ "${label}" == "${FIX_BUDGET_LABEL_PREFIX}"* ]] || continue
n="${label#"${FIX_BUDGET_LABEL_PREFIX}"}"
# Bound the digit count. An arbitrarily long value would overflow Bash's
# signed 64-bit arithmetic in the `-lt` comparison (e.g. 2^64 evaluates as
# 0), which would look "tighter" than any cap and block every fix run.
# A budget above 99999 is meaningless next to caps of 5/10, so treat an
# over-long value as malformed and ignore it.
[[ "${n}" =~ ^[1-9][0-9]{0,4}$ ]] || continue
if [[ -z "${best}" || "${n}" -lt "${best}" ]]; then
best="${n}"
fi
done <<< "${labels}"
[[ -n "${best}" ]] && printf '%s\n' "${best}"
return 0
}
40 changes: 40 additions & 0 deletions scripts/lib/review-labels.lib.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
#!/usr/bin/env bash
# shellcheck shell=bash
# review-labels.lib.sh — recognize pipeline-managed "control" labels.
#
# Control labels are set by the review pipeline (or a maintainer, for the
# fix-budget knob), not by the review agent. post-review refuses to add or
# remove them via agent-recommended label_actions. Kept in a sourceable lib so
# the same definition is exercised by both production and the unit test — a
# duplicated copy in the test would pass even if the production branch drifted.
#
# Bundled into post-review.sh via bundle-sh.sh.

[[ -n "${REVIEW_LABELS_SH_LOADED:-}" ]] && return 0
REVIEW_LABELS_SH_LOADED=1

REVIEW_CONTROL_LABELS=(
"ready-for-merge" "requires-manual-review" "rejected"
"ready-for-review" "fullsend-no-fix" "fullsend-fix"
)

# is_control_label LABEL — return 0 if LABEL is pipeline-managed, 1 otherwise.
is_control_label() {
local label="$1"
local cl
for cl in "${REVIEW_CONTROL_LABELS[@]}"; do
if [[ "${cl}" == "${label}" ]]; then
return 0
fi
done
# Pipeline-managed label prefixes.
if [[ "${label}" == risk/* ]]; then
return 0
fi
# Maintainer-set fix-loop budget (fullsend-fix-budget/N); pipeline-managed so
# the review agent preserves it rather than treating it as a contextual label.
if [[ "${label}" == fullsend-fix-budget/* ]]; then
return 0
fi
return 1
}
69 changes: 68 additions & 1 deletion scripts/post-fix.sh
Original file line number Diff line number Diff line change
Expand Up @@ -1206,6 +1206,60 @@ classify_branch_vs_pr_head() {
fi
}
# END bundled: lib/branch-guard.lib.sh
# shellcheck source=lib/fix-budget.lib.sh
# BEGIN bundled: lib/fix-budget.lib.sh
# shellcheck shell=bash
# fix-budget.lib.sh — parse a per-PR fix-loop budget from PR labels.
#
# A label of the form `fullsend-fix-budget/N` (N a positive integer) lets a
# maintainer cap the review->fix loop for a single PR below the global
# iteration cap. The label can only TIGHTEN the cap, never raise it:
# enforcement lives in pre-fix, which applies min(label_budget, cap).
#
# Bundled into pre-fix.sh via bundle-sh.sh.
#
# Expected env vars (optional):
# PR_LABELS — PR label names separated by commas and/or newlines. Absent/empty
# is fine: parse_fix_budget then returns nothing and the cap is
# unchanged. (The upstream dispatcher comma-joins labels; a
# newline-joined value is also accepted.)

[[ -n "${FIX_BUDGET_SH_LOADED:-}" ]] && return 0
FIX_BUDGET_SH_LOADED=1

FIX_BUDGET_LABEL_PREFIX="fullsend-fix-budget/"

# parse_fix_budget [labels]
# Reads label names (arg 1, or PR_LABELS env when omitted) separated by commas
# and/or newlines. Echoes the smallest valid budget found, or nothing when no
# valid label is present. A malformed value (non-integer, zero, negative) is
# ignored, not fatal — a bad label must not silently drop the existing cap.
parse_fix_budget() {
local labels="${1-${PR_LABELS:-}}"
local best="" label n
# Accept comma-joined labels (the upstream dispatcher format) as well as
# newline-joined: normalize commas to newlines before splitting.
labels="${labels//,/$'\n'}"
while IFS= read -r label; do
# Trim surrounding whitespace so " fullsend-fix-budget/3 " still matches.
label="${label#"${label%%[![:space:]]*}"}"
label="${label%"${label##*[![:space:]]}"}"
[[ "${label}" == "${FIX_BUDGET_LABEL_PREFIX}"* ]] || continue
n="${label#"${FIX_BUDGET_LABEL_PREFIX}"}"
# Bound the digit count. An arbitrarily long value would overflow Bash's
# signed 64-bit arithmetic in the `-lt` comparison (e.g. 2^64 evaluates as
# 0), which would look "tighter" than any cap and block every fix run.
# A budget above 99999 is meaningless next to caps of 5/10, so treat an
# over-long value as malformed and ignore it.
[[ "${n}" =~ ^[1-9][0-9]{0,4}$ ]] || continue
if [[ -z "${best}" || "${n}" -lt "${best}" ]]; then
best="${n}"
fi
done <<< "${labels}"
[[ -n "${best}" ]] && printf '%s\n' "${best}"
return 0
}
# END bundled: lib/fix-budget.lib.sh


# ---------------------------------------------------------------------------
Expand Down Expand Up @@ -1544,6 +1598,16 @@ fi
# ---------------------------------------------------------------------------
ITERATION="${FIX_ITERATION:-1}"
BOT_CAP="${ITERATION_CAP:-5}"

# A per-PR `fullsend-fix-budget/N` label may tighten the BOT cap only (never
# raise it, never touch the human cap) — matching pre-fix. Mirror it here so the
# needs-human warning and the iteration summary reflect the cap pre-fix actually
# enforces. Without this, a budget of 2 under a global cap of 5 would report
# "2 of 5" and never add needs-human, even though pre-fix rejects the next cycle.
FIX_BUDGET="$(parse_fix_budget "${PR_LABELS:-}")"
if [[ -n "${FIX_BUDGET}" && "${FIX_BUDGET}" -lt "${BOT_CAP}" ]]; then
BOT_CAP="${FIX_BUDGET}"
fi
WARN_THRESHOLD=$(( BOT_CAP - 1 ))

# The needs-human label is based on the bot cap — it signals that the
Expand All @@ -1568,5 +1632,8 @@ echo " Trigger: ${TRIGGER_SOURCE}"
if is_bot_user "${TRIGGER_SOURCE}"; then
echo " Iteration: ${ITERATION} of ${BOT_CAP} (bot cap)"
else
echo " Iteration: ${ITERATION} of ${ITERATION_CAP_HUMAN:-10} (human cap, total across bot+human)"
# The fix-budget label never tightens the human cap, so the human escape hatch
# always reports its full ITERATION_CAP_HUMAN budget.
HUMAN_CAP="${ITERATION_CAP_HUMAN:-10}"
echo " Iteration: ${ITERATION} of ${HUMAN_CAP} (human cap, total across bot+human)"
fi
17 changes: 16 additions & 1 deletion scripts/post-fix.src.sh
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,8 @@ source "${SCRIPT_DIR_POST}/lib/gitleaks-install.lib.sh"
source "${SCRIPT_DIR_POST}/lib/precommit-gate.lib.sh"
# shellcheck source=lib/branch-guard.lib.sh
source "${SCRIPT_DIR_POST}/lib/branch-guard.lib.sh"
# shellcheck source=lib/fix-budget.lib.sh
source "${SCRIPT_DIR_POST}/lib/fix-budget.lib.sh"


# ---------------------------------------------------------------------------
Expand Down Expand Up @@ -403,6 +405,16 @@ fi
# ---------------------------------------------------------------------------
ITERATION="${FIX_ITERATION:-1}"
BOT_CAP="${ITERATION_CAP:-5}"

# A per-PR `fullsend-fix-budget/N` label may tighten the BOT cap only (never
# raise it, never touch the human cap) — matching pre-fix. Mirror it here so the
# needs-human warning and the iteration summary reflect the cap pre-fix actually
# enforces. Without this, a budget of 2 under a global cap of 5 would report
# "2 of 5" and never add needs-human, even though pre-fix rejects the next cycle.
FIX_BUDGET="$(parse_fix_budget "${PR_LABELS:-}")"
if [[ -n "${FIX_BUDGET}" && "${FIX_BUDGET}" -lt "${BOT_CAP}" ]]; then
BOT_CAP="${FIX_BUDGET}"
fi
WARN_THRESHOLD=$(( BOT_CAP - 1 ))

# The needs-human label is based on the bot cap — it signals that the
Expand All @@ -427,5 +439,8 @@ echo " Trigger: ${TRIGGER_SOURCE}"
if is_bot_user "${TRIGGER_SOURCE}"; then
echo " Iteration: ${ITERATION} of ${BOT_CAP} (bot cap)"
else
echo " Iteration: ${ITERATION} of ${ITERATION_CAP_HUMAN:-10} (human cap, total across bot+human)"
# The fix-budget label never tightens the human cap, so the human escape hatch
# always reports its full ITERATION_CAP_HUMAN budget.
HUMAN_CAP="${ITERATION_CAP_HUMAN:-10}"
echo " Iteration: ${ITERATION} of ${HUMAN_CAP} (human cap, total across bot+human)"
fi
29 changes: 10 additions & 19 deletions scripts/post-review-test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -302,25 +302,11 @@ run_downgrade_test "approve-all-filtered-removes-findings" \
# ---------------------------------------------------------------------------
# Control-label guard tests
# ---------------------------------------------------------------------------

REVIEW_CONTROL_LABELS=(
"ready-for-merge" "requires-manual-review" "rejected"
"ready-for-review" "fullsend-no-fix" "fullsend-fix"
)

is_control_label() {
local label="$1"
for cl in "${REVIEW_CONTROL_LABELS[@]}"; do
if [[ "${cl}" == "${label}" ]]; then
return 0
fi
done
# Pipeline-managed label prefixes
if [[ "${label}" == risk/* ]]; then
return 0
fi
return 1
}
# Source the production is_control_label from its lib, so these tests fail if
# the production branch is removed or drifts — not a copy that always passes.
_LABELS_LIB_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/lib"
# shellcheck source=lib/review-labels.lib.sh
source "${_LABELS_LIB_DIR}/review-labels.lib.sh"

run_control_label_test() {
local test_name="$1"
Expand Down Expand Up @@ -360,8 +346,13 @@ run_control_label_test "risk-elevated-is-control" "risk/elevated" "true"
run_control_label_test "risk-high-is-control" "risk/high" "true"
run_control_label_test "risk-critical-is-control" "risk/critical" "true"

# Maintainer-set fix-budget labels should be control labels
run_control_label_test "fix-budget-3-is-control" "fullsend-fix-budget/3" "true"
run_control_label_test "fix-budget-99999-is-control" "fullsend-fix-budget/99999" "true"

# Non-control labels should NOT be recognized
run_control_label_test "area-api-not-control" "area/api" "false"
run_control_label_test "fix-budget-prefix-only-not-control" "fullsend-fix-budget" "false"
run_control_label_test "priority-high-not-control" "priority/high" "false"
run_control_label_test "bug-not-control" "bug" "false"
run_control_label_test "empty-not-control" "" "false"
Expand Down
Loading
Loading