| name | siae-fix-evidence |
| description | Use when review-evidence v2 hook ha emesso BLOCK_REGRESSION e vuoi tentare un auto-fix loop hook-driven. Skill composer che legge `block_reasons` atomici e dispatcha `siae-tdd` o `siae-code-standards` via Skill tool fino ad AUTO_APPROVE (max 5 iter, token budget 200k, oscillation guard). Trigger: /forge-fix-evidence, "auto-fix evidence", "rimedio automatico", "fixa block_regression", "loop di remediation", BLOCK_REGRESSION ricevuto.
|
| allowed-tools | Read, Bash, Skill |
╔══════════════════════════════════════════════════════════════════╗
║ SIAE Fix Evidence — Auto-Fix Loop ║
║ hook-driven remediation composer ║
╚══════════════════════════════════════════════════════════════════╝
siae-fix-evidence — Auto-Fix Loop hook-driven
Tipo: Rigid | Fase SDLC: 4. Implementation (auto-remediation)
Skill composer: legge .claude/review-evidence/<sha>.json e auto-dispatcha
sub-skill DevForge (siae-tdd, siae-code-standards) fino ad
AUTO_APPROVE o escalation. NON modifica file direttamente — invoca
solo sub-skill che fanno il commit reale.
LA LEGGE DI FERRO
SU BLOCK_REGRESSION DEL HOOK review-evidence, AUTO-FIX FINO AD
AUTO_APPROVE O ESCALATE. Mai loop infiniti, mai bypass nascosti, mai
fix direttamente — solo composizione di sub-skill esistenti.
Quando si applica
review-evidence v2 ha emesso regression_verdict.decision == BLOCK_REGRESSION
- Branch e' ahead di
main (commit gia' presenti)
- Working tree pulito (
git status --porcelain empty)
- Trigger:
/forge-fix-evidence (manuale, MVP)
Skip conditions — ESCALATE human
| Condizione | Motivo |
|---|
hard_floor_breaches non vuoto | Non-overridable: solo fix reale dello score, human judgment (security/compliance) |
is_bot_pr == True | Bot (Dependabot/Renovate) non usa DevForge |
decision == SEVERELY_DEGRADED | Tooling rotto, fix infra prima |
Action kind == "unknown" | Reason format ignoto, escalate per safety |
| Working tree dirty pre-loop | Rischio commit accidentale -> abort |
Algorithm (pseudo-Python)
def auto_fix_loop(
repo_root: Path,
sha: str,
max_iter: int = 5,
token_budget: int = 200_000,
max_cumulative_iter: int = 10,
):
"""Loop di remediation. Sequenziale, no concurrent sub-skill.
Stato PERSISTENTE per-branch (design 2026-07-09 §4.B): il run successivo
RIPRENDE i totali invece di ripartire da zero. Il file di stato vive in
.claude/fix-evidence-state/<branch-slug>.json (gitignorato), accanto al
counter <branch-slug>.autocount scritto dal hook review-evidence.
"""
branch_slug = run_bash("git rev-parse --abbrev-ref HEAD").replace("/", "-")
state_file = repo_root / ".claude/fix-evidence-state" / f"{branch_slug}.json"
autocount_file = repo_root / ".claude/fix-evidence-state" / f"{branch_slug}.autocount"
state = read_json_or_default(state_file, {
"iter_count_total": 0, "tokens_total": 0,
"last_reasons_set": None, "reverts": 0, "updated_at": None,
})
iter_count = 0
tokens_consumed = 0
last_reasons_set = (frozenset(state["last_reasons_set"])
if state["last_reasons_set"] else None)
while iter_count < max_iter and tokens_consumed < token_budget:
if state["iter_count_total"] >= max_cumulative_iter:
return {"status": "ESCALATED", "reason": "cumulative_iter_exceeded"}
evidence = evidence_from_json(
read_json(repo_root / ".claude/review-evidence" / f"{sha}.json")
)
if should_escalate(evidence):
persist_state(state_file, state, last_reasons_set)
return {"status": "ESCALATED", "reason": skip_reason(evidence)}
actions = parse_block_reasons(evidence)
if not actions:
run_bash(f'rm -f "{state_file}" "{autocount_file}"')
return {"status": "SUCCESS_AUTO_APPROVE", "iters": iter_count}
if actions[0].kind == "unknown":
persist_state(state_file, state, last_reasons_set)
return {"status": "ESCALATED", "reason": "unknown_block_reason"}
current_set = frozenset(evidence.verdict.block_reasons)
if last_reasons_set is not None and current_set == last_reasons_set:
persist_state(state_file, state, last_reasons_set)
return {"status": "ESCALATED", "reason": "oscillation_detected"}
sha_pre_iter = sha
action = actions[0]
Skill(skill=action.sub_skill, args=action.prompt)
sha = git_rev_parse_head()
run_bash("hooks/review-evidence")
new_evidence = evidence_from_json(
read_json(repo_root / ".claude/review-evidence" / f"{sha}.json")
)
new_set = frozenset(new_evidence.verdict.block_reasons)
if new_set >= current_set and len(new_set) > len(current_set):
run_bash(f"git reset --hard {sha_pre_iter}")
sha = sha_pre_iter
state["reverts"] += 1
if state["reverts"] >= 2:
persist_state(state_file, state, last_reasons_set)
return {"status": "ESCALATED", "reason": "worsening_detected"}
else:
last_reasons_set = current_set
iter_tokens = estimate_iter_tokens()
tokens_consumed += iter_tokens
iter_count += 1
state["iter_count_total"] += 1
state["tokens_total"] += iter_tokens
persist_state(state_file, state, last_reasons_set)
if iter_count >= max_iter:
return {"status": "MAX_ITER_EXCEEDED", "iters": iter_count}
return {"status": "TOKEN_BUDGET_EXCEEDED", "tokens": tokens_consumed}
persist_state scrive il JSON con last_reasons_set serializzato come lista
ordinata e updated_at ISO-8601 — eseguito via Bash (python3 - <<PY o
jq -n), coerente con allowed-tools: Read, Bash, Skill.
Dispatch tabella (MVP — 2 pattern)
block_reason format | kind | priority | Sub-skill |
|---|
coverage_below_threshold:X<Y | coverage | 2 | siae-tdd |
lint_errors:N>M | lint | 1 | siae-code-standards |
(out-of-scope MVP) complexity_max:X>Y | TODO | TODO | siae-tdd refactor |
(out-of-scope MVP) drift_severity:high | TODO | TODO | siae-brainstorming |
(out-of-scope MVP) ci_critical:N>M | TODO | TODO | siae-debugging |
Priority lower = applied first. Lint prima di coverage (small surface ->
basso blast radius).
Limiti operativi
- Max iter: 5 (hard cap, no override)
- Max iter cumulative per branch: 10 (persistite in
.claude/fix-evidence-state/<branch-slug>.json; superate -> escalate
cumulative_iter_exceeded; reset SOLO umano: rimozione del file)
- Keep-or-rollback: iterazione che PEGGIORA i
block_reasons
(superset proprio) -> git reset --hard al SHA pre-iterazione; al 2°
revert cumulativo sul branch -> escalate worsening_detected
- Token budget: 200_000 token (
DEVFORGE_FIX_EVIDENCE_TOKEN_BUDGET)
- Esecuzione: sequenziale (NO concurrent sub-skill — race su .git/index)
- Oscillation guard: stesso
frozenset(block_reasons) per 2 iter
consecutivi -> escalate (memory feedback_* su fix oscillatorio)
- Working tree: deve essere pulito a inizio loop, ogni iter chiude con
commit del subagent
- Idempotenza: ogni iter parte da un SHA diverso (HEAD aggiornato)
Vincoli
- NON modificare file direttamente — invoca solo sub-skill via
Skill tool. Il subagent fa il commit reale.
- NON pushare — solo commit locali. La PR viene aggiornata fuori da
questa skill (workflow utente).
- ESCALATE quando human judgment serve: hard_floor, security
critical, oscillation, unknown reason, SEVERELY_DEGRADED.
- Riusa skill esistenti (
siae-tdd, siae-code-standards) via ADR-7
dynamic prompt — NON re-implementarle.
- Forward-compat: reason format ignoto ->
FixAction(kind="unknown")
-> escalate. Mai crash silenzioso, mai drop silenzioso.
Pre-flight check (manuale prima del loop)
git status --porcelain
SHA=$(git rev-parse HEAD)
test -f ".claude/review-evidence/${SHA}.json" || bash hooks/review-evidence
jq -r '.regression_verdict.decision' ".claude/review-evidence/${SHA}.json"
jq -r '.regression_verdict.hard_floor_breaches' ".claude/review-evidence/${SHA}.json"
Se uno qualsiasi dei check fallisce -> abort + escalate.
Final report (a fine loop)
fix-evidence loop — SHA <final-sha>
Status: <AUTO_APPROVE | ESCALATED | MAX_ITER_EXCEEDED | TOKEN_BUDGET_EXCEEDED>
Iters: <N>/5
Tokens: <consumed>/200_000
Final reasons: [<lista block_reasons al termine>]
Sub-skill log: [<lista (iter, sub_skill, kind)>]
Cumulative: <iter_count_total>/10 (vita branch)
Reverts: <reverts> (keep-or-rollback)
Escalation: <reason se ESCALATED, altrimenti none>
Env var rilevanti
| Env var | Default | Note |
|---|
DEVFORGE_FIX_EVIDENCE_TOKEN_BUDGET | 200000 | Token budget loop (Claude API usage rough). |
DEVFORGE_FIX_EVIDENCE_MAX_ITER | 5 | Hard cap iter (override richiede design review). |
DEVFORGE_FIX_EVIDENCE_AUTO | 1 | Default ON (BREAKING dal follow-up feat/fix-evidence-auto-trigger). Auto-trigger fully-autonomous: hook review-evidence emette signal AUTO_FIX_TRIGGER in additional_context su BLOCK_REGRESSION, agent intercept e auto-invoca questa skill. Set 0 per disabilitare (opt-out kill-switch). |
DEVFORGE_FIX_EVIDENCE_MAX_AUTO_TRIGGERS | 3 | Cap cumulativo di emissioni AUTO_FIX_TRIGGER per branch, applicato dal hook review-evidence (counter .claude/fix-evidence-state/<branch-slug>.autocount, fail-open se corrotto). Azzerato dalla skill su SUCCESS_AUTO_APPROVE. Raggiunto il cap: il block resta, nessun auto-trigger — fix human-only. |
Auto-trigger pattern (fully-autonomous)
Quando si attiva: sempre, di default (DEVFORGE_FIX_EVIDENCE_AUTO=1
e' il default dal follow-up feat/fix-evidence-auto-trigger).
BREAKING change vs v1.55 PR #244 dove il default era 0 (opt-in).
Pattern semantic (doc-level, NO bash enforcement) per chiudere il loop "zero
bug usando DevForge" senza azione utente manuale. DevForge e' opinionato
verso questo comportamento: ogni BLOCK_REGRESSION clean (no hard floor,
no bot, not degraded) tenta un auto-fix loop prima di propagare il block
all'utente.
Signal canonico hook -> agent
Quando il hook review-evidence v2 emette BLOCK_REGRESSION E
DEVFORGE_FIX_EVIDENCE_AUTO=1 E nessuna skip condition matcha, l'output JSON
contiene additional_context con marker grep-friendly:
AUTO_FIX_TRIGGER:/forge-fix-evidence:sha=<SHA>
Comportamento agent (Claude Code)
Quando vedi AUTO_FIX_TRIGGER:/forge-fix-evidence in tool output, l'agent
DEVE:
- Intercept il marker PRIMA di propagare il block all'utente.
- Invocare automaticamente
Skill(skill="siae-fix-evidence", args="sha=<SHA>").
- Aspettare result della skill (max 5 iter + token budget 200k).
- Se skill ritorna
AUTO_APPROVE → ri-prova action originale (gh pr create).
- Se
ESCALATED → mostra all'utente con context per intervento manuale.
Non chiedere conferma utente: il design e' opt-in via env, la conferma e'
gia' stata data attivando DEVFORGE_FIX_EVIDENCE_AUTO=1.
Skip conditions hook-level (no signal emitted)
Identiche alle skill SKIP conditions:
| Condizione | Hook check |
|---|
regression_verdict.hard_floor_breaches non vuoto | jq '.regression_verdict.hard_floor_breaches | length' > 0 -> skip |
| Bot PR (Dependabot / Renovate / github-actions) | GITHUB_ACTOR matches pattern -> skip |
decision == SEVERELY_DEGRADED | Diverso case branch, signal non raggiunto |
decision == BLOCK_HARD_FLOOR | Diverso case branch, signal non raggiunto |
In tutte queste condizioni il hook non emette il signal: il block resta
ma senza auto-trigger, escalation human come da skill skip table.
Vincoli di safety
- Signal e' ADDITIVO, NOT al posto di
decision:block. Block resta per
safety; agent intercept additional_context -> auto-launch skill ->
re-check block sulla nuova evidence.
- Default ON, opt-out kill-switch via env:
DEVFORGE_FIX_EVIDENCE_AUTO=1
e' il default. Set 0 per disabilitare globalmente (vedi sezione
"Opt-out" sotto). Skip conditions semantic (hard floor / bot /
SEVERELY_DEGRADED) restano invariate.
- Hook resta single-file (B3 PR #243 fix preserved): il signal e' una
stringa nel campo
additional_context esistente, non un nuovo hook.
Opt-out
Per disabilitare globalmente l'auto-trigger (es. agent non-Claude-Code che
non riesce a fare intercept del marker, debug/troubleshooting, policy
aziendale "human in the loop" mandatoria):
export DEVFORGE_FIX_EVIDENCE_AUTO=0
echo 'export DEVFORGE_FIX_EVIDENCE_AUTO=0' >> ~/.zshrc
DEVFORGE_FIX_EVIDENCE_AUTO=0 gh pr create --title "..."
Con =0 il hook non emette il marker AUTO_FIX_TRIGGER: BLOCK_REGRESSION
si comporta come pre-PR-#244 (block + reason, fix manuale via
/forge-fix-evidence o human review). Le skip conditions semantic restano
in vigore identicamente (hard floor / bot / SEVERELY_DEGRADED non emettono
mai il marker, anche con =1).
Design + Plan
- Design:
docs/plans/2026-05-13-fix-evidence-auto-loop-design.md
- Predecessor:
docs/plans/2026-05-13-review-evidence-v2-scoring-design.md
- Parser:
lib/review_evidence/fix_parser.py
- Tests:
tests/test_fix_parser.py
Out-of-scope MVP (follow-up PR-D)
- 3 atomic pattern aggiuntivi (
complexity_max, drift_severity,
ci_critical)
- Test E2E loop con mock subagent
- Auto-trigger su hook output (richiede hook change)
- Telemetria fix success rate
Guardrail operativi (centralizzati)
- Classificazione Rischio Operazioni: vedi
lib/risk-taxonomy.md (tassonomia comune DevForge).
- Tabella Anti-Razionalizzazione: vedi
lib/risk-taxonomy.md + lib/permission-denied-handling.md; regola locale: nessuna scorciatoia "solo questa volta" — se un gate blocca, il fix è soddisfarlo, non aggirarlo.