Skip to content

feat(triage): effort-estimation skill and auto-promotion gate - #646

Open
rh-hemartin wants to merge 1 commit into
mainfrom
feat/2207-effort-gating
Open

feat(triage): effort-estimation skill and auto-promotion gate#646
rh-hemartin wants to merge 1 commit into
mainfrom
feat/2207-effort-gating

Conversation

@rh-hemartin

@rh-hemartin rh-hemartin commented Aug 4, 2026

Copy link
Copy Markdown
Member

Summary

  • Add effort-estimation skill that scores implementation effort on a 1-5 scale across scope, testing, domain knowledge, and risk. Issues scoring >= 4 block auto-promotion.
  • Replace requires_workflow_changes boolean with block_auto_promotion object ({blocked, reason}) in triage_summary. The post-script gates ready-to-code vs triaged for bug/documentation/performance categories.
  • Harden the post-script against workflow-command injection: ::warning:: uses a fixed message (no interpolation of untrusted content), and BLOCK_REASON is sanitized (:: collapsed to :) before any use.
  • Switch the comment field to prose-only (no fenced code blocks) to avoid model abbreviation of identifiers inside JSON strings.
  • Add eval cases 008 (high-effort multi-component) and 009 (low-effort single-file) with runnable fixture repos.

Migration: the schema replaces requires_workflow_changes with block_auto_promotion. Because triage_summary uses additionalProperties: false, all components (schema, prompt, post-script) must be deployed and rolled back together per the lockstep release model.

Closes fullsend-ai/fullsend#2207

Test plan

  • Post-triage tests pass (including injection sanitization, empty-reason fallback)
  • Schema validation tests pass
  • Both eval fixture repos run (pytest + server endpoints)
  • Eval run confirms case 008 gets triaged and 009 gets ready-to-code

🤖 Generated with Claude Code

@rh-hemartin
rh-hemartin requested a review from a team as a code owner August 4, 2026 13:04
@fullsend-ai-review

fullsend-ai-review Bot commented Aug 4, 2026

Copy link
Copy Markdown

🤖 Finished Review · ✅ Success · Started 1:05 PM UTC · Completed 1:23 PM UTC
Commit: b672253 · View workflow run →

@qodo-code-review

Copy link
Copy Markdown

PR Summary by Qodo

Add effort-estimation gating and block_auto_promotion to triage auto-promotion

✨ Enhancement 🧪 Tests 📝 Documentation ⚙️ Configuration changes 🕐 40+ Minutes

Grey Divider

AI Description

• Add an effort-estimation skill to score bug/docs/perf effort and gate auto-promotion.
• Replace requires_workflow_changes with triage_summary.block_auto_promotion {blocked, reason}.
• Update post-triage labeling/comment behavior and add schema + post-triage test coverage.
Diagram

graph TD
  A[/"GitHub Issue"/] --> B(["Triage agent"])
  B --> C[["effort-estimation skill"]] --> D[("triage-result schema")]
  B --> E["triage result JSON"] --> F(["post-triage.sh"])
  F --> G{"blocked?"} --> H["Apply triaged + append reason"] --> I(["GitHub labels/comment"])
  G --> J["Apply ready-to-code"] --> I
  K[["post-triage & schema tests"]] --> F
  L[["eval cases 008/009 fixtures"]] --> B
  subgraph Legend
    direction LR
    _ext[/"External"/] ~~~ _agent(["Agent/script"]) ~~~ _skill[["Skill/doc"]] ~~~ _schema[("Schema")]
  end
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Add numeric `effort_score` alongside `block_auto_promotion`
  • ➕ Improves observability/trending (can chart scores over time)
  • ➕ Avoids encoding the score inside a free-form reason string
  • ➕ Allows future gating policy changes without changing output shape
  • ➖ Expands schema surface area and requires more backward/forward compatibility handling
  • ➖ May tempt downstream consumers to re-implement gating logic outside the triage agent
2. Keep `requires_workflow_changes` and add a separate `block_auto_promotion`
  • ➕ Less breaking for any downstream tooling that still reads the old boolean
  • ➕ Clear separation between workflow permission limits and effort gating
  • ➖ Duplicates concepts and increases risk of conflicting fields (one blocked, one not)
  • ➖ Post-script logic becomes more complex
3. Policy-only gating in post-triage script (no skill)
  • ➕ Centralizes gating rules in one place
  • ➕ No new skill to maintain
  • ➖ Post-script has less context than the model+repo inspection
  • ➖ Harder to justify/communicate decisions in the triage comment without a model-derived rationale

Recommendation: The PR’s approach (a single generalized block_auto_promotion {blocked, reason} gate populated by an effort-estimation skill and reused for workflow-permission blocking) is the best default: it unifies all “don’t auto-dispatch yet” reasons into one contract that the post-script can apply consistently, and it ensures maintainers get an explicit rationale appended to the triage comment. If you want better long-term metrics, consider a follow-up to add an optional numeric effort_score field (with strict schema) rather than parsing it from the reason string.

Files changed (30) +1465 / -49

Enhancement (3) +118 / -19
triage.mdAdd effort-estimation skill and switch gating field to block_auto_promotion +14/-3

Add effort-estimation skill and switch gating field to block_auto_promotion

• Registers the new 'effort-estimation' skill for the triage agent and updates triage guidance to use 'triage_summary.block_auto_promotion' instead of 'requires_workflow_changes'. Adds instructions to run effort estimation for bug/documentation/performance categories and documents when/why to block auto-promotion.

agents/triage.md

post-triage.shGate ready-to-code promotion on block_auto_promotion and append reason to comment +21/-16

Gate ready-to-code promotion on block_auto_promotion and append reason to comment

• Updates post-triage logic to read 'triage_summary.block_auto_promotion.blocked/reason' and, for bug/docs/perf categories, apply 'triaged' (and emit a warning) instead of deferring 'ready-to-code' when blocked. When blocked with a reason, appends a footer to the posted triage comment.

scripts/post-triage.sh

SKILL.mdIntroduce effort-estimation skill spec and scoring rubric +83/-0

Introduce effort-estimation skill spec and scoring rubric

• Adds a new built-in skill document defining how to score effort across scope/testing/domain knowledge/risk, compute an overall score, and map it to 'block_auto_promotion' output. Includes example payloads and guidance to write the reason for maintainers.

skills/effort-estimation/SKILL.md

Tests (23) +1307 / -22
annotations.yamlAdd eval assertions for high-effort multi-component bug gating +56/-0

Add eval assertions for high-effort multi-component bug gating

• Introduces evaluation expectations requiring 'triaged' (and forbidding 'ready-to-code') for a multi-component session/auth/rate-limit bug. Encodes why the scenario should score effort >= 4 and validates that auto-promotion is blocked with an appropriate reason.

eval/triage/cases/008-effort-high-multi-component/annotations.yaml

input.yamlAdd high-effort issue fixture describing session leak + logout invalidation bug +41/-0

Add high-effort issue fixture describing session leak + logout invalidation bug

• Adds a GitHub-issue fixture describing a cross-cutting session memory leak and logout security flaw, including references to impacted components and reproduction/impact details. Designed to drive the triage agent toward a high effort score and auto-promotion block.

eval/triage/cases/008-effort-high-multi-component/input.yaml

README.mdAdd runnable fixture repo README for multi-component auth service +31/-0

Add runnable fixture repo README for multi-component auth service

• Provides a minimal architecture/run guide for the eval repo used in case 008, including environment variables and component layout to support reproducible triage verification.

eval/triage/cases/008-effort-high-multi-component/repo/README.md

app.pyAdd minimal HTTP server fixture wiring auth, users API, and rate limiting +110/-0

Add minimal HTTP server fixture wiring auth, users API, and rate limiting

• Implements a simple HTTP server router used by the eval case to exercise login/logout/session endpoints and user CRUD endpoints, with rate limiting applied per request.

eval/triage/cases/008-effort-high-multi-component/repo/app.py

users.pyAdd authenticated user CRUD handlers for eval repo +65/-0

Add authenticated user CRUD handlers for eval repo

• Defines authenticated endpoint handlers for listing, fetching, updating, and deleting users. Uses the auth middleware decorator and in-memory DB layer to create multi-module touch points for the effort-estimation scenario.

eval/triage/cases/008-effort-high-multi-component/repo/src/api/users.py

session.pyAdd in-memory session store with lazy TTL eviction (intentional leak surface) +77/-0

Add in-memory session store with lazy TTL eviction (intentional leak surface)

• Implements session creation/lookup/refresh on a global dict with TTL expiration checked only on lookup, explicitly documenting that expired sessions remain until accessed. This forms the core of the case’s memory leak and risk analysis surface.

eval/triage/cases/008-effort-high-multi-component/repo/src/auth/session.py

validators.pyAdd email/password validation helpers for eval repo +39/-0

Add email/password validation helpers for eval repo

• Introduces basic email and password validators that raise typed exceptions, supporting the login flow and providing realistic code for triage inspection.

eval/triage/cases/008-effort-high-multi-component/repo/src/auth/validators.py

views.pyAdd login/logout/session views with intentional logout invalidation bug +60/-0

Add login/logout/session views with intentional logout invalidation bug

• Implements login and session status endpoints and includes an intentionally incomplete logout handler that does not invalidate sessions. This drives the ‘multi-component’ and ‘security risk’ aspects of the high-effort triage case.

eval/triage/cases/008-effort-high-multi-component/repo/src/auth/views.py

users.pyAdd in-memory user DB layer for eval repo +75/-0

Add in-memory user DB layer for eval repo

• Provides a small in-memory user store and CRUD-like operations to support the API module and enable cross-package interactions for triage analysis.

eval/triage/cases/008-effort-high-multi-component/repo/src/db/users.py

auth.pyAdd auth middleware decorator using session token validation +36/-0

Add auth middleware decorator using session token validation

• Implements an authentication decorator that validates Bearer tokens via the session store and refreshes session activity. Contributes to the cross-cutting set of modules implicated by the case 008 bug report.

eval/triage/cases/008-effort-high-multi-component/repo/src/middleware/auth.py

rate_limit.pyAdd rate limiter with lazy cleanup note mirroring session behavior +57/-0

Add rate limiter with lazy cleanup note mirroring session behavior

• Implements an in-memory per-IP rate limiter and documents that entries persist for inactive IPs, paralleling the session store’s lazy eviction pattern. This supports the case’s claim that a similar accumulation issue exists in rate limiting.

eval/triage/cases/008-effort-high-multi-component/repo/src/middleware/rate_limit.py

test_db.pyAdd unit tests for in-memory users DB fixture +64/-0

Add unit tests for in-memory users DB fixture

• Adds pytest coverage for user DB functions (get/list/update/delete) to keep the fixture repo runnable and credible for triage inspection.

eval/triage/cases/008-effort-high-multi-component/repo/tests/test_db.py

test_validators.pyAdd validator unit tests for auth fixture +44/-0

Add validator unit tests for auth fixture

• Adds pytest coverage for email/password validators, providing baseline tests while still leaving the targeted session lifecycle gap that the case highlights.

eval/triage/cases/008-effort-high-multi-component/repo/tests/test_validators.py

annotations.yamlAdd eval assertions for low-effort single-file bug auto-promotion +55/-0

Add eval assertions for low-effort single-file bug auto-promotion

• Introduces evaluation expectations requiring 'ready-to-code' (and forbidding 'triaged') for a trivial username-regex bug. Encodes why effort should be < 4 and what verification the triage should perform.

eval/triage/cases/009-effort-low-single-file/annotations.yaml

input.yamlAdd low-effort issue fixture for two-character username rejection +34/-0

Add low-effort issue fixture for two-character username rejection

• Adds a GitHub-issue fixture describing a minimal regex constraint bug with a clear suggested one-character fix. Designed to drive the triage agent toward allowing auto-promotion.

eval/triage/cases/009-effort-low-single-file/input.yaml

README.mdAdd runnable fixture repo README for low-effort auth service +25/-0

Add runnable fixture repo README for low-effort auth service

• Provides basic run/test instructions and endpoint list for the eval repo used in case 009.

eval/triage/cases/009-effort-low-single-file/repo/README.md

app.pyAdd minimal HTTP server fixture for login/logout/register endpoints +58/-0

Add minimal HTTP server fixture for login/logout/register endpoints

• Implements a simple HTTP server router to make the eval repo runnable and to provide realistic context around input validation behavior.

eval/triage/cases/009-effort-low-single-file/repo/app.py

validators.pyAdd validators with username regex that enforces 3–30 chars (bug surface) +57/-0

Add validators with username regex that enforces 3–30 chars (bug surface)

• Defines email/password/username validators and sets 'USERNAME_PATTERN' to require a 3-character minimum via '{2,29}' after the initial letter. The mismatch with the expected 2-character minimum is the intentional bug for case 009.

eval/triage/cases/009-effort-low-single-file/repo/src/auth/validators.py

views.pyAdd login/logout/register views using validators +61/-0

Add login/logout/register views using validators

• Implements handler logic that invokes the validators (including username validation during registration), providing the call path that makes the regex issue user-visible.

eval/triage/cases/009-effort-low-single-file/repo/src/auth/views.py

test_validators.pyAdd validator tests including 3-char username boundary (intended to be updated by fix) +105/-0

Add validator tests including 3-char username boundary (intended to be updated by fix)

• Adds pytest coverage for email/password/username validation, including boundaries at 3 and 30 characters and a failing-too-short case at 2 chars. Serves as existing coverage that makes the bug low-effort to fix.

eval/triage/cases/009-effort-low-single-file/repo/tests/test_validators.py

test_views.pyAdd view handler unit tests for login and registration flows +88/-0

Add view handler unit tests for login and registration flows

• Adds tests for missing-field behavior and basic valid/invalid registration logic, ensuring the fixture repo is runnable and that validation errors surface through handlers.

eval/triage/cases/009-effort-low-single-file/repo/tests/test_views.py

post-triage-test.shExpand post-triage automation tests for block_auto_promotion gating +47/-22

Expand post-triage automation tests for block_auto_promotion gating

• Replaces workflow-change-only tests with broader auto-promotion blocking coverage: blocked vs unblocked behavior, label application differences, warning output, and verifying that the blocked reason footer is appended for bug/docs/perf but not for feature issues.

scripts/post-triage-test.sh

validate-output-schema-test.shAdd schema validation tests for block_auto_promotion required fields/shape +22/-0

Add schema validation tests for block_auto_promotion required fields/shape

• Adds positive and negative schema tests ensuring 'block_auto_promotion' requires both 'blocked' and 'reason' and rejects unknown extra fields.

scripts/validate-output-schema-test.sh

Documentation (2) +30 / -7
code.mdUpdate ready-to-code label semantics to reflect auto-promotion blocking +1/-1

Update ready-to-code label semantics to reflect auto-promotion blocking

• Updates the 'ready-to-code' label description to reflect that it is applied by the triage post-script when auto-promotion is not blocked, and is otherwise applied manually after human review (e.g., high-effort issues or workflow changes).

docs/code.md

triage.mdDocument effort-estimation skill and block_auto_promotion-driven triage behavior +29/-6

Document effort-estimation skill and block_auto_promotion-driven triage behavior

• Updates label meaning descriptions to reflect that bugs/docs can be either 'ready-to-code' or 'triaged' depending on 'block_auto_promotion'. Adds a new section documenting the 'effort-estimation' skill, its scoring dimensions/threshold, and how repositories can override it with a local skill implementation.

docs/triage.md

Other (2) +10 / -1
triage.yamlRegister effort-estimation skill in triage harness configuration +1/-0

Register effort-estimation skill in triage harness configuration

• Adds 'skills/effort-estimation' to the triage harness so the skill is available during triage runs and evaluations.

harness/triage.yaml

triage-result.schema.jsonReplace requires_workflow_changes with block_auto_promotion object in schema +9/-1

Replace requires_workflow_changes with block_auto_promotion object in schema

• Updates the triage result JSON schema to require 'triage_summary.block_auto_promotion' (when present) to include both 'blocked' and 'reason', and disallows additional properties. Removes the legacy 'requires_workflow_changes' boolean from the schema.

schemas/triage-result.schema.json

@qodo-code-review

qodo-code-review Bot commented Aug 4, 2026

Copy link
Copy Markdown

Code Review by Qodo

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

Grey Divider


Action required

1. BLOCK_REASON unsanitized in ::warning:: ✓ Resolved 📜 Skill insight ⛨ Security
Description
scripts/post-triage.sh interpolates the agent-produced/untrusted BLOCK_REASON directly into a
GitHub Actions ::warning:: workflow command, allowing workflow-command injection via ::,
newlines, or control characters that can create additional annotations or disrupt runner command
parsing. This violates PR Compliance ID 1538382’s requirement to sanitize every interpolated
workflow-command value individually.
Code

scripts/post-triage.sh[398]

+      echo "::warning::Skipping ready-to-code — auto-promotion blocked: ${BLOCK_REASON}"
Relevance

●●● Strong

Strong recent precedent: sanitize untrusted values before embedding in GitHub Actions workflow
commands to prevent injection.

PR-#573

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
PR Compliance ID 1538382 requires that all variables interpolated into GitHub Actions workflow
commands be sanitized individually, yet the script reads block_auto_promotion.reason from the
triage/agent result JSON into BLOCK_REASON and then embeds it directly into an `echo
"::warning::...${BLOCK_REASON}" line with no escaping. Because ::warning::` lines are parsed
specially by GitHub Actions runners, any unescaped newlines, :: sequences, or other
special/control characters in BLOCK_REASON can be interpreted as additional workflow
commands/annotations or otherwise corrupt workflow-command parsing.

scripts/post-triage.sh[392-399]
scripts/post-triage.sh[382-407]
harness/triage.yaml[24-29]
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-triage.sh` emits a GitHub Actions workflow command (`::warning::...`) that interpolates `BLOCK_REASON` without sanitization. Because `BLOCK_REASON` originates from the triage agent output (and can be influenced by issue content), it must be sanitized (or removed from the workflow command) to prevent GitHub Actions workflow-command/log injection.

## Issue Context
- `BLOCK_REASON` is extracted from the triage result JSON (agent-produced/untrusted text).
- The value is embedded in an `::warning::` line, which is a special GitHub Actions workflow-command channel; untrusted content inside `::warning::...` can be interpreted as additional workflow commands if it contains `::`, `%0A/%0D`, raw `\n/\r`, control characters, or ANSI escape sequences.
- This is a workflow-command/log injection risk (not shell execution) and violates PR Compliance ID 1538382’s requirement that each interpolated workflow-command value be sanitized individually.
- Acceptable approaches include either not including the reason inside the workflow command (emit it as a separate plain log line), or escaping the value for GitHub Actions command format (at minimum `%` -> `%25`, `\r` -> `%0D`, `\n` -> `%0A`) before interpolation.
- Optionally add a regression test in `scripts/post-triage-test.sh` using a reason containing a newline and a `::warning::`-like prefix to ensure it cannot produce a second workflow command.

## Fix Focus Areas
- scripts/post-triage.sh[392-399]

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


2. Protected paths modified in PR 📜 Skill insight § Compliance
Description
This PR modifies protected governance/infrastructure paths (e.g., agents/, scripts/, harness/,
schemas/, skills/), which must not be auto-approved and requires human review. Even with the
linked issue (#2207), a protected-path finding is required.
Code

skills/effort-estimation/SKILL.md[R1-4]

+---
+name: effort-estimation
+description: >-
+  Score implementation effort for triaged issues and decide whether to block
Relevance

●● Moderate

Protected-path governance concerns appear in reviews, but precedent is mixed on how
strictly/explicitly it’s enforced.

PR-#631
PR-#476

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
PR Compliance ID 1538392 requires raising a finding whenever protected paths are modified. The PR
adds/changes files under protected directories including skills/ and scripts/ (and others), so
this PR must receive human approval and cannot be auto-approved.

skills/effort-estimation/SKILL.md[1-7]
scripts/post-triage.sh[387-406]
Skill: pr-review



Informational

3. Missing block reason silent ✓ Resolved 🐞 Bug ☼ Reliability
Description
If block_auto_promotion.blocked is true but block_auto_promotion.reason is missing/empty,
post-triage.sh still applies triaged but adds no explanation because it only appends the footer
when BLOCK_REASON is non-empty. The script explicitly warns it can’t fully trust schema validation,
so this can occur in best-effort outputs and leaves maintainers without the blocking rationale.
Code

scripts/post-triage.sh[393]

+    BLOCK_REASON=$(jq -r '.triage_summary.block_auto_promotion.reason // empty' "${RESULT_FILE}")
Relevance

●●● Strong

Deterministic reliability fix; repo has precedent for best-effort jq parsing when fields may be
missing/null.

PR-#284

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The script extracts reason with // empty and only appends the explanatory footer when it is
non-empty; separately, the script documents that validation may not have gated malformed output,
making this path plausible.

scripts/post-triage.sh[271-276]
scripts/post-triage.sh[392-407]

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

### Issue description
When auto-promotion is blocked, `scripts/post-triage.sh` may end up with an empty `${BLOCK_REASON}` (e.g., best-effort agent output after failed validations). In that case the script blocks promotion but does not append any explanation to the issue comment.

### Issue Context
The script notes it cannot fully trust that schema validation prevented malformed output, so defensive handling should produce a human-readable reason even when missing.

### Fix Focus Areas
- scripts/post-triage.sh[271-276]
- scripts/post-triage.sh[392-407]

### Suggested fix
When `${BLOCKED} == true` for bug/documentation/performance:
- If `${BLOCK_REASON}` is empty, set a fallback reason (e.g., `"Auto-promotion blocked, but triage did not provide a reason."`).
- Append the fallback to the comment (and optionally emit a separate plain-log warning).

Optionally add a `scripts/post-triage-test.sh` case where `blocked: true` but `reason` is missing/empty and assert the comment contains the fallback footer.

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


Grey Divider

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

To customize comments, go to the Qodo configuration screen, or learn more in the docs.

Qodo Logo

Comment thread scripts/post-triage.sh Outdated
Comment thread skills/effort-estimation/SKILL.md
Comment thread scripts/post-triage.sh
@fullsend-ai-review

fullsend-ai-review Bot commented Aug 4, 2026

Copy link
Copy Markdown

Review

Findings

Medium

  • [logic-error] scripts/post-triage.sh:64 — The fenced code block stripping uses sed '/^```/d' which only deletes the fence delimiter lines (e.g., ```bash and ```) but preserves all code content lines between them. After stripping, raw code appears as unformatted prose in the posted GitHub comment. The test comment claims this "mirrors post-scribe.sh enforcement," but post-scribe.sh uses gate_reject to reject the entire item when code blocks are detected — it does not attempt partial stripping.
    Remediation: Replace sed '/^```/d' with sed '/^```/,/^```/d' to strip the full fenced code block including its content, or update the warning message to accurately describe the behavior.

  • [protected-path] agents/triage.md, harness/triage.yaml, scripts/post-triage.sh, scripts/post-triage-test.sh, scripts/validate-output-schema-test.sh, skills/effort-estimation/SKILL.md — This PR modifies 6 files under protected paths (agents/, harness/, scripts/, skills/). The linked issue (dispatch: no effort-based gating before auto-promoting issues to coder fullsend#2207) and PR description explain the rationale for the changes. Human approval is required for all protected-path changes regardless of justification.

Low

  • [injection-vuln] scripts/post-triage.sh:401 — The prior finding (triple-colon bypass of :: sanitization) is now fixed. The new while loop iterates until no :: sequences remain, correctly collapsing :::error::: to :error:. BLOCK_REASON never reaches the GHA log — it is only embedded in COMMENT via printf '%s'. Residual risk is negligible.

  • [scope-creep] agents/triage.md — The instruction discouraging fenced code blocks in triage comments is unrelated to the effort-estimation feature authorized by issue #2207.

  • [schema-migration] schemas/triage-result.schema.json:155 — Removing requires_workflow_changes and adding block_auto_promotion is a breaking schema change. All three components (schema, prompt, post-script) are updated atomically in this PR, and the PR body documents the lockstep deployment requirement per AGENTS.md section 6.

  • [punctuation-consistency] agents/triage.md:157 — The diff changes em-dash (—) to double-hyphen (--) in one location, inconsistent with the em-dash style used elsewhere in the same file.

  • [punctuation-consistency] skills/effort-estimation/SKILL.md:22 — The range notation 1--5 uses a double-hyphen, inconsistent with the single-hyphen range convention used across all other skill and agent files.

Previous run

Review

Findings

Medium

  • [protected-path] agents/triage.md, harness/triage.yaml, scripts/post-triage.sh, scripts/post-triage-test.sh, scripts/validate-output-schema-test.sh, skills/effort-estimation/SKILL.md — This PR modifies 6 files under protected paths (agents/, harness/, scripts/, skills/). The linked issue (dispatch: no effort-based gating before auto-promoting issues to coder fullsend#2207) and PR description explain the rationale for the changes. Human approval is required for all protected-path changes regardless of justification.

Low

  • [API-contract-contradiction] schemas/triage-result.schema.json:156 — The schema description says "Only include when blocking; omit entirely when effort is low enough for auto-promotion" but the effort-estimation SKILL.md and triage.md both instruct the agent to emit block_auto_promotion even with blocked: false. Runtime behavior is unaffected since the post-script handles both cases via jq // false, but the contradictory guidance may cause inconsistent agent output.

  • [injection-vuln] scripts/post-triage.sh:395 — The :: collapsing sanitization (${BLOCK_REASON//::/:}) is bypassable with triple colons: :::error::: becomes ::error:: after substitution. Not currently exploitable because BLOCK_REASON is never echoed to the GHA log — it is only embedded in COMMENT which is piped to tools via printf.
    Remediation: Consider collapsing runs of 2+ colons to a single colon via sed 's/::*/:/g'.

  • [scope-creep] agents/triage.md — The instruction discouraging fenced code blocks in triage comments is unrelated to the effort-estimation feature authorized by issue #2207.

  • [unauthorized-refactoring] schemas/triage-result.schema.json:155 — The PR replaces requires_workflow_changes with block_auto_promotion, unifying workflow-change detection (Triage should detect likely GHA workflow modifications and skip automatic code agent #325) and effort estimation (#2207) under one structure. The unification is documented in the PR body and is a reasonable engineering choice, but extends beyond the issue's explicit authorization.

  • [schema-migration] schemas/triage-result.schema.json:155 — Removing requires_workflow_changes and adding block_auto_promotion is a breaking schema change. All three components (schema, prompt, post-script) are updated atomically in this PR, and the PR body documents the lockstep deployment requirement per AGENTS.md section 6.

  • [punctuation-consistency] agents/triage.md — The diff changes em-dash (—) to double-hyphen (--) in one location, inconsistent with the em-dash style used elsewhere in the same file.

  • [test-adequacy] scripts/validate-output-schema-test.sh — No schema validation test covers block_auto_promotion with an empty reason string ("reason": ""). The schema enforces minLength: 1 on reason; adding a test (expecting rejection) would confirm the constraint works.

Previous run (2)

Review

Findings

High

  • [GHA workflow command injection] scripts/post-triage.sh:398BLOCK_REASON is extracted from untrusted agent JSON via jq -r and interpolated directly into a GHA workflow command: echo "::warning::Skipping ready-to-code — auto-promotion blocked: ${BLOCK_REASON}". The jq -r flag decodes JSON escape sequences into literal characters, so a reason containing \n::stop-commands::token or \n::error::injected would produce multiple output lines that GitHub Actions interprets as separate workflow commands. This is the first unconstrained free-text string in this script that is interpolated into a workflow command — all prior uses (LA_LABEL, TARGET_REPO) are either character-validated or schema-constrained.
    Remediation: Sanitize BLOCK_REASON before interpolating into the ::warning:: command. Either strip newlines (BLOCK_REASON_SAFE=$(printf '%s' "${BLOCK_REASON}" | tr '\n' ' ' | sed 's/%/%25/g')) or use a fixed message without interpolation: echo "::warning::Skipping ready-to-code — auto-promotion blocked (see comment for details)".

Medium

  • [backward-compatibility] schemas/triage-result.schema.json:155 — Removing requires_workflow_changes from triage_summary (which has additionalProperties: false) means any existing triage agent output containing this field will fail schema validation. The lockstep versioning model (AGENTS.md section 6) mitigates this for normal deployments since the schema, agent prompt, and post-script deploy atomically. However, version skew during rollback or partial deployment would cause fail-open behavior: old agent results with requires_workflow_changes would pass through without blocking auto-promotion.
    Remediation: Verify all components are deployed atomically in the same release tag. Consider documenting the migration in the PR description for operators who may need to roll back.

  • [edge-case] schemas/triage-result.schema.json:155 — The block_auto_promotion object requires both blocked and reason (with minLength: 1) whenever present, but the object itself is optional on triage_summary. This means blocked: false + reason is a valid but practically unreachable state — the skill would simply omit the field when not blocking rather than emit blocked: false with a forced reason. The blocked boolean becomes redundant (presence of the object implies blocking).
    Remediation: Either (a) make reason required only when blocked: true via a conditional schema (if/then), or (b) document the convention that the field should only be present when blocking.

  • [protected-path] agents/triage.md, harness/triage.yaml, scripts/post-triage.sh, scripts/post-triage-test.sh, scripts/validate-output-schema-test.sh, skills/effort-estimation/SKILL.md — This PR modifies 6 files under protected paths (agents/, harness/, scripts/, skills/). The linked issue (dispatch: no effort-based gating before auto-promoting issues to coder fullsend#2207) and PR description provide context for the changes. Human approval is required for all protected-path changes regardless of justification.

Low

  • [test-adequacy] scripts/post-triage-test.sh — No test covers block_auto_promotion present with blocked: false and a reason string. While blocked: false is functionally equivalent to omitting the field (both fall through via jq // false), a test for this state would guard against regressions in the bash string comparison logic.

  • [schema-validation-gap] schemas/triage-result.schema.json:155 — The reason field has minLength: 1 but no maxLength. The top-level comment field has maxLength: 16384 for defense-in-depth. Adding a similar bound (e.g., maxLength: 1024) to reason would prevent unnecessarily large ::warning:: annotations and maintain consistency.


Labels: PR modifies triage agent instructions, post-script, skill, and eval cases.


Next steps:

  • /fs-fix — agent addresses review findings automatically
  • /fs-fix <your instruction> — agent fixes with your specific guidance
  • Push commits directly — review re-runs automatically on push
  • /fs-fix-stop — disable automatic fix runs for this PR

fullsend-ai-review[bot]

This comment was marked as outdated.

@rh-hemartin rh-hemartin self-assigned this Aug 4, 2026
@rh-hemartin
rh-hemartin force-pushed the feat/2207-effort-gating branch from b672253 to 3d72bb1 Compare August 4, 2026 14:17
@fullsend-ai-review

fullsend-ai-review Bot commented Aug 4, 2026

Copy link
Copy Markdown

🤖 Review · ⚠️ Cancelled · Started 2:18 PM UTC · Ended 2:31 PM UTC
Commit: 3d72bb1 · View workflow run →

@rh-hemartin
rh-hemartin force-pushed the feat/2207-effort-gating branch from 3d72bb1 to 2686001 Compare August 4, 2026 14:30
@fullsend-ai-review

fullsend-ai-review Bot commented Aug 4, 2026

Copy link
Copy Markdown

🤖 Review · ⚠️ Cancelled · Started 2:32 PM UTC · Ended 2:45 PM UTC
Commit: 2686001 · View workflow run →

@rh-hemartin
rh-hemartin force-pushed the feat/2207-effort-gating branch from 2686001 to 743fabd Compare August 4, 2026 14:44
@fullsend-ai-review

fullsend-ai-review Bot commented Aug 4, 2026

Copy link
Copy Markdown

🤖 Review · ⚠️ Cancelled · Started 2:45 PM UTC · Ended 2:59 PM UTC
Commit: 743fabd · View workflow run →

@rh-hemartin
rh-hemartin force-pushed the feat/2207-effort-gating branch from 743fabd to f8405d6 Compare August 4, 2026 14:58
@fullsend-ai-review

fullsend-ai-review Bot commented Aug 4, 2026

Copy link
Copy Markdown

🤖 Finished Review · ✅ Success · Started 3:00 PM UTC · Completed 3:16 PM UTC
Commit: f8405d6 · View workflow run →

@fullsend-ai-review
fullsend-ai-review Bot dismissed their stale review August 4, 2026 15:16

Superseded by updated review

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 Aug 4, 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.

[MEDIUM] Effort-estimation threshold and traceability are unverified before merge (PR description / skills/effort-estimation/SKILL.md)

Three compounding gaps:

  1. The PR body says "Closes #2207", but repos/fullsend-ai/agents/issues/2207 returns 404 — issue #2207 does not exist in this repo. The actual issue is fullsend-ai/fullsend#2207 ("dispatch: no effort-based gating before auto-promoting issues to coder"), which is open. GitHub's auto-close syntax only closes same-repo issues unless the owner/repo#number form is used, so merging as-is will not close the tracked issue, and the entire feature (the four scoring dimensions, 1-5 scale, and the specific ">=4 blocks" threshold) is only traceable to a cross-repo issue that the PR text doesn't correctly reference.

  2. There is no cited derivation (historical data, prior incidents) for the ">=4 average blocks" threshold in skills/effort-estimation/SKILL.md; it reads as a picked-because-it-feels-right number, and no eval case near the boundary (e.g. ~3.5-4.5) exists to confirm the cutoff behaves as intended — the two added fixtures (eval/triage/cases/008, 009) are deliberately constructed extremes.

  3. The PR's own test plan leaves unchecked: "Eval run confirms case 008 gets triaged and 009 gets ready-to-code" — meaning the one piece of evidence that would validate the threshold against a real triage agent run has not been collected, and this is shipped as final production gating logic rather than as an explicitly-flagged, tunable heuristic pending that verification.

Suggestion: update the PR description to "Closes fullsend-ai/fullsend#2207" so the cross-repo issue actually closes on merge. Before merging the gating logic as final, run the eval harness on cases 008/009 (and ideally a boundary case around effort ~4.0) and record actual pass/fail, or explicitly mark the threshold as a tunable default subject to recalibration pending that evidence.

Comment thread docs/triage.md Outdated
Comment thread agents/triage.md
@rh-hemartin

rh-hemartin commented Aug 5, 2026

Copy link
Copy Markdown
Member Author

About the compounding gaps:

Fixed the PR body, the decision of using 1 to 5 has been raised in internal communications, not sure there is a reference and I don't think we need a middle case, it will bring flakyness, let's see how this goes; and the test plan has been checked.

Add an effort-estimation skill that scores issues on four dimensions
(1-5 each). An average >= 4 blocks auto-promotion, keeping the issue
at triaged for human review.

Replace requires_workflow_changes with block_auto_promotion ({blocked,
reason}) in triage_summary and the post-script gate. Harden the
post-script against workflow-command injection.

Add eval cases 008 and 009 with runnable fixture repos.

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 feat/2207-effort-gating branch from f8405d6 to 6423d80 Compare August 5, 2026 06:31
@fullsend-ai-review

fullsend-ai-review Bot commented Aug 5, 2026

Copy link
Copy Markdown

🤖 Finished Review · ✅ Success · Started 6:32 AM UTC · Completed 6:51 AM UTC
Commit: 6423d80 · View workflow run →

@fullsend-ai-review fullsend-ai-review Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

See the review comment for full details.

Comment thread scripts/post-triage.sh

if echo "${COMMENT}" | grep -q '```'; then
echo "::warning::Stripping fenced code blocks from triage comment"
COMMENT=$(echo "${COMMENT}" | sed '/^```/d')

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[medium] logic-error

The fenced code block stripping uses sed '/^```/d' which only deletes the fence delimiter lines but preserves all code content lines between them. After stripping, raw code appears as unformatted prose in the posted GitHub comment. The test comment claims this 'mirrors post-scribe.sh enforcement,' but post-scribe.sh uses gate_reject to reject the entire item when code blocks are detected - it does not attempt partial stripping.

Suggested fix: Replace sed '/^/d' with sed '/^/,/^```/d' to strip the full fenced code block including its content, or update the warning message to accurately describe the behavior.

Comment thread scripts/post-triage.sh
BLOCK_REASON=$(jq -r '.triage_summary.block_auto_promotion.reason // empty' "${RESULT_FILE}")
# Collapse runs of 2+ colons to a single colon so untrusted text
# can never form a GHA workflow command (e.g., ::error::).
while [[ "${BLOCK_REASON}" == *::* ]]; do

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[low] injection-vuln

The prior finding (triple-colon bypass of :: sanitization) is now fixed. The new while loop iterates until no :: sequences remain, correctly collapsing :::error::: to :error:. BLOCK_REASON never reaches the GHA log - it is only embedded in COMMENT via printf. Residual risk is negligible.

Comment thread schemas/triage-result.schema.json
Comment thread agents/triage.md
Comment thread skills/effort-estimation/SKILL.md

@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-only pass (inline comments added below). One additional finding on unchanged code that isn't part of this PR's diff, so noting it here instead of inline:

[HIGH] Auto-promotion gate is undermined by the unconditional /fs-code footer appended to the same comment (scripts/post-triage.sh, the "Append action-hints footer (sufficient only)" block, ~line 558)

When block_auto_promotion.blocked is true and category is bug/documentation/performance, the script appends "Auto-promotion blocked: <reason>". But the unconditional "Next steps" footer, gated only on ACTION == "sufficient" with no check of AUTO_PROMOTION_BLOCKED, still appends "/fs-code — agent creates a PR to implement this issue". docs/code.md confirms /fs-code triggers the code agent independent of label state ("triggered when the ready-to-code label is applied... or via the /fs-code command"). The result is a single comment that both explains why the issue was held for review and hands the reader a working one-click bypass of that exact hold.

Suggested fix: Suppress or reword the Next-steps footer when AUTO_PROMOTION_BLOCKED is true, e.g. "This issue was held for review — run /fs-code only after confirming the concerns above."

"recommended_fix": { "type": "string", "minLength": 1 },
"proposed_test_case": { "type": "string", "minLength": 1 },
"requires_workflow_changes": { "type": "boolean" }
"block_auto_promotion": {

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.

[HIGH] Schema does not require block_auto_promotion for the categories the gate is meant to cover

block_auto_promotion is entirely optional inside $defs.triage_summary (not in the required array a few lines up), and unlike the top-level allOf block elsewhere in this schema which conditionally requires fields based on action, there is no conditional requiring block_auto_promotion when category is bug/documentation/performance. scripts/post-triage.sh reads it as jq -r '.triage_summary.block_auto_promotion.blocked // false', so if the model omits the field entirely (prompt drift, rushed run, or issue-content injection), the omission is indistinguishable from an explicit blocked: false and the issue silently auto-promotes to ready-to-code with no schema-validation error and no warning — exactly the scenario the gate exists to prevent. This is distinct from the existing "edge-case" thread on this object, which only discusses blocked/reason being required within the object once present, not the object's own presence being unenforced for the categories the gate targets.

Suggested fix: Add a conditional requirement (mirroring the existing top-level allOf pattern) inside $defs.triage_summary: when category is bug/documentation/performance, require block_auto_promotion to be present. This turns a silent gate bypass into a hard schema-validation error.

@@ -0,0 +1,83 @@
---

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] New skill introduces a second, unreconciled "Effort" scoring scheme instead of reusing/aligning with the prioritize agent's existing RICE Effort dimension

fullsend-ai/fullsend#2207 (the issue this PR closes) explicitly names existing infrastructure to build on — both the issue body ("The prioritize agent already computes an Effort dimension via RICE") and the prioritize agent's own RICE comment on that issue ("Effort: 1 — The RICE prioritize agent already computes an Effort dimension, so the core estimation logic exists"). Instead, this skill defines an independent 4-dimension (scope/testing/domain/risk), 1-5, averaged rubric with a hardcoded >=4 block threshold. agents/prioritize.md shows the existing RICE Effort dimension uses a 0.25-3 scale with inverse semantics (higher effort = lower priority, used as a denominator). Neither the PR body, docs/triage.md, nor this file explains why the existing dimension wasn't reused or how the two same-named "Effort" concepts (different scales, different semantics, no cross-reference) are meant to coexist.

Suggested fix: Add a short note explaining why an independent scale was introduced (e.g., prioritize runs on a schedule, decoupled from synchronous triage), or align this rubric's scale/semantics with the prioritize agent's Effort definition.

4. High regression risk, needs careful rollout
5. Breaking change affecting downstream consumers

Compute the overall effort as the average of the four dimensions, rounded

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] Averaging the four effort dimensions lets a single maximal-risk dimension slip through the gate undetected

Step 3 blocks only when the straight average of scope/testing/domain/risk is >= 4. An issue scoring Scope=1, Testing=1, Domain=1 but Risk=5 ("Breaking change affecting downstream consumers", the top of the Risk rubric above) averages to 2.0 and is NOT blocked — a change the rubric itself flags as breaking for downstream consumers would still auto-promote unattended. Both new eval fixtures (008 and 009) score uniformly across all four dimensions, so this dilution failure mode — the exact scenario the gate exists to catch — is completely untested by this PR's own eval suite.

Suggested fix: Use max() or a rule that any single dimension >= 4 forces blocking, rather than a straight average. Add an eval case with a mixed profile (one dimension at 5, others at 1) to confirm intended blocking behavior.

auth middleware, rate_limit.py) — 4.
- Testing: no existing session lifecycle tests; needs new test
infrastructure for time-dependent behavior (mocking time.time) — 4.
- Domain knowledge: requires understanding session token security

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] Domain-knowledge annotation score contradicts the skill's own rubric, making this fixture potentially flaky at the >=4 threshold

This annotation scores "Domain knowledge... coordinating invalidation across the session store, auth middleware, and rate limiter" as a 4. But skills/effort-estimation/SKILL.md defines domain-knowledge level 4 as "Requires cross-repo or external API knowledge" and level 3 as "Requires understanding subsystem design". The described scenario (session store + auth middleware + rate limiter, all confirmed single-repo/in-process components per this fixture's own description) matches level 3, not 4. Recomputing with domain=3 instead of 4 (scope=4, testing=4, domain=3, risk=4) gives (4+4+3+4)/4 = 3.75, rounding to 3.8 per the skill's "rounded to one decimal place" rule — below the >=4 blocking threshold. A model faithfully applying the documented rubric could legitimately set blocked: false, contradicting this fixture's required triaged label and forbidden ready-to-code label.

Suggested fix: Either revise the justification to genuinely require cross-repo/external-API knowledge (matching level 4), or lower the expected score to 3 and re-verify the threshold math with the other three dimensions still yields >=4.

Comment thread agents/triage.md
```

**Workflow change detection (optional):** If the issue likely requires modifying GitHub Actions workflow files (`.github/workflows/`, `.fullsend/.github/workflows/`, or enrolled-repo shim workflows), set `requires_workflow_changes: true` in `triage_summary`. When set, the post-triage script skips auto-triggering the code agent because the code agent cannot modify workflow files under current permissions. The triage comment should warn about this limitation and note that manual intervention is required. When `requires_workflow_changes` is not set or is `false`, auto-triggering proceeds normally.
**Blocking auto-promotion:** Use the `block_auto_promotion` field in `triage_summary` to prevent the post-triage script from auto-promoting the issue to the code agent. Set `blocked: true` with a `reason` when:

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] "Blocking auto-promotion" section overstates block_auto_promotion's effect versus what post-triage.sh actually implements

This line tells the model it can set blocked: true for "any other condition where auto-dispatch would be premature" (not just effort/workflow-changes) and states unqualified that "the post-script applies triaged instead of ready-to-code and appends the reason to the triage comment." In reality, scripts/post-triage.sh only honors block_auto_promotion inside a gate requiring CATEGORY to be bug/documentation/performance — for feature, security, or other categories the flag is silently ignored and the "Auto-promotion blocked:" footer is never appended. A maintainer or future contributor reading only this section would reasonably expect the reason to always surface when blocked: true, which is false for three of six category values.

Suggested fix: Qualify the doc to state explicitly that the reason-append/triaged-override behavior only applies to bug/documentation/performance categories, matching the CATEGORY guard in post-triage.sh.

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 triage-agent

Projects

None yet

Development

Successfully merging this pull request may close these issues.

dispatch: no effort-based gating before auto-promoting issues to coder

2 participants