Skip to content

chore(retro): reject "Evidence for" proposals in post-retro.sh#85

Merged
rh-hemartin merged 1 commit into
mainfrom
fix/reject-evidence-for-proposals
Jul 10, 2026
Merged

chore(retro): reject "Evidence for" proposals in post-retro.sh#85
rh-hemartin merged 1 commit into
mainfrom
fix/reject-evidence-for-proposals

Conversation

@rh-hemartin

Copy link
Copy Markdown
Member

Summary

  • Add a deterministic title-pattern gate in post-retro.sh that rejects proposals matching "Evidence for" patterns before they reach gh issue create
  • Rejected proposals are logged as ::warning:: and their content folded into the summary comment posted on the originating PR/issue
  • The retro-analysis skill already instructs the agent not to file these, but the agent ignores that instruction (~335 evidence issues filed in 4 days)

Closes fullsend-ai/fullsend#3881

What changed

scripts/post-retro.sh: Case-insensitive regex check in the filing loop rejects titles matching:

  • ^evidence (for|of|:)
  • ^additional evidence
  • evidence for # anywhere

Titles like "Fix evidence gathering bug" pass through (the word "evidence" in non-matching context is not filtered).

scripts/post-retro-test.sh: 6 new test cases (rejection, case-insensitive, "Additional evidence" variant, false-positive guard, mixed proposals, content folding into summary). Mock updated to capture gh api stdin for body assertions.

Test plan

  • All 17 tests pass (bash scripts/post-retro-test.sh)
  • Full test suite passes (make test)
  • Deploy and confirm no "Evidence for" issues filed on next retro run

🤖 Generated with Claude Code

@rh-hemartin rh-hemartin requested a review from a team as a code owner July 9, 2026 15:45
@qodo-code-review

Copy link
Copy Markdown

PR Summary by Qodo

Reject “Evidence for” retro proposals before filing GitHub issues

🐞 Bug fix 🧪 Tests 🕐 20-40 Minutes

Grey Divider

AI Description

• Add a title-pattern gate in "scripts/post-retro.sh" to skip "Evidence for" proposals.
• Fold rejected proposal notes into the posted summary comment and log warnings.
• Expand "scripts/post-retro-test.sh" with fixtures and assertions for filtering behavior.
Diagram

graph TD
  A["post-retro.sh"] --> B["agent-result.json"] --> C["Evidence title filter"]
  C --> D["gh issue create"] --> E["Issue links"] --> F["Summary comment"] --> G{{"GitHub API"}}
  C --> H["Evidence notes"] --> F
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Fix at the source (prompt/skill hardening)
  • ➕ No additional filtering logic in the post-script
  • ➕ Keeps behavior aligned with agent intent
  • ➖ Non-deterministic; LLM can ignore instructions (already happening)
  • ➖ Harder to validate reliably with unit tests
2. Make patterns configurable (allowlist/blocklist file)
  • ➕ Easier to evolve patterns without code changes
  • ➕ Can be tuned per repo/team over time
  • ➖ Adds configuration surface area and parsing complexity
  • ➖ Risk of misconfiguration causing under/over-filtering
3. Server-side enforcement via GitHub labels/rules
  • ➕ Central policy enforcement across all issue creation paths
  • ➕ Can provide visibility/audit via labeling
  • ➖ More infrastructure/setup; depends on GitHub features/automation
  • ➖ Still needs client-side handling to fold notes into summary

Recommendation: Keep the deterministic post-script gate as implemented: it’s testable, immediate, and robust against agent non-compliance. If the pattern set is expected to change frequently, consider a follow-up to externalize patterns into a small config file while retaining the same filtering + summary-folding behavior.

Files changed (2) +236 / -7

Bug fix (1) +32 / -4
post-retro.shAdd evidence-title filter and fold rejected items into summary comment +32/-4

Add evidence-title filter and fold rejected items into summary comment

• Introduces a case-insensitive title-pattern gate that rejects "Evidence for"/"Additional evidence"-style proposals before calling "gh issue create". Rejected items are logged as workflow warnings and appended into a new "Evidence notes" section in the final summary comment; also prints a filtered-count message.

scripts/post-retro.sh

Tests (1) +204 / -3
post-retro-test.shAdd fixtures and assertions for evidence filtering and comment-body folding +204/-3

Add fixtures and assertions for evidence filtering and comment-body folding

• Extends the gh mock to capture stdin for "gh api" calls so tests can assert on the posted comment body. Adds new JSON fixtures and helper runners to verify evidence proposals are not filed, filtering is case-insensitive, false positives are avoided, mixed proposals behave correctly, and filtered content is folded into the summary.

scripts/post-retro-test.sh

@fullsend-ai-review

fullsend-ai-review Bot commented Jul 9, 2026

Copy link
Copy Markdown

🤖 Finished Review · ✅ Success · Started 3:46 PM UTC · Completed 3:56 PM UTC
Commit: 032b3f1 · View workflow run →

@qodo-code-review

qodo-code-review Bot commented Jul 9, 2026

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (0) 📘 Rule violations (0) 📜 Skill insights (1)

Context used
✅ Compliance rules (platform): 55 rules
✅ Skills: 4 invoked
  code-review
  code-implementation
  pr-review
  docs-review

Grey Divider


Action required

1. ::warning:: uses weak sanitization ✓ Resolved 📜 Skill insight ⛨ Security
Description
scripts/post-retro.sh emits a ::warning::proposal[$i] rejected ... ${SAFE_TITLE} GitHub Actions
workflow command but sanitizes SAFE_TITLE only by stripping :: and %0A/%0D substrings, leaving
% encoding, literal newlines/CR, ANSI escapes, and other control characters unsanitized. A crafted
proposal title can therefore break workflow-command framing to spoof logs or inject additional
GitHub Actions workflow commands/annotations.
Code

scripts/post-retro.sh[R90-95]

+    SAFE_TITLE="${TITLE//::/}"
+    SAFE_TITLE="${SAFE_TITLE//%0A/}"
+    SAFE_TITLE="${SAFE_TITLE//%0a/}"
+    SAFE_TITLE="${SAFE_TITLE//%0D/}"
+    SAFE_TITLE="${SAFE_TITLE//%0d/}"
+    echo "::warning::proposal[$i] rejected — title matches evidence-for pattern: ${SAFE_TITLE}. Folding into summary."
Relevance

⭐⭐ Medium

No repo history found specifically requiring stronger sanitization for GitHub Actions ::warning::
workflow commands.

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
PR Compliance ID 1538382 requires sanitizing all interpolated values used in GitHub Actions workflow
commands, yet post-retro.sh builds SAFE_TITLE by removing only :: and percent-encoded CR/LF
(%0A/%0D) and then echoes it inside a ::warning::...${SAFE_TITLE} workflow command, which still
permits problematic characters like literal $'\n'/$'\r' (and other control sequences) to alter
command parsing. The repository’s existing mitigation is demonstrated in post-review.sh, which
strips literal newline and carriage return characters before emitting workflow commands,
highlighting that post-retro.sh is missing equivalent protections.

scripts/post-retro.sh[90-95]
scripts/post-retro.sh[90-96]
scripts/post-review.sh[258-266]
Skill: pr-review

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`scripts/post-retro.sh` interpolates `SAFE_TITLE` (derived from proposal titles) into a GitHub Actions workflow command (`::warning::proposal[...] rejected ... ${SAFE_TITLE}`) with incomplete sanitization. Update the sanitization so that all untrusted interpolated values in workflow commands are robustly cleaned to prevent workflow-command injection and log spoofing (including stripping/neutralizing literal newlines and carriage returns, `::` sequences, `%0A/%0D`, `%` encoding concerns, ANSI escapes, and other control characters as required by policy).

## Issue Context
The evidence-for rejection path logs the proposal title in a `::warning::` command; if an attacker crafts a multi-line title (or includes other control sequences), it can break the workflow-command framing and inject additional commands/annotations into the runner output. Other post scripts in this repo already mitigate this class of issue by explicitly stripping `$'\n'` and `$'\r'` before echoing workflow commands (see `post-review.sh` behavior), so `post-retro.sh` should match that established pattern and apply the same sanitization anywhere it emits `::warning::...` / `::error::...` style workflow commands.

## Fix Focus Areas
- scripts/post-retro.sh[90-96]
- scripts/post-retro.sh[127-133]

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


2. Pipefail breaks filtering ✓ Resolved 🐞 Bug ☼ Reliability
Description
In scripts/post-retro.sh, the evidence-for rejection path appends a note using `$(jq ... | head
-1) while running under set -euo pipefail, which can abort the entire script when head` closes
the pipe and jq exits non-zero. This can stop processing remaining proposals and prevent posting
the summary comment whenever a filtered proposal is encountered.
Code

scripts/post-retro.sh[R96-99]

+    EVIDENCE_NOTES="${EVIDENCE_NOTES}
+- **${TITLE}** (${TARGET_REPO}): $(jq -r ".proposals[$i].what_happened" "${RESULT_FILE}" | head -1)"
+    FILTERED_COUNT=$((FILTERED_COUNT + 1))
+    continue
Relevance

⭐⭐ Medium

Team cares about pipefail hard-fail regressions (pipefail best-effort change partially accepted in
PR #41), but not exact jq|head case.

PR-#41

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The script enables set -euo pipefail, and the new filtered path introduces a jq | head -1
pipeline inside command substitution; the repo already uses ... | head -1 || true elsewhere to
avoid pipefail-related exits in similar patterns.

scripts/post-retro.sh[14-15]
scripts/post-retro.sh[86-99]
scripts/post-code.sh[417-418]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The evidence-for filtering branch uses a `jq | head -1` pipeline inside command substitution while `set -euo pipefail` is enabled. This pattern can fail the script due to SIGPIPE/pipefail behavior.

## Issue Context
This is in the newly added rejection gate path, so it will trigger exactly on the proposals you’re trying to filter most often.

## Fix Focus Areas
- scripts/post-retro.sh[86-100]

## Suggested fix
Avoid `head` entirely by extracting the first line inside `jq` or in bash without a pipe, e.g.:
- `FIRST_LINE=$(jq -r ".proposals[$i].what_happened | gsub(\"\\r\";\"\") | split(\"\\n\")[0]" "${RESULT_FILE}")`
- then append `${FIRST_LINE}` into `EVIDENCE_NOTES`.
Alternatively, if you keep a pipeline, make it explicitly non-fatal (e.g. wrap with `|| true`) in a way that can’t terminate the script under `-euo pipefail`.

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


3. Protected scripts/ files modified 📜 Skill insight § Compliance
Description
This PR modifies files under scripts/, which is a protected governance/infrastructure path that
must not be auto-approved and requires explicit human review. Even with the linked issue
justification, a protected-path compliance finding is required.
Code

scripts/post-retro.sh[R75-100]

ISSUE_LINKS=""
+EVIDENCE_NOTES=""
+FILTERED_COUNT=0
for i in $(seq 0 $((PROPOSAL_COUNT - 1))); do
  TARGET_REPO=$(jq -r ".proposals[$i].target_repo" "${RESULT_FILE}")
  TITLE=$(jq -r ".proposals[$i].title" "${RESULT_FILE}")

+  # Deterministic gate: reject "Evidence for" proposals.
+  # The retro-analysis skill instructs the agent not to file these, but the
+  # agent ignores the instruction frequently enough that a post-script gate
+  # is needed. See fullsend-ai/fullsend#3881.
+  TITLE_LOWER=$(echo "${TITLE}" | tr '[:upper:]' '[:lower:]')
+  if [[ "${TITLE_LOWER}" =~ ^evidence[[:space:]]+(for|of|:) ]] || \
+     [[ "${TITLE_LOWER}" =~ ^additional[[:space:]]+evidence ]] || \
+     [[ "${TITLE_LOWER}" =~ evidence[[:space:]]+for[[:space:]]+\# ]]; then
+    SAFE_TITLE="${TITLE//::/}"
+    SAFE_TITLE="${SAFE_TITLE//%0A/}"
+    SAFE_TITLE="${SAFE_TITLE//%0a/}"
+    SAFE_TITLE="${SAFE_TITLE//%0D/}"
+    SAFE_TITLE="${SAFE_TITLE//%0d/}"
+    echo "::warning::proposal[$i] rejected — title matches evidence-for pattern: ${SAFE_TITLE}. Folding into summary."
+    EVIDENCE_NOTES="${EVIDENCE_NOTES}
+- **${TITLE}** (${TARGET_REPO}): $(jq -r ".proposals[$i].what_happened" "${RESULT_FILE}" | head -1)"
+    FILTERED_COUNT=$((FILTERED_COUNT + 1))
+    continue
+  fi
Relevance

⭐⭐ Medium

No historical suggestions found about protected scripts/ compliance/auto-approval gating in this
repo.

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
PR Compliance ID 1538392 mandates a finding whenever protected paths (including scripts/) are
modified. The diff shows substantive changes in both scripts/post-retro.sh and
scripts/post-retro-test.sh.

scripts/post-retro.sh[75-100]
scripts/post-retro-test.sh[28-95]
Skill: pr-review

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Protected governance/infrastructure paths were modified (`scripts/`). These changes require explicit human review (do not auto-approve).

## Issue Context
The PR includes justification (linked issue), but policy still requires raising a protected-path finding to ensure appropriate review handling.

## Fix Focus Areas
- scripts/post-retro.sh[75-100]
- scripts/post-retro-test.sh[28-95]

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



Remediation recommended

4. Unbounded comment growth ✓ Resolved 🐞 Bug ☼ Reliability
Description
The summary comment body is built from SUMMARY plus all ISSUE_LINKS plus all EVIDENCE_NOTES
without any size guard, but the script treats non-401/403 comment-post failures as fatal. This makes
the run fragile to comment rejections caused by oversized aggregated content.
Code

scripts/post-retro.sh[R161-167]

+COMMENT="${SUMMARY}"
+if [[ -n "${ISSUE_LINKS}" ]]; then
+  COMMENT=$(printf '%s\n\n### Proposals filed\n\n%s' "${COMMENT}" "${ISSUE_LINKS}")
+fi
+if [[ -n "${EVIDENCE_NOTES}" ]]; then
+  COMMENT=$(printf '%s\n\n### Evidence notes (not filed as issues)\n%s' "${COMMENT}" "${EVIDENCE_NOTES}")
fi
Relevance

⭐⭐⭐ High

Comment-length/aggregation fragility fixes were definitely accepted before (truncate/limit to avoid
rejection) in PR #10.

PR-#10

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
post-retro.sh appends the new evidence notes section into the comment without any truncation, and
post-retro-test.sh explicitly models HTTP 422 as a fatal condition. Prior accepted fix PR #10
documents the same “aggregated content exceeds comment limit and gets rejected” pattern in a related
post script.

scripts/post-retro.sh[161-167]
scripts/post-retro.sh[176-206]
scripts/post-retro-test.sh[426-432]
PR-#10

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`COMMENT` is assembled by concatenating multiple unbounded sections and then posted via `gh api`; failures (e.g. HTTP 422) are fatal. With many proposals/notes, the comment payload can become too large and cause the run to fail even when issue filing succeeded.

## Issue Context
A similar failure mode (aggregated text exceeding a comment-length gate) has already occurred in another post script and was fixed by adding truncation/limits.

## Fix Focus Areas
- scripts/post-retro.sh[161-178]
- scripts/post-retro.sh[180-206]

## Suggested fix
Introduce a `MAX_COMMENT_LEN` and enforce it before posting, prioritizing retention of `SUMMARY` and `### Proposals filed`, and truncating/dropping `EVIDENCE_NOTES` first.
Example approach:
- If `${#COMMENT}` exceeds the cap, truncate `EVIDENCE_NOTES` to fit and append a clear `...(truncated)` marker.

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



Informational

5. Echo option edge case ✓ Resolved 🐞 Bug ≡ Correctness
Description
TITLE_LOWER is computed via echo "${TITLE}" | tr ..., but bash echo can treat leading strings
like -n/-e as options and alter the title before regex matching. This can cause incorrect
filtering decisions for option-prefixed titles.
Code

scripts/post-retro.sh[R86-87]

+  TITLE_LOWER=$(echo "${TITLE}" | tr '[:upper:]' '[:lower:]')
+  if [[ "${TITLE_LOWER}" =~ ^evidence[[:space:]]+(for|of|:) ]] || \
Relevance

⭐⭐ Medium

No prior review evidence found enforcing printf-over-echo for option/escape safety in scripts.

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The code currently uses echo to feed untrusted title text into tr; elsewhere in the repo,
printf '%s' is used when safely piping arbitrary strings (to avoid option/escape interpretation).

scripts/post-retro.sh[86-87]
scripts/post-triage.sh[73-75]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Using `echo` for arbitrary string data is unsafe because it may interpret leading `-n`/`-e` as options.

## Issue Context
This only affects the normalization step used for regex matching, but it’s easy to harden.

## Fix Focus Areas
- scripts/post-retro.sh[86-87]

## Suggested fix
Replace:
- `TITLE_LOWER=$(echo "${TITLE}" | tr ...)`
With:
- `TITLE_LOWER=$(printf '%s' "${TITLE}" | tr '[:upper:]' '[:lower:]')`

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


Grey Divider

Qodo Logo

Comment thread scripts/post-retro.sh Outdated
Comment thread scripts/post-retro.sh
Comment thread scripts/post-retro.sh
Comment thread scripts/post-retro.sh Outdated
Comment thread scripts/post-retro.sh
@fullsend-ai-review

fullsend-ai-review Bot commented Jul 9, 2026

Copy link
Copy Markdown

Review

Verdict: approve — all prior review findings addressed; no medium+ issues remain.

This is a re-review. The prior review (SHA 032b3f1, provenance: app-verified) raised one medium and three low findings. All four have been addressed in this revision:

Prior finding Severity Status
Regex colon alternative unreachable (^evidence[[:space:]]+(for|of|:)) medium Fixed — colon pattern split into separate ^evidence: condition
Mixed-proposals test missing issue-creation assertion low Fixedrun_test "evidence-for-mixed-issue-created" added
docs/retro.md line 14 not updated low Fixed — now documents evidence-for filtering
Raw title in EVIDENCE_NOTES markdown low Acknowledged — unchanged, follows existing ISSUE_LINKS pattern

What changed

The PR adds a deterministic post-script gate in post-retro.sh to reject proposals whose titles match evidence-for patterns before they reach gh issue create. This compensates for the retro-analysis agent ignoring its skill instructions (~335 spurious issues in 4 days). The approach — regex-based title filtering in the filing loop — follows the established guardrail pattern used in other post-scripts (post-triage.sh guard clauses, post-code.sh artifact stripping).

The regex patterns now correctly handle three cases as separate conditions:

  1. ^evidence (for|of) # — evidence-for/of with issue reference
  2. ^evidence: — colon-prefixed evidence titles
  3. ^additional evidence — additional evidence variant

The comment-assembly refactoring from a two-branch if/else to three independent conditionals is a necessary consequence of adding the evidence-notes output path. Comment truncation at 65000 chars (safely under GitHub's 65536 limit) prevents oversized comments when many proposals are filtered.

Analysis

Correctness: Regex patterns are verified correct. The ^evidence[[:space:]]+(for|of)[[:space:]]+\# pattern requires a # after the preposition, correctly allowing titles like "Evidence for improving X" to pass through. The seq 0 $((PROPOSAL_COUNT - 1)) iteration handles zero proposals correctly. Comment assembly produces correct output for all combinations (0 proposals, all filtered, mixed, none filtered). Truncation maximum is 65000 + 17 bytes = 65017, safely under the 65536 limit.

Security: The ::warning:: output sanitizes the interpolated title for GHA workflow command injection (strips ::, %0A/%0D, literal newlines). The $i loop counter is always a non-negative integer. The EVIDENCE_NOTES markdown uses raw ${TITLE}, but the content is JSON-encoded by jq --arg before posting to the GitHub API, preventing API-level injection. Markdown rendering injection risk is low since the title originates from the agent's own output.

Test coverage: 14 new test cases cover: rejection, case insensitivity, "additional evidence" variant, false-positive guards (mid-title "evidence for", no issue ref), mixed proposals (both filtering and issue creation verified), content folding into summary, GHA command injection sanitization (:: and %0A), and comment truncation (both marker presence and length limit).

Architecture: Post-script filtering is the correct layer — the agent runs in a sandbox and cannot be prevented from producing evidence-for proposals, so the post-script gate provides defense-in-depth. This matches the pattern in post-triage.sh (guard clauses) and post-code.sh (artifact stripping).

Style: Follows established codebase conventions (variable naming, error handling, test structure, mock patterns, sanitization idioms).

Documentation: docs/retro.md line 14 now accurately documents the filtering behavior.

Remaining low-severity notes

  1. Raw title in EVIDENCE_NOTES (scripts/post-retro.sh): ${TITLE} is interpolated into EVIDENCE_NOTES without escaping markdown-significant characters ([, ], *). A crafted title could alter rendered comment structure. Risk is low: title originates from agent output, GitHub sanitizes against script execution, and the pre-existing ISSUE_LINKS on line 121 uses the same raw-title pattern. Severity anchored from prior review.

  2. Raw what_happened in EVIDENCE_NOTES (scripts/post-retro.sh): The first line of what_happened is interpolated unsanitized. Same risk profile as finding 1 — agent-controlled data, JSON-encoded before posting.


Protected paths detected — this PR modifies files under one or more
protected paths. The review agent cannot approve PRs that touch these paths.
A human reviewer must approve this PR.

Protected files in this PR:

  • scripts/post-retro-test.sh
  • scripts/post-retro.sh
Previous run

Review

Verdict: comment — one medium-severity finding worth noting; no blocking issues.

The change adds a deterministic post-script gate to reject "Evidence for" proposals before they reach gh issue create, compensating for the retro-analysis agent ignoring its skill instructions (~335 spurious issues in 4 days). The approach — a regex-based title filter in the post-script — follows the established guardrail pattern used in other post-scripts (post-triage.sh control labels, post-review.sh severity filtering). Test coverage is solid with 6 new cases covering rejection, case insensitivity, false positives, mixed proposals, and content folding.

The comment-assembly refactoring from a two-branch if/else to three independent conditionals is a necessary consequence of adding the evidence-notes output path and preserves behavior for all existing code paths.

Findings

1. Regex colon alternative is unreachable (medium · correctness)

File: scripts/post-retro.sh, line ~83

The pattern ^evidence[[:space:]]+(for|of|:) requires one or more whitespace characters between "evidence" and the alternation group. This means the colon alternative only matches titles like "Evidence : review gap" (with a space before the colon) — not the natural format "Evidence: review gap" (colon immediately after "evidence"). Since the PR body lists ^evidence (for|of|:) as an intended pattern, titles in the "Evidence:" format will silently pass through the gate.

Remediation: If "Evidence:" titles should be caught, restructure the first pattern to handle the colon case without requiring whitespace, e.g.:

if [[ "${TITLE_LOWER}" =~ ^evidence(:|[[:space:]]+(for|of)) ]] || \

Alternatively, if the colon format does not occur in practice, remove it from the alternation to avoid dead code.

2. Mixed-proposals test does not verify issue creation (low · test-adequacy)

File: scripts/post-retro-test.sh, line ~466

The evidence-for-mixed test uses run_test_stdout and only verifies that stdout contains "1 proposal(s) filtered". It does not assert that gh issue create was actually called for the non-filtered proposal. A regression that accidentally filters both proposals would still pass this test.

Remediation: Add a companion assertion (via run_test or a GH_LOG check) verifying that gh issue create appears in the call log for the non-evidence proposal.

3. docs/retro.md does not mention evidence-for filtering (low · documentation-staleness)

File: docs/retro.md, line 14

Line 14 states "Post-script creates GitHub issues from the agent's proposals" without mentioning that proposals matching evidence-for patterns are now filtered out and folded into the summary comment as evidence notes.

Remediation: Update line 14 to note that evidence-for proposals are excluded from issue creation and included in the summary comment instead.

4. Raw title in EVIDENCE_NOTES markdown (low · markdown-injection)

File: scripts/post-retro.sh

The TITLE variable is interpolated into EVIDENCE_NOTES without escaping markdown-significant characters ([, ], (, )). A crafted title could inject markdown links into the summary comment. The risk is low because (a) the title originates from the agent's own JSON output, (b) GitHub sanitizes rendered markdown against script execution, and (c) the pre-existing ISSUE_LINKS construction on line 121 uses the same raw-title pattern. Noting for awareness, not as a blocking concern.

fullsend-ai-review[bot]

This comment was marked as outdated.

@fullsend-ai-review fullsend-ai-review Bot added the requires-manual-review Review requires human judgment label Jul 9, 2026

@waynesun09 waynesun09 left a comment

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.

Review-squad pass (4 agents: Claude, Gemini, Codex). Most medium+ findings overlap with the existing automated review already on this PR (pipefail crash on jq | head -1, unreachable colon regex, weak SAFE_TITLE sanitization) — not re-posting those. One new finding below: the regex is also too broad in places, causing false-positive rejections of legitimate proposals, in addition to being too narrow for the "Evidence:" case already flagged.

Comment thread scripts/post-retro.sh Outdated
@rh-hemartin rh-hemartin self-assigned this Jul 10, 2026
@rh-hemartin rh-hemartin force-pushed the fix/reject-evidence-for-proposals branch from 032b3f1 to 62e252e Compare July 10, 2026 06:45
The retro agent routinely ignores the prompt instruction not to file
"Evidence for" issues (~335 filed in 4 days). Move enforcement from
the LLM to a deterministic title-pattern gate in the post-script.

Rejected proposals are logged as warnings and their content folded
into the summary comment on the originating PR/issue.

Closes fullsend-ai/fullsend#3881

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Hector Martinez <hemartin@redhat.com>
@rh-hemartin rh-hemartin force-pushed the fix/reject-evidence-for-proposals branch from 62e252e to 0478721 Compare July 10, 2026 06:45
@rh-hemartin

Copy link
Copy Markdown
Member Author

If I got that right, all feedback addressed.

@fullsend-ai-review

fullsend-ai-review Bot commented Jul 10, 2026

Copy link
Copy Markdown

🤖 Finished Review · ✅ Success · Started 6:46 AM UTC · Completed 6:55 AM UTC
Commit: 0478721 · View workflow run →

@fullsend-ai-review fullsend-ai-review Bot added requires-manual-review Review requires human judgment and removed requires-manual-review Review requires human judgment labels Jul 10, 2026
This was referenced Jul 10, 2026
This was referenced Jul 10, 2026
@rh-hemartin rh-hemartin added this pull request to the merge queue Jul 10, 2026
Merged via the queue into main with commit 12e356e Jul 10, 2026
11 of 12 checks passed
@rh-hemartin rh-hemartin deleted the fix/reject-evidence-for-proposals branch July 10, 2026 15:00
@fullsend-ai-retro

fullsend-ai-retro Bot commented Jul 10, 2026

Copy link
Copy Markdown

🤖 Finished Retro · ✅ Success · Started 3:02 PM UTC · Completed 3:09 PM UTC
Commit: 0478721 · View workflow run →

@waynesun09 waynesun09 left a comment

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.

Review-squad pass (4 agents: Claude ×2, Gemini, Codex). 6 new MEDIUM findings posted inline — all verified against the head branch code. Existing review comments on sanitization weakness, pipefail/SIGPIPE, unreachable colon regex, and ^evidence broadness were already present and are not re-posted.

New comments posted: 6
Already posted (skipped): 5 (sanitization weakness, protected-path compliance, pipefail crash, echo option edge case, unbounded comment growth)
Updated in-place: 0

Assisted-by: Claude (review), Gemini (review), Codex (review)

Comment thread scripts/post-retro.sh
- **${TITLE}** (${TARGET_REPO}): $(jq -r ".proposals[$i].what_happened | split(\"\\n\")[0]" "${RESULT_FILE}")"
FILTERED_COUNT=$((FILTERED_COUNT + 1))
continue
fi

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] correctness — unsanitized TITLE in EVIDENCE_NOTES

SAFE_TITLE is computed at lines 92-98 but the raw TITLE is used here in the evidence notes that get posted as a GitHub comment. If the title contains markdown metacharacters (**, [](url), <), they'll render in the comment body and could break formatting.

Suggestion: Use ${SAFE_TITLE} instead of ${TITLE}:

EVIDENCE_NOTES="${EVIDENCE_NOTES}
- **${SAFE_TITLE}** (${TARGET_REPO}): $(jq -r ...)"

Flagged by 3/4 agents (Claude, Gemini, Codex)

Comment thread scripts/post-retro.sh
TITLE_LOWER=$(printf '%s' "${TITLE}" | tr '[:upper:]' '[:lower:]')
if [[ "${TITLE_LOWER}" =~ ^evidence[[:space:]]+(for|of)[[:space:]]+\# ]] || \
[[ "${TITLE_LOWER}" =~ ^evidence: ]] || \
[[ "${TITLE_LOWER}" =~ ^additional[[:space:]]+evidence ]]; then

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] correctness — ^additional[[:space:]]+evidence is broader than other patterns

The first pattern requires an issue reference (#) but this third pattern matches anything starting with "Additional evidence" — including legitimate titles like "Additional evidence-gathering capabilities for review agent". The asymmetry means this branch is more false-positive-prone than the others.

Suggestion: Either tighten to require a qualifier after "evidence" (e.g. ^additional[[:space:]]+evidence[[:space:]]+(for|of)) or add a code comment explaining why the broader match is intentional for this variant.

Flagged by 3/4 agents (Claude, Gemini, Codex)

Comment thread scripts/post-retro.sh
if [[ -n "${EVIDENCE_NOTES}" ]]; then
COMMENT=$(printf '%s\n\n### Evidence notes (not filed as issues)\n%s' "${COMMENT}" "${EVIDENCE_NOTES}")
fi

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] documentation — misleading comment about truncation strategy

Comment says "Truncate EVIDENCE_NOTES first if over" but the code truncates the entire COMMENT string positionally at 65000 chars. The comment describes intent the code doesn't implement.

Suggestion: Update to match actual behavior:

# GitHub comment limit is 65536 chars. Truncate from the end if over.
# Since evidence notes are appended last, they are trimmed first.

Flagged by 2/4 agents (Claude coder, Claude researcher)


# Fixture: a valid agent result with no proposals.
FIXTURE_NO_PROPOSALS='{
"summary": "The retro analysis found no actionable improvements.",

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] test-coverage — missing tests for ^evidence: and evidence of regex branches

Two of the three regex branches have zero test coverage:

  1. ^evidence: — no fixture exercises a title like "Evidence: review coverage gap". If this branch were accidentally removed, no test would catch it.
  2. evidence of #N — the (for|of) alternation accepts "Evidence of #1234" but no fixture tests the "of" variant.

Suggestion: Add two fixtures:

FIXTURE_EVIDENCE_COLON='{ ... "title": "Evidence: review coverage gap" ... }'
FIXTURE_EVIDENCE_OF='{ ... "title": "Evidence of #1234: repeated pattern" ... }'

with corresponding run_test_no_gh_call assertions.

Flagged by 3/4 agents (Claude coder, Claude researcher, Gemini)


# --- Results ---

if [[ ${FAILURES} -gt 0 ]]; then

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] test-correctness — truncation length test passes vacuously if preceding test fails

If run_test_stdin "comment-truncated-at-limit" fails (script exits non-zero before posting), GH_STDIN_LOG is empty (0 bytes) and this length check passes vacuously — it never catches a real regression.

Suggestion: Guard with a non-empty check:

if [[ ! -s "${GH_STDIN_LOG}" ]]; then
  echo "FAIL: comment-truncated-length — GH_STDIN_LOG is empty (precondition not met)"
  FAILURES=$((FAILURES + 1))
elif [[ ${POSTED_LEN} -gt 65536 ]]; then
  ...

Flagged by 1/4 agents (Claude researcher)

Comment thread scripts/post-retro.sh
Comment on lines +92 to +98
SAFE_TITLE="${SAFE_TITLE//::/:}"
SAFE_TITLE="${SAFE_TITLE//%0A/}"
SAFE_TITLE="${SAFE_TITLE//%0a/}"
SAFE_TITLE="${SAFE_TITLE//%0D/}"
SAFE_TITLE="${SAFE_TITLE//%0d/}"
echo "::warning::proposal[$i] rejected — title matches evidence-for pattern: ${SAFE_TITLE}. Folding into summary."
EVIDENCE_NOTES="${EVIDENCE_NOTES}

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] maintainability — sanitization logic duplicated with inconsistent coverage

This block (lines 92-98) sanitizes newlines + ::: + %0A/%0D. The issue-filing path (lines 131-135) sanitizes :: → empty + %0A/%0D but omits newlines and uses a different :: replacement (${TITLE//::/} vs ${TITLE//::/:}).

Both paths prevent GHA workflow command injection but diverge in behavior. A title with :: renders as foo:bar in the rejection warning but foobar in the filing output.

Suggestion: Extract into a shared block at the top of the loop, or at minimum align the two paths to use the same substitution rules.

Flagged by 3/4 agents (Claude coder, Gemini, Codex)

@fullsend-ai-retro

Copy link
Copy Markdown

Retro: PR #85 — reject "Evidence for" proposals in post-retro.sh

PR #85 adds a deterministic post-script gate to filter out retro agent proposals whose titles match evidence-for patterns (e.g., "Evidence for #1234: ..."). The change is well-implemented with comprehensive test coverage (13+ test cases), proper GHA workflow command injection sanitization, and follows the established defense-in-depth pattern used by other agent post-scripts (review severity filtering, scribe content gates).

The change addresses a real recurring problem: the retro-analysis skill explicitly instructs the agent not to file evidence-for proposals (SKILL.md line 145), but the agent ignores this frequently enough to require a post-script gate (referenced as fullsend-ai/fullsend#3881).

Limitation: GitHub API access was unavailable during this retro, so I could not trace workflow runs, review comments, or search for existing open issues. Proposals below should be checked for duplicates before filing.

Observations

  1. Defense-in-depth is the right pattern — The post-script gate catches evidence-for proposals that slip past the prompt instruction. This matches how post-review.sh handles severity filtering and post-scribe.sh handles sensitive content.
  2. Missed optimization opportunity — Evidence-for proposals caught by the post-script have already consumed the agent's token budget. Moving the check into the validation loop would give the agent a chance to rewrite proposals during the 2 allowed iterations.
  3. Instruction placement — The evidence-for prohibition lives only in skills/retro-analysis/SKILL.md. The agent definition at agents/retro.md does not mention it. Since the agent definition is loaded first and sets the agent's core constraints, reinforcing the prohibition there could improve compliance.
  4. Test coverage is excellent — False positive protection ("Fix evidence gathering bug", mid-title evidence, no issue ref) prevents the filter from being too aggressive.

Proposals filed

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

requires-manual-review Review requires human judgment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

post-retro.sh: add deterministic gate to reject "Evidence for" proposals

3 participants