Skip to content

feat: triage agentic workflow (IMPL-0025) — WIP#268

Merged
galanko merged 34 commits into
mainfrom
feat/triage-agentic-workflow
Jun 14, 2026
Merged

feat: triage agentic workflow (IMPL-0025) — WIP#268
galanko merged 34 commits into
mainfrom
feat/triage-agentic-workflow

Conversation

@galanko

@galanko galanko commented Jun 10, 2026

Copy link
Copy Markdown
Collaborator

User description

Status: draft / in progress. Implementing the triage agentic workflow per the approved design.

What this is

Two-speed triage: keep the $0 Quick read (ADR-0051 synthesizer) as the default, add a read-only, plan-don't-execute agentic Deep dive that escalates only on uncertainty/stakes within a budget, grounded by a new per-repo Project profile (repo knowledge base). Validated by an eval whose dataset stays private in cliff-os (ADR-0050 hybrid).

Design of record (private cliff-os): PRD-0008 rev. 2 + PRD-0009; ADR-0052 + ADR-0053; IMPL-0025.

Phase 1 — Repo knowledge base (ADR-0053) ✅

repo entity + canonicalizer + migration; atomic store + lazy PROFILE.md; one-build CAS mutex + reaper; credential-less clone + GC; access layer; 3 read-only profile builders; eager profiling at scan; /api/repos/profile* + dashboard card.

Phase 2 — the Deep dive (ADR-0052) ✅

deep-dive schemas (additive TriageOutput, no migration); read-only grep; model tiering (amends ADR-0037); escalation gate + budget (severity alone never escalates); the five agents gather_facts → rule_out → trace_path → plan_exploit → challenge (deterministic majority-refutes resolution); DeepDiveRunner with fail-cheap exits, wired into the triage path (best-effort, never breaks triage).

Phase 3 — the eval (ADR-0052 §Evaluation) ✅

Harness here (public); the valuable dataset lives in private cliff-os/eval and never enters this repo.

  • deterministic HARD gates: false-clear (golden real never cleared, zero-tolerance), citation grounding (every cited file:line resolves — fabrication caught by code, no judge), read-only tool boundary; graded verdict-match
  • run_deep_dive_eval (stages case.files → temp repo) + make_live_deep_dive_pipeline (real DeepDiveRunner for the private live lane)
  • tiny synthetic CI sample in this repo; the moat dataset (vulnerable/patched pairs, the research-vertical corpus) is private
  • 8 tests proving the gates fire

Remaining

  • Phase 4 — UI (exploit-plan card + challenge trail + narration; needs UX-0009)
  • Phase 5 — LSP fast-follow
  • Phase 6 — sandbox / live execution — separate future ADR, NOT in this PR

All TDD-first, TestModel in CI (no real LLM). Do not merge — for review by @galanko.

🤖 Generated with Claude Code


Generated description

Below is a concise technical summary of the changes proposed in this PR:
Drive the ADR-0052 agentic Deep dive by wiring DeepDiveRunner, the stage agents/challenge panel, retry/budgeted read+grep tools, and the deterministic eval into triage so uncertain/high-stakes findings safely escalate. Bootstrap ADR-0053’s repo knowledge base via RepoDirManager/ProfileRunner plus the repo APIs and dashboard card so every Deep dive run is grounded in a canonical project profile that users can inspect or refresh.

TopicDetails
Repo knowledge base Stand up the ADR-0053 repo knowledge base: canonical repo entity/migration, DAO + profile store, credentialless clone + GC adapters, RepoDirManager schema/knowledge surface, profile builders/runner/service, API, frontend hooks/tests, and dashboard wiring so triage reads resilient project context and users can trigger/observe profile builds.
Modified files (32)
  • backend/cliff/api/routes/repos.py
  • backend/cliff/db/migrations/025_repo_entity.sql
  • backend/cliff/main.py
  • backend/cliff/models/__init__.py
  • backend/cliff/models/repo.py
  • backend/cliff/repos/__init__.py
  • backend/cliff/repos/clone.py
  • backend/cliff/repos/dao.py
  • backend/cliff/repos/gc.py
  • backend/cliff/repos/git_ops.py
  • backend/cliff/repos/identity.py
  • backend/cliff/repos/knowledge.py
  • backend/cliff/repos/profile_agents.py
  • backend/cliff/repos/profile_runner.py
  • backend/cliff/repos/repo_dir_manager.py
  • backend/cliff/repos/schemas.py
  • backend/cliff/repos/service.py
  • backend/tests/api/openapi_snapshot.json
  • backend/tests/repos/__init__.py
  • backend/tests/repos/conftest.py
  • backend/tests/repos/test_clone.py
  • backend/tests/repos/test_git_ops.py
  • backend/tests/repos/test_identity.py
  • backend/tests/repos/test_knowledge.py
  • backend/tests/repos/test_profile_agents.py
  • backend/tests/repos/test_profile_runner.py
  • backend/tests/repos/test_repo_dao.py
  • backend/tests/repos/test_repo_dir_manager.py
  • backend/tests/repos/test_service.py
  • backend/tests/test_routes_repos.py
  • frontend/src/api/__tests__/repos.test.tsx
  • frontend/src/api/repos.ts
Latest Contributors(1)
UserCommitDate
galank@gmail.comfix(triage): address C...June 14, 2026
Deep dive flow Orchestrate the escalation-gated Deep dive flow: tier the models, cap read/grep output, define the stage prompts/challenge panel, assemble DeepDiveRunner/integration into triage, extend the schema/sidebar contracts, and ship the evaluation harness + UI coverage so every agent run stays deterministic, read-only, and safely degrades.
Modified files (36)
  • backend/cliff/agents/runtime/deps.py
  • backend/cliff/agents/runtime/model_tiers.py
  • backend/cliff/agents/runtime/tools/__init__.py
  • backend/cliff/agents/runtime/tools/grep.py
  • backend/cliff/agents/runtime/tools/read.py
  • backend/cliff/agents/schemas.py
  • backend/cliff/agents/triage_deep/__init__.py
  • backend/cliff/agents/triage_deep/agents.py
  • backend/cliff/agents/triage_deep/challenge.py
  • backend/cliff/agents/triage_deep/escalation.py
  • backend/cliff/agents/triage_deep/integration.py
  • backend/cliff/agents/triage_deep/runner.py
  • backend/cliff/agents/triage_runner.py
  • backend/cliff/api/routes/agent_execution.py
  • backend/cliff/api/routes/assessment.py
  • backend/cliff/evals/__init__.py
  • backend/cliff/evals/cases.py
  • backend/cliff/evals/deep_dive_evaluators.py
  • backend/cliff/evals/repo_fetch.py
  • backend/cliff/evals/runners.py
  • backend/tests/agents/eval/triage_deep_dive.jsonl
  • backend/tests/agents/test_deep_dive_agents.py
  • backend/tests/agents/test_deep_dive_integration.py
  • backend/tests/agents/test_deep_dive_runner.py
  • backend/tests/agents/test_deep_dive_schemas.py
  • backend/tests/agents/test_escalation.py
  • backend/tests/agents/test_evals_deep_dive.py
  • backend/tests/agents/test_grep_tool.py
  • backend/tests/agents/test_model_tiers.py
  • backend/tests/agents/test_read_budget.py
  • backend/tests/agents/test_repo_fetch.py
  • frontend/src/api/client.ts
  • frontend/src/components/dashboard/ProjectProfileCard.tsx
  • frontend/src/components/issues/IssueSidePanel.tsx
  • frontend/src/components/issues/__tests__/IssueSidePanel.test.tsx
  • frontend/src/pages/DashboardPage.tsx
Latest Contributors(1)
UserCommitDate
galank@gmail.comfix(triage): address C...June 14, 2026
Review this PR on Baz | Customize your next review

galanko and others added 2 commits June 10, 2026 21:58
…R-0053 P1)

IMPL-0025 Phase 1 foundation: first-class repo entity (migration 025) keyed by
a canonicalized URL (the normalization the app lacked), its DAO with a
one-build-per-repo CAS mutex + stale-build reaper, and RepoDirManager — the
shared per-repo Project-profile store with atomic writes and a lazy generated
PROFILE.md. 49 tests, TDD-first.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…3 P1.5)

Agents declare which profile sections they consume; the loader populates only
those, so no agent pays tokens for a section it doesn't use. Mirrors WorkspaceDeps
for the per-repo tier. 5 tests.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jun 10, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

This PR adds deep-dive scanner triage with staged agents and challenge review, introduces per-repo profile storage/build APIs and background jobs, extends evaluation support for live repo checkouts, and surfaces repo profiles plus deep-dive triage details in the frontend.

Changes

Deep dive triage and repo profiling

Layer / File(s) Summary
Runtime contracts and tool limits
backend/cliff/agents/runtime/deps.py, backend/cliff/agents/runtime/model_tiers.py, backend/cliff/agents/runtime/tools/*, backend/cliff/agents/schemas.py
Adds read budgeting, provider tier resolution, a read-only grep tool, and new deep-dive triage schema fields and stage artifact models.
Repo entity, storage, and profile build flow
backend/cliff/repos/*, backend/cliff/models/repo.py, backend/cliff/models/__init__.py, backend/cliff/db/migrations/025_repo_entity.sql, backend/cliff/api/routes/repos.py, backend/cliff/api/routes/assessment.py, backend/cliff/main.py
Creates the repo table, URL canonicalization, clone/profile storage managers, profile build orchestration and scheduling, and API endpoints for profile status and rebuild.
Deep-dive triage execution and scanner wiring
backend/cliff/agents/triage_deep/*, backend/cliff/agents/triage_runner.py, backend/cliff/api/routes/agent_execution.py
Implements staged deep-dive agents, escalation decisions, challenge/disproof panels, runner orchestration, repo knowledge loading, and best-effort scanner triage escalation.
Evaluation harness and backend validation
backend/cliff/evals/*, backend/tests/agents/*, backend/tests/repos/*, backend/tests/test_routes_repos.py, backend/tests/api/openapi_snapshot.json
Adds deep-dive eval runners and gates, pinned-SHA repo checkout support, synthetic eval data, and backend tests covering deep-dive flow, repo profiling, runtime limits, and API contracts.
Frontend API and UI surfaces
frontend/src/api/client.ts, frontend/src/api/repos.ts, frontend/src/api/__tests__/repos.test.tsx, frontend/src/components/dashboard/ProjectProfileCard.tsx, frontend/src/components/issues/IssueSidePanel.tsx, frontend/src/components/issues/__tests__/IssueSidePanel.test.tsx, frontend/src/pages/DashboardPage.tsx
Adds repo-profile client hooks and dashboard rendering, extends triage types for deep-dive fields, and renders exploit plan, challenge, and provenance details in the issue side panel.

Sequence Diagram(s)

sequenceDiagram
  participant Route as POST /findings/{id}/triage
  participant TriageRunner as run_triage
  participant DeepDive as maybe_deep_dive
  participant RepoStore as RepoDirManager
  participant Runner as DeepDiveRunner

  Route->>TriageRunner: run_triage(ai_env, model_full_id)
  TriageRunner->>TriageRunner: build quick triage
  TriageRunner->>DeepDive: maybe_deep_dive(quick, repo_url, finding)
  DeepDive->>RepoStore: load profile/code_map/threat + clone_dir
  DeepDive->>Runner: run(finding, repo_knowledge, clone_dir)
  Runner-->>TriageRunner: deep-dive TriageOutput
  TriageRunner-->>Route: selected triage result
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Possibly related PRs

Suggested reviewers

  • baz-reviewer

Poem

🐇 I sniffed through code in a moonlit dive,
Found profiles and triage all spring alive.
With grep in paw and budgets tight,
The burrow now maps each repo right.
I thump with joy: what a well-wired night!

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/triage-agentic-workflow

galanko and others added 27 commits June 10, 2026 22:24
Persistent clone obtains the token via GIT_ASKPASS+env (never argv/URL), so
.git/config stays credential-less — verified by an offline clone against a local
bare repo. refresh_repo fetches+resets to bring a clone current. gc_repo_clones
evicts LRU clones over a byte budget while keeping the (re-clonable) profile
artifacts. 10 tests.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…R-0053 P1.3/1.7)

Ties the mutex + clone sync + store + manifest + status lifecycle into one
build flow. Builders / clone-sync / head-sha / token lookup are injected, so the
whole backbone is proven end-to-end with fakes (mutex-skip, error→status,
token-passthrough, artifacts+manifest+digest). The real LLM builders satisfy the
ProfileBuilder shape. 4 tests.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
repo_profiler / code_map / threat_history — read-only in-process Pydantic AI
agents emitting typed artifacts (RepoProfile/CodeMap/ThreatHistory, extra=allow).
They satisfy the ProfileBuilder shape so ProfileRunner drives them, reuse the
existing read tool (workspace_dir → clone), and are read-only by design. Driven
end-to-end with TestModel; quality is the key-gated eval. 6 tests.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
git_ops (real sync_clone clone-or-refresh + git_head_sha, offline-tested) +
service.build_profile_runner / schedule_profile_build (assembles a real
ProfileRunner from the canonical AI state + vault token, fires a tracked
background task; best-effort no-op when no AI provider). Hooked into the
assessment run route so a scan grounds triage. 16 tests; full offline e2e
through the real git adapters.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…kend)

GET /api/repos/profile (status + freshness + generated PROFILE.md, resolving
the current repo from the GitHub integration when not given) and POST
/api/repos/profile/rebuild (schedules the eager build; skipped cleanly without
an AI provider). Registered the router; OpenAPI snapshot regenerated. 5 tests.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
repos API client + useRepoProfile/useRebuildProfile hooks, and a Cyberdeck
ProjectProfileCard on the dashboard: shows what Cliff understood, freshness
(built N ago · sha), a 'what Cliff understood' disclosure, and a re-profile
action (polls while building). Renders nothing when there's no repo/profile.
tsc + eslint clean; 3 api tests + DashboardPage regression green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…052 P2.9)

FindingFacts / RuleOutResult / DeepReachability (the per-finding trail) +
ExploitPlan / Challenge / TriageProvenance (additive TriageOutput blocks, all
optional → shipped UI + persisted rows unaffected). KillClass enum, specific
Disproof. 8 tests; backward-compat verified.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…2.1/2.2/2.3)

Read-only pure-Python grep over the clone (no shell, path-scoped, skips .git);
model tiering deriving cheap/strong/judge from one provider's lineup (amends
ADR-0037, judge out-ranks strong per ADR-0050); escalation gate + per-assessment
deep-dive budget where severity ALONE never escalates (the cost control that
keeps a flood cheap). 17 tests.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… (ADR-0052 P2.4-2.8)

gather_facts/rule_out/trace_path/plan_exploit (read-only, read+grep over the
clone, walk-* + FP-class disciplines in the prompts) + the challenge panel (one
adversarial reviewer per lens, DETERMINISTIC majority-refutes resolution that
downgrades to needs_review). Exported grep from the tools package. TestModel-
driven; 10 tests.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…-0052 P2.10)

Linear backbone wiring the five stages with early exits (rule-out kill →
false_positive; disproof → unexploitable; unknown → needs_review; reachable-no-
exploit → unexploitable/hardening; challenge holds → real, refuted → downgrade)
and TriageOutput assembly (mapped reachability for the shipped UI, exploit_plan,
challenge, SHA-pinned provenance with per-step model tiers). Injected stages →
every path TDD'd. 6 tests.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…052 P2.10)

maybe_deep_dive gates on the escalation decision + a ready repo profile, then
runs the DeepDiveRunner over the cached clone; threaded into _run_scanner_triage
(best-effort — any failure keeps the Quick-read verdict, triage never breaks).
The triage route passes the canonical AI state so escalation can build the tier
models. OpenAPI snapshot regenerated for the additive TriageOutput. 4 tests +
triage_runner regression green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…052 P3, public)

The HARNESS only (ADR-0050 hybrid — data stays in private cliff-os/eval, never
this repo). Deterministic gates: false-clear (golden real never cleared,
zero-tolerance), citation grounding (every cited file:line resolves in the
staged repo — fabrication caught by code, no judge), read-only tool boundary,
graded verdict-match. run_deep_dive_eval stages case.files into a temp repo and
applies them; make_live_deep_dive_pipeline drives the real DeepDiveRunner for the
private live lane. Tiny SYNTHETIC CI sample only. 8 tests proving the gates fire.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…0052 P3)

So the private cliff-os/eval runner can import run_deep_dive_eval +
make_live_deep_dive_pipeline.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…0052 P3)

EvalCase gains repo+sha (a real public repo at a pinned commit, mutually
exclusive with the synthetic files map). run_deep_dive_eval checks it out
(checkout_at_sha — single-commit fetch by SHA) so the agent walks REAL code,
not a micro-repo — the vulnerable/patched SHA-pair test. Proven offline against
a two-commit local bare repo. 3 tests.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…dget degradation (ADR-0052)

Surfaced by the first live run against a real model:
- grep: resolve the workspace root too (macOS /var -> /private/var symlink made
  relative_to() crash on every match)
- DeepDiveRunner degrades to needs_review on UsageLimitExceeded instead of
  crashing (never a false clear); challenge reviewers degrade per-lens (an
  incomplete reviewer holds, never wrongly downgrades); request_limit 25 -> 40
  for real-repo walks.
Validated live: false_positive/real/unexploitable verdicts correct, 0 false-clears.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…R-0052)

From the cheap real-repo run (all-Haiku on gradio/mlflow): the agent over-read
big repos, looped, overflowed the 200K window (crash), and rule_out false-cleared
a real CVE. Fixes:
- ReadBudget: per-stage cumulative read/grep byte cap (120KB) so tool output
  can't overflow context; executor unaffected (None = unlimited).
- runner catches the context-overflow ModelHTTPError -> needs_review (never crash).
- prompts: read the cited file sparingly, not the whole repo.
- rule_out: only kill with positive evidence; reachable code is NEVER a kill
  (stops the false-clear) — when in doubt, killed=false.
4 budget tests; full deep-dive suite green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…he false-clear (ADR-0052)

Prompt tightening alone didn't stop Haiku false-clearing the real Gradio CVE at
rule_out. Architectural fix: the runner only HONORS a rule_out kill when it's
structurally corroborated — duplicate_of_known (a matching prior issue in the
threat history) or root_cause_in_nonship_code (a root-cause file matching an
excluded code-map glob). A model's 'looks safe' hunch no longer terminally
clears; it falls through to trace_path, which must produce a real disproof the
challenge panel checks. Guarantees zero rule-out false-clears regardless of
model. rule_out prompt restricted to structural classes to match.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ve (ADR-0052)

The Gemini run hit a 503 'high demand' on the flash tier and crashed — the
DeepDiveRunner had no retry (unlike the executor). run_agent_with_retry retries
429/503 with exponential backoff (1/2/4s) across all stages + the challenge
panel; a sustained transient (or context overflow) degrades to needs_review
instead of crashing.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…pped 10s)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…-0052)

The panel fired 3 reviewer calls in parallel; against the AI Studio capacity
ceiling that burst is a prime 503 trigger. Run them one at a time — a little
slower, far fewer transient failures.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…n challenge, retune retry (ADR-0052)

Three prompt/logic fixes, each targeting a failure observed on the real CVE
pairs and reproduced on cheap synthetic guard cases (0/2 -> 2/2):

1. trace_path now REQUIRES hunting a neutralizing guard before concluding
   reached=yes — opening helper functions on the path (e.g. is_safe_path). A
   guard found there -> reached=no + disproof -> unexploitable. Fixes the
   over-flag where patched code was called real (trace missed the fix's guard).

2. challenge reviewers refute ONLY on a specific, code-verified defect, not on
   generic caution. Stops the panel downgrading genuine reals to needs_review.

3. challenge 'impact' lens judged impact as 'demonstrated vs assumed' — but V1
   is plan-don't-execute, so NOTHING is demonstrated and it refuted every real
   by construction. Reframed to judge the impact the path PROVES.

Plus: retune run_agent_with_retry for AI Studio's fast-recovering 503s (12
short attempts, backoff capped at 4s) so a multi-call pipeline survives a
saturated window instead of collapsing to needs_review.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…-0052)

Real-repo eval surfaced a SAFETY bug: the guard-hunt fix made trace eager to
find guards, and on genuinely-vulnerable gradio it intermittently latched onto a
PHANTOM guard, returned reached=no, and the runner cleared the finding as
unexploitable — a false-clear of a real CVE. Root cause: reached=no exited
DIRECTLY to unexploitable, bypassing the adversarial Challenge panel (the safety
gate only ran on the 'real' path).

Fix: symmetrize the gate. A disproof CLEARS a finding (the worst verdict to get
wrong), so it now gets MORE scrutiny, not less. reached=no routes through a new
adversarial disproof-challenge panel (lenses: bypass / scope / phantom) tasked to
BREAK the guard. resolve_disproof is unanimous-to-clear: any reviewer that finds
a concrete bypass drops the verdict to needs_review. A phantom guard can no
longer false-clear a real vuln. This is exactly what the rule_out comment already
promised ('a real disproof the challenge panel checks') — now the code does it.

Also: retry httpx network timeouts (the mlflow ReadTimeout that crashed a case at
710s isn't a 503, so it slipped past the retry).

Shares the panel machinery (_run_reviewers) with the real-verdict challenge; the
incomplete-reviewer default flips by direction (real holds / disproof refutes) so
an incomplete panel never false-clears. 9 new/updated tests, 47 green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…DR-0052)

On the judge tier the disproof panel now correctly upholds a real guard
(gradio-patched is_http_url_like -> unexploitable, with a precise open-redirect-
vs-SSRF analysis) instead of flash's reflexive bypass:refuted. The lens primed
speculation ('can the attacker encode/symlink/null-byte...'); now it requires a
CONCRETE, code-grounded bypass and explicitly holds when the guard validates
correctly. Tier-correct real-repo run: gradio-patched unexploitable + mlflow-
vulnerable real, zero false-clears.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…R-0052)

Overnight accuracy iteration on the 16-case real-CVE benchmark (Gemini Pro
tier-correct). Net effect: gradio-patched now clears correctly, vulnerable side
4/4, zero false-clears held throughout.

- temp=0 on all deep-dive agents (DEEP_DIVE_MODEL_SETTINGS): reproducible
  verdicts; perspective diversity comes from the challenge lenses, not sampling.
- resolve_disproof: unanimous-to-clear -> MAJORITY (matches resolve_challenge).
  Unanimous let one reviewer nitpicking a complex-but-correct patch veto every
  legitimate clear (e.g. the file branch of a fixed SSRF route). Majority is the
  right balance; real vulns reach 'real' via trace reached=yes (not this panel),
  so the eval's zero-false-clear gate still holds (verified: 0 across all runs).
- disproof lenses scoped to the REPORTED sink; HOLD when the fix routes around
  the sink / uses a safe API / the residual issue is a different vuln class.
- trace guard-hunt recognizes safe-API replacements (safe_load, SandboxedEnv,
  libarchive EXTRACT_SECURE_*, parameterized query, escape helpers) and INLINE
  sanitization right above a sink; anchors on the finding's named path.

48 tests green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…se-clear (ADR-0052)

Overnight flash run exposed a false-clear: plain majority let two cosmetic
holds outvote a CORRECT bypass-refute and cleared a real vuln
(mlflow-pathtrav-vulnerable). Fix: the three disproof lenses are not equal. The
'bypass' lens is the direct 'can the attacker defeat the guard and reach the
sink?' probe, so a refute there is a concrete bypass = the finding is real — it
now VETOES the clear and can never be outvoted. scope/phantom (guard
completeness/existence) still resolve by majority, so a lone nitpick on a
complex-but-correct patch no longer blocks a legitimate clear. Clearing requires
bypass HOLDS and a majority hold.

Pro verification: gradio-patched -> unexploitable, vulnerable side stays real,
zero false-clears. Also: trace recognises a check that REJECTS the attacker's
specific malicious input (raise/abort) as confining the sink, with an explicit
'do not invent one on genuinely-vulnerable code' guard. 49 tests green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
galanko and others added 2 commits June 13, 2026 16:01
…ver auto-dismiss (ADR-0052)

Structural no-false-clear guarantee for weak/thin-lineup configs. The flash run
showed that on a weak judge the deep dive can both phantom-clear a real vuln and
have all disproof reviewers miss the bypass -> a false-clear (the one verdict
that can HIDE a real vuln). Production never hits this for known providers
(resolve_tier_model_ids always derives a strong judge: opus/gpt-5/pro), but a
thin-lineup provider (ollama/custom/unrecognized) collapses every tier to one
possibly-weak model.

Fix: clearing_is_trusted(model_full_id) is True only for known-lineup providers.
DeepDiveRunner(can_clear=...) gates a single point: any DISMISSAL verdict
(unexploitable/false_positive) on an untrusted config is routed to needs_review
with a 'Tier gate' note. Detection (real) and flagging (needs_review) stay
unrestricted on every tier. Wired into both construction sites (integration +
the live eval pipeline). 58 tests green incl. the gate branches.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…052)

The backend already persists exploit_plan / challenge / provenance to
sidebar_state.triage; this surfaces them. Frontend TriageOutput type extended
with the deep-dive blocks, and three Cyberdeck components added to SPTriage:

- ExploitPlanPanel — ranked exploit hypotheses (impact class, attacker input,
  reached sink, expected impact) + a collapsible reproduction recipe, framed as
  'a plan, not a demonstrated exploit'; the reachable-but-no-exploit case renders
  a calm hardening note.
- ChallengePanel — the adversarial panel: held vs downgraded, per-reviewer lens +
  holds/refuted + refutation text.
- DeepDiveProvenance — 'How Cliff dug in': the stage trail in user-facing names
  (Gather the facts -> Rule out -> Trace the path -> Plan the exploit -> Challenge
  the verdict) + the SHA the verdict is valid for.

No 1px borders, mono eyebrows, sentence case, --cd-* tiers. tsc + eslint clean,
52 IssueSidePanel tests green (3 new). Live per-stage narration (SSE) is a noted
future enhancement (no streaming channel today).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@galanko galanko marked this pull request as ready for review June 14, 2026 14:36
Comment on lines +182 to +191
deep = await maybe_deep_dive(
db,
finding=finding_ctx,
quick=quick,
repo_url=workspace.repo_url,
enrichment=enrichment,
exposure=exposure,
ai_env=ai_env,
model_full_id=model_full_id,
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

maybe_deep_dive() is called without budget_remaining, so DEFAULT_DEEP_DIVE_BUDGET resets per finding and the budget_remaining > 0 gate in decide_escalation() never really decrements — should we thread a persisted remaining budget through run_triage and pass it in?

Severity

Want Baz to fix this for you? Activate Fixer

Other fix methods

Fix in Cursor

Prompt for AI Agents
Before applying, verify this suggestion against the current code. In
backend/cliff/agents/triage_runner.py around lines 182-191 inside the
_run_scanner_triage method, update the call to maybe_deep_dive to pass an explicit
remaining budget (instead of relying on integration.py’s DEFAULT_DEEP_DIVE_BUDGET
fallback). Refactor run_triage (around lines 109-138) and _run_scanner_triage (around
lines 141-201) to accept/load a persisted remaining-budget value for this
finding/assessment from the DB, decrement it according to the deep-dive decision, and
persist the updated remaining budget so decide_escalation() can’t effectively reset to
10 per finding. Ensure the scanner path mirrors whatever budget handling already exists
on the report triage path so the per-assessment escalation cap in escalation.py is
respected.

Heads up!

Your free trial ends tomorrow.
To keep getting your PRs reviewed by Baz, update your team's subscription

return final


async def _run_report_triage(

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

_run_report_triage(...) still returns sidebar.triage after report_triager and skips maybe_deep_dive, so decide_escalation(..., source="report") stays unreachable for reports — should we route this path through maybe_deep_dive(..., source="report", ai_env/model args) like the non-report flow?

Severity

Want Baz to fix this for you? Activate Fixer

Other fix methods

Fix in Cursor

Prompt for AI Agents
Before applying, verify this suggestion against the current code. In
backend/cliff/agents/triage_runner.py around lines 204 (the _run_report_triage helper),
stop short-circuiting after report_triager/sidebar triage returns, and instead route the
report verdict through maybe_deep_dive just like _run_scanner_triage does. Update
_run_report_triage’s signature to accept ai_env and model_full_id, and update
run_triage (around the REPORT_SOURCE_TYPE branch) to pass those arguments into
_run_report_triage. When building the parameters for maybe_deep_dive, ensure the
deep-dive/escalation logic can detect this is the report path (e.g., pass
source="report" if supported, or include enough context like finding.source_type/source)
and keep the “best-effort: keep quick verdict on deep-dive failure” behavior
consistent.

Heads up!

Your free trial ends tomorrow.
To keep getting your PRs reviewed by Baz, update your team's subscription

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Commit b27affb addressed this comment by routing _run_report_triage(...) through maybe_deep_dive(...) instead of returning sidebar.triage مباشرة. It now passes ai_env, model_full_id, and source="report", and preserves the best-effort fallback to the quick verdict on deep-dive failure.

Comment on lines +498 to +501
# Canonical AI state (ADR-0037) — enables the agentic Deep dive on
# escalation (ADR-0052); absent it, triage stays the cheap Quick read.
ai_env = dict(getattr(request.app.state, "ai_env_cache", {}) or {})
ai_model = getattr(request.app.state, "ai_model_cache", None)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

The AI cache lookup here duplicates cliff.repos.service.schedule_profile_build, should we resolve it once via shared app.state.ai_env_resolver/ai_model_resolver helpers?

Severity

Want Baz to fix this for you? Activate Fixer

Heads up!

Your free trial ends tomorrow.
To keep getting your PRs reviewed by Baz, update your team's subscription

Comment on lines +101 to +102
if kill_class == "duplicate_of_known":
return bool((repo_knowledge.get("threat") or {}).get("prior_issues"))

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

_kill_corroborated should check RuleOutResult.dedup_match against threat.prior_issues[*].id instead of treating any non-empty prior_issues as enough, otherwise unrelated history can clear a new finding.

Severity

Want Baz to fix this for you? Activate Fixer

Other fix methods

Fix in Cursor

Prompt for AI Agents
Before applying, verify this suggestion against the current code. In
backend/cliff/agents/triage_deep/runner.py around lines 91-107, in the
`_kill_corroborated` helper, the `duplicate_of_known` branch incorrectly returns true
whenever `repo_knowledge['threat']['prior_issues']` is non-empty, even if
`RuleOutResult.dedup_match` didn’t match that prior issue. Refactor this branch to
read the matched prior issue id from the `ro` dict (e.g., `ro.get('dedup_match')` or the
appropriate field name) and return true only when a prior issue in
`repo_knowledge['threat']['prior_issues']` has `id` equal to that matched id. Keep the
existing structural check for `root_cause_in_nonship_code`, but ensure the duplicate
gate requires an id-level corroboration rather than mere history presence.

Heads up!

Your free trial ends tomorrow.
To keep getting your PRs reviewed by Baz, update your team's subscription

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Commit 46b0f2f addressed this comment by changing duplicate_of_known to require ro.get("dedup_match") to match one of the threat history prior_issues[*].id values. This removes the old behavior where any non-empty prior history could clear an unrelated new finding.

Comment on lines +230 to +234
# Fresh per-stage read budget so cumulative tool output can't overflow the
# context window on a large real repo (ADR-0052).
deps = replace(deps, read_budget=ReadBudget(DEEP_DIVE_READ_BUDGET))
result = await run_agent_with_retry(agent, render_context(deps), deps)
return result.output.model_dump()

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Deep-dive stages return result.output.model_dump() straight into TriageOutput, so agent_run/SidebarState never get the intermediate gather_facts/rule_out/trace_path/plan_exploit/challenge results; should we persist each stage or route them through the normal executor before finalizing?

Severity

Want Baz to fix this for you? Activate Fixer You can also update your AI coding guidelines based on this comment by apply pr to [branch name]

Other fix methods

Fix in Cursor

Prompt for AI Agents
Before applying, verify this suggestion against the current code. In
backend/cliff/agents/triage_deep/agents.py around lines 229-234 in the _run(agent, deps)
function, the code currently returns result.output.model_dump() without persisting the
stage output anywhere. Update _run (and thus
run_gather_facts/run_rule_out/run_trace_path/run_plan_exploit) to write each stage
result to both required sinks: create the chat-timeline agent_run entry and persist the
intermediate output to SidebarState before returning the final verdict. Prefer routing
stage execution through the project’s normal agent executor/persistence utility (the
one used by other agents) so logging and SidebarState updates happen consistently;
otherwise, add explicit persistence calls right after run_agent_with_retry returns and
before result.output.model_dump() is returned.

Heads up!

Your free trial ends tomorrow.
To keep getting your PRs reviewed by Baz, update your team's subscription

@coderabbitai coderabbitai 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.

Actionable comments posted: 17

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@backend/cliff/agents/runtime/deps.py`:
- Around line 27-33: The take() method in the ReadBudget class only checks if
the budget is already exhausted before subtracting, but does not validate
whether the requested amount n can actually be deducted without going negative.
This allows an oversized read to bypass the limit on the first occurrence. Fix
this by changing the condition to check if the remaining budget is sufficient
before allowing the deduction — replace the condition that checks if remaining
<= 0 with a check that verifies self.remaining < n, ensuring that take() returns
False whenever the requested deduction would violate the budget cap.

In `@backend/cliff/agents/runtime/model_tiers.py`:
- Around line 54-65: The clearing_is_trusted function accepts malformed model
IDs as trusted by checking only the provider name portion. Malformed inputs like
"openai" (missing the "/" separator) will incorrectly be treated as trusted even
though they are not valid provider/model configurations. Modify the return
statement to validate that model_full_id actually contains a "/" separator
before checking if the provider portion is in _LINEUP, ensuring that only
properly formatted provider/model IDs with recognized providers are trusted for
dismissal.

In `@backend/cliff/agents/triage_deep/runner.py`:
- Around line 91-107: The _kill_corroborated function uses overly permissive
checks on lines 101 and 104 that can cause false terminal clears of real
findings. For the duplicate_of_known kill class, the check only verifies that
any prior issue exists rather than confirming a prior issue actually matches the
current finding being evaluated. For the root_cause_in_nonship_code kill class,
the check clears if any candidate file matches an excluded pattern rather than
verifying the actual root cause file matches. Strengthen both conditions: for
duplicate_of_known, add logic to verify the prior_issues contain one that
corresponds to or matches the current threat context, and for
root_cause_in_nonship_code, modify the check to ensure only the true root cause
file (not just any candidate) is matched against the excluded_roots patterns.
This prevents unrelated findings from being incorrectly dismissed as terminal
clears.

In `@backend/cliff/agents/triage_runner.py`:
- Line 146: The finding parameter in the function signature on line 146 is typed
as Any, but it is used on lines 179-180 with the method call
finding.model_dump(...), which requires a concrete Pydantic model type. Replace
the Any type hint with the concrete finding or Pydantic model type returned by
get_finding to enable proper type checking and comply with the strict type hint
requirements for backend code.

In `@backend/cliff/api/routes/repos.py`:
- Around line 70-76: The call to get_repo_by_url(db, url) can raise
InvalidRepoUrlError for malformed input, but this exception is not being caught,
causing a 500 error instead of the appropriate 422 validation error response.
Wrap the get_repo_by_url call in a try-except block to catch InvalidRepoUrlError
and return a RepoProfileStatus with an appropriate error status (such as "error"
or "invalid") to properly handle client validation errors with a 422 response.

In `@backend/cliff/evals/cases.py`:
- Around line 80-85: Add validation to the EvalCase class to enforce the mutual
exclusivity constraint documented in the comments. Implement a validator (such
as a Pydantic validator or a __post_init__ method) that ensures either both repo
and sha are provided together (for deep dive live lane mode), or both are None
(for synthetic micro-repo mode with files). The validator should raise a clear
error if partial inputs are detected (e.g., repo without sha, or sha without
repo) or if repo/sha are provided alongside files, preventing silent fallbacks
to unintended evaluation modes.

In `@backend/cliff/evals/deep_dive_evaluators.py`:
- Around line 67-70: The code constructs a target path by joining repo with rel
without validating that the result stays within the repository bounds, allowing
citations to escape via absolute paths or parent directory traversal. After
constructing target from repo / rel, resolve both paths to their absolute forms
and verify that the resolved target path is contained within the resolved repo
directory before calling is_file(). Use pathlib methods like resolve() to get
canonical paths and ensure the target starts with or is relative to the repo
directory.

In `@backend/cliff/evals/runners.py`:
- Around line 401-404: The code at lines 401-404 directly writes files using
untrusted relative paths from case.files without validation, allowing path
traversal attacks (e.g., ../../../ or absolute paths) to escape the repo_dir
boundary. After computing fp = repo_dir / rel, resolve the path to its absolute
form and verify that the resolved path is still within repo_dir before creating
parent directories or writing content. Use pathlib operations like .resolve()
and check that the result starts with repo_dir.resolve() to ensure the file
stays within the intended repository boundary.
- Around line 371-387: The run_deep_dive_eval function currently allows empty
case lists and returns a passing result, while other eval runners in the module
hard-fail on empty datasets. Add an early validation check at the beginning of
run_deep_dive_eval (after the result initialization) that raises an appropriate
exception if the cases list is empty, making this function consistent with the
other eval runner functions in the module.

In `@backend/cliff/repos/git_ops.py`:
- Around line 48-50: The conditional check at lines 48-50 in the git_ops.py file
only verifies that a .git directory exists before calling refresh_repo, but does
not validate that the existing clone's origin remote URL matches the
canonical_url parameter. Add a check to verify that the origin URL of the
existing repository matches the canonical_url before deciding to refresh it. If
the origin does not match canonical_url, the existing clone should be removed
and a new clone from the correct canonical_url should be created instead of
calling refresh_repo.

In `@backend/cliff/repos/identity.py`:
- Around line 44-56: The code extracts only the hostname using parsed.hostname
on line 45, which discards port information and causes different URLs with
non-default ports to be treated identically. Replace the hostname extraction to
preserve port information by using parsed.netloc (which includes both hostname
and port) instead of parsed.hostname, then normalize it by converting to
lowercase. Update the assignment on line 45 where host is set and ensure the
port information is retained when the canonical URL is constructed on line 56 in
the return statement.

In `@backend/cliff/repos/repo_dir_manager.py`:
- Around line 72-85: The write_artifact and read_artifact methods in
repo_dir_manager.py currently handle all artifacts as raw dictionaries without
schema validation, allowing malformed data to be persisted and consumed
downstream. To fix this, modify write_artifact to validate the incoming data
parameter against the appropriate Pydantic model (RepoProfile, CodeMap, or
ThreatHistory depending on the artifact name) before calling _atomic_write_json,
and modify read_artifact to deserialize the JSON into the corresponding Pydantic
model instance and return that typed object instead of a raw dict. Map each
artifact name to its corresponding Pydantic model using a lookup structure, and
apply the same validation pattern to the affected code at lines 144-145.
- Around line 136-142: The _atomic_write_text function uses a fixed temporary
filename pattern where tmp is assigned using path.with_name(path.name + ".tmp"),
which causes collisions when multiple concurrent writes target the same file.
Replace this fixed naming scheme with a unique temporary file path per write
operation. Use a method like Python's tempfile module or generate a unique
identifier (such as UUID or process/thread ID) to create distinct temp filenames
for each write attempt, ensuring concurrent writers do not overwrite each
other's temporary files before the final os.replace operation.

In `@backend/cliff/repos/service.py`:
- Around line 73-91: The import of _github_token_from_integration, the
build_profile_runner call, and the asyncio.create_task scheduling block are all
unguarded and can raise exceptions that propagate to the caller, contradicting
the function's documented behavior. Wrap the entire code block from the import
statement through the final app.state.profile_tasks assignment in a try-except
block to catch any failures and log them instead of raising to the caller,
ensuring best-effort scheduling semantics are maintained.

In `@backend/tests/agents/test_model_tiers.py`:
- Line 18: The chained inequality assertion `assert ids["cheap"] !=
ids["strong"] != ids["judge"]` does not verify that all three tier IDs are
pairwise distinct; it only checks that cheap differs from strong AND strong
differs from judge, but not that cheap differs from judge. Replace this
assertion with a set cardinality check that converts the three ID values
(ids["cheap"], ids["strong"], ids["judge"]) into a set and verifies the set has
exactly 3 elements, ensuring all three tiers are truly distinct from one
another.

In `@frontend/src/components/issues/__tests__/IssueSidePanel.test.tsx`:
- Around line 1082-1107: The test data for DEEP_DIVE_TRIAGE includes a
repro_recipe with a docker_compose field set to null, but there is no
corresponding assertion verifying that this field is correctly rendered in the
TriageReproRecipe component. Add an assertion that validates docker_compose is
properly displayed or handled in the UI at both test locations (the anchor site
and the sibling site referenced in consolidated_sites) to close the contract gap
and catch future regressions in recipe field rendering.

In `@frontend/src/components/issues/IssueSidePanel.tsx`:
- Around line 1062-1098: The ReproRecipe function includes recipe.docker_compose
in the has visibility check but does not render its content in the JSX. Add a
conditional rendering block for docker_compose in the flex container (alongside
the existing blocks for image, setup, trigger, and expected_observation) to
display the docker_compose value when it exists, ensuring that valid recipe
details are not dropped from the UI and the visibility logic matches the actual
rendered content.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 19c611af-8573-438b-99d2-b6f53cc17d5e

📥 Commits

Reviewing files that changed from the base of the PR and between 6d0cffb and 2c3f73e.

📒 Files selected for processing (68)
  • backend/cliff/agents/runtime/deps.py
  • backend/cliff/agents/runtime/model_tiers.py
  • backend/cliff/agents/runtime/tools/__init__.py
  • backend/cliff/agents/runtime/tools/grep.py
  • backend/cliff/agents/runtime/tools/read.py
  • backend/cliff/agents/schemas.py
  • backend/cliff/agents/triage_deep/__init__.py
  • backend/cliff/agents/triage_deep/agents.py
  • backend/cliff/agents/triage_deep/challenge.py
  • backend/cliff/agents/triage_deep/escalation.py
  • backend/cliff/agents/triage_deep/integration.py
  • backend/cliff/agents/triage_deep/runner.py
  • backend/cliff/agents/triage_runner.py
  • backend/cliff/api/routes/agent_execution.py
  • backend/cliff/api/routes/assessment.py
  • backend/cliff/api/routes/repos.py
  • backend/cliff/db/migrations/025_repo_entity.sql
  • backend/cliff/evals/__init__.py
  • backend/cliff/evals/cases.py
  • backend/cliff/evals/deep_dive_evaluators.py
  • backend/cliff/evals/repo_fetch.py
  • backend/cliff/evals/runners.py
  • backend/cliff/main.py
  • backend/cliff/models/__init__.py
  • backend/cliff/models/repo.py
  • backend/cliff/repos/__init__.py
  • backend/cliff/repos/clone.py
  • backend/cliff/repos/dao.py
  • backend/cliff/repos/gc.py
  • backend/cliff/repos/git_ops.py
  • backend/cliff/repos/identity.py
  • backend/cliff/repos/knowledge.py
  • backend/cliff/repos/profile_agents.py
  • backend/cliff/repos/profile_runner.py
  • backend/cliff/repos/repo_dir_manager.py
  • backend/cliff/repos/schemas.py
  • backend/cliff/repos/service.py
  • backend/tests/agents/eval/triage_deep_dive.jsonl
  • backend/tests/agents/test_deep_dive_agents.py
  • backend/tests/agents/test_deep_dive_integration.py
  • backend/tests/agents/test_deep_dive_runner.py
  • backend/tests/agents/test_deep_dive_schemas.py
  • backend/tests/agents/test_escalation.py
  • backend/tests/agents/test_evals_deep_dive.py
  • backend/tests/agents/test_grep_tool.py
  • backend/tests/agents/test_model_tiers.py
  • backend/tests/agents/test_read_budget.py
  • backend/tests/agents/test_repo_fetch.py
  • backend/tests/api/openapi_snapshot.json
  • backend/tests/repos/__init__.py
  • backend/tests/repos/conftest.py
  • backend/tests/repos/test_clone.py
  • backend/tests/repos/test_git_ops.py
  • backend/tests/repos/test_identity.py
  • backend/tests/repos/test_knowledge.py
  • backend/tests/repos/test_profile_agents.py
  • backend/tests/repos/test_profile_runner.py
  • backend/tests/repos/test_repo_dao.py
  • backend/tests/repos/test_repo_dir_manager.py
  • backend/tests/repos/test_service.py
  • backend/tests/test_routes_repos.py
  • frontend/src/api/__tests__/repos.test.tsx
  • frontend/src/api/client.ts
  • frontend/src/api/repos.ts
  • frontend/src/components/dashboard/ProjectProfileCard.tsx
  • frontend/src/components/issues/IssueSidePanel.tsx
  • frontend/src/components/issues/__tests__/IssueSidePanel.test.tsx
  • frontend/src/pages/DashboardPage.tsx

Comment thread backend/cliff/agents/runtime/deps.py
Comment thread backend/cliff/agents/runtime/model_tiers.py Outdated
Comment thread backend/cliff/agents/triage_deep/runner.py
Comment thread backend/cliff/agents/triage_runner.py Outdated
Comment thread backend/cliff/api/routes/repos.py
Comment on lines +136 to +142
def _atomic_write_text(path: Path, text: str) -> None:
"""Write *text* atomically — a concurrent reader sees old-or-new, never torn."""
path.parent.mkdir(parents=True, exist_ok=True)
tmp = path.with_name(path.name + ".tmp")
tmp.write_text(text)
os.replace(tmp, path)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Use a unique temp path per write; fixed *.tmp names can collide.

At Line 139, path.with_name(path.name + ".tmp") is shared by all concurrent writers to the same artifact. Two overlapping writes can clobber each other’s temp file and fail/lose updates before os.replace.

Suggested fix
+import tempfile
+
 def _atomic_write_text(path: Path, text: str) -> None:
     """Write *text* atomically — a concurrent reader sees old-or-new, never torn."""
     path.parent.mkdir(parents=True, exist_ok=True)
-    tmp = path.with_name(path.name + ".tmp")
-    tmp.write_text(text)
-    os.replace(tmp, path)
+    fd, tmp_name = tempfile.mkstemp(prefix=f".{path.name}.", suffix=".tmp", dir=path.parent)
+    try:
+        with os.fdopen(fd, "w", encoding="utf-8") as f:
+            f.write(text)
+        os.replace(tmp_name, path)
+    finally:
+        try:
+            os.unlink(tmp_name)
+        except FileNotFoundError:
+            pass
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/cliff/repos/repo_dir_manager.py` around lines 136 - 142, The
_atomic_write_text function uses a fixed temporary filename pattern where tmp is
assigned using path.with_name(path.name + ".tmp"), which causes collisions when
multiple concurrent writes target the same file. Replace this fixed naming
scheme with a unique temporary file path per write operation. Use a method like
Python's tempfile module or generate a unique identifier (such as UUID or
process/thread ID) to create distinct temp filenames for each write attempt,
ensuring concurrent writers do not overwrite each other's temporary files before
the final os.replace operation.

Comment on lines +73 to +91
from cliff.api._engine_dep import _github_token_from_integration

runner = build_profile_runner(
db, model=model, token_provider=_github_token_from_integration
)

tasks: set[asyncio.Task[None]] = getattr(app.state, "profile_tasks", None) or set()

async def _run() -> None:
try:
await runner.build(repo_url)
except Exception:
logger.exception("eager profile build failed for %s", repo_url)

task = asyncio.create_task(_run(), name=f"profile:{repo_url}")
tasks.add(task)
task.add_done_callback(tasks.discard)
app.state.profile_tasks = tasks
return task

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Best-effort scheduler still has unguarded exception paths.

This function documents “never raises into the caller,” but the import/runner construction/task scheduling block is outside try. Failures there can propagate to /assessment/run and break request handling.

Suggested fix
-    from cliff.api._engine_dep import _github_token_from_integration
-
-    runner = build_profile_runner(
-        db, model=model, token_provider=_github_token_from_integration
-    )
-
-    tasks: set[asyncio.Task[None]] = getattr(app.state, "profile_tasks", None) or set()
-
-    async def _run() -> None:
-        try:
-            await runner.build(repo_url)
-        except Exception:
-            logger.exception("eager profile build failed for %s", repo_url)
-
-    task = asyncio.create_task(_run(), name=f"profile:{repo_url}")
-    tasks.add(task)
-    task.add_done_callback(tasks.discard)
-    app.state.profile_tasks = tasks
-    return task
+    try:
+        from cliff.api._engine_dep import _github_token_from_integration
+
+        runner = build_profile_runner(
+            db, model=model, token_provider=_github_token_from_integration
+        )
+
+        tasks: set[asyncio.Task[None]] = getattr(app.state, "profile_tasks", None) or set()
+
+        async def _run() -> None:
+            try:
+                await runner.build(repo_url)
+            except Exception:
+                logger.exception("eager profile build failed for %s", repo_url)
+
+        task = asyncio.create_task(_run(), name=f"profile:{repo_url}")
+        tasks.add(task)
+        task.add_done_callback(tasks.discard)
+        app.state.profile_tasks = tasks
+        return task
+    except Exception:
+        logger.exception("profile build scheduling failed for %s", repo_url)
+        return None
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/cliff/repos/service.py` around lines 73 - 91, The import of
_github_token_from_integration, the build_profile_runner call, and the
asyncio.create_task scheduling block are all unguarded and can raise exceptions
that propagate to the caller, contradicting the function's documented behavior.
Wrap the entire code block from the import statement through the final
app.state.profile_tasks assignment in a try-except block to catch any failures
and log them instead of raising to the caller, ensuring best-effort scheduling
semantics are maintained.

assert ids["strong"] == "anthropic/claude-sonnet-4-6"
assert ids["judge"] == "anthropic/claude-opus-4-8"
# judge out-ranks strong → a real second opinion.
assert ids["cheap"] != ids["strong"] != ids["judge"]

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Use a true pairwise-distinct assertion for three tiers.

Line 18 uses chained !=, which does not verify cheap != judge. For a “three distinct tiers” invariant, assert set cardinality instead.

Suggested fix
-    assert ids["cheap"] != ids["strong"] != ids["judge"]
+    assert len({ids["cheap"], ids["strong"], ids["judge"]}) == 3
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
assert ids["cheap"] != ids["strong"] != ids["judge"]
assert len({ids["cheap"], ids["strong"], ids["judge"]}) == 3
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/tests/agents/test_model_tiers.py` at line 18, The chained inequality
assertion `assert ids["cheap"] != ids["strong"] != ids["judge"]` does not verify
that all three tier IDs are pairwise distinct; it only checks that cheap differs
from strong AND strong differs from judge, but not that cheap differs from
judge. Replace this assertion with a set cardinality check that converts the
three ID values (ids["cheap"], ids["strong"], ids["judge"]) into a set and
verifies the set has exactly 3 elements, ensuring all three tiers are truly
distinct from one another.

Comment on lines +1082 to +1107
const DEEP_DIVE_TRIAGE = {
verdict: 'real',
confidence: 0.9,
recommended_close: null,
reachability: { reached: true, path: [{ label: 'entry', detail: 'app.py:1' }], summary: null },
exploitability: { exploitable: 'yes', reason: 'reachable with a credible path' },
report: null,
checks: [],
exploit_plan: {
hypotheses: [
{
id: 'h1',
trigger_condition: 'GET /file= with a URL-like value',
attacker_input: 'http://169.254.169.254/latest/meta-data/',
reached_sink: 'gradio/routes.py:438',
expected_impact: 'SSRF to the cloud metadata endpoint',
impact_class: 'SSRF',
confidence: 0.8,
repro_recipe: {
setup: ['pip install gradio'],
docker_compose: null,
image: 'gradio:vuln',
ports: [7860],
trigger: ['curl "http://target/file=http://169.254.169.254/"'],
expected_observation: 'metadata document returned',
},

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial | ⚡ Quick win

Add one assertion for docker_compose rendering in deep-dive recipe tests.

This closes a contract gap for TriageReproRecipe so regressions in recipe field rendering are caught early.

Suggested test update
 const DEEP_DIVE_TRIAGE = {
@@
         repro_recipe: {
           setup: ['pip install gradio'],
-          docker_compose: null,
+          docker_compose: 'docker-compose.yml',
           image: 'gradio:vuln',
           ports: [7860],
           trigger: ['curl "http://target/file=http://169.254.169.254/"'],
           expected_observation: 'metadata document returned',
         },
@@
   it('renders the exploit plan with its hypothesis + repro recipe', async () => {
@@
     expect(screen.getByText(/Reproduction recipe \(plan\)/)).toBeInTheDocument()
+    expect(screen.getByText(/docker compose:\s*docker-compose\.yml/i)).toBeInTheDocument()
   })

Also applies to: 1153-1161

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/src/components/issues/__tests__/IssueSidePanel.test.tsx` around
lines 1082 - 1107, The test data for DEEP_DIVE_TRIAGE includes a repro_recipe
with a docker_compose field set to null, but there is no corresponding assertion
verifying that this field is correctly rendered in the TriageReproRecipe
component. Add an assertion that validates docker_compose is properly displayed
or handled in the UI at both test locations (the anchor site and the sibling
site referenced in consolidated_sites) to close the contract gap and catch
future regressions in recipe field rendering.

Comment thread frontend/src/components/issues/IssueSidePanel.tsx
galanko and others added 3 commits June 14, 2026 17:56
…ding-gate hole (ADR-0052/0054)

Self-review of the branch surfaced real defects in the no-false-clear path:

P0 — _gate_clear left a stale recommended_close (unexploitable/false_positive)
after downgrading a clear to needs_review (model_copy skips the validator), so the
UI pre-selected the dismissal the tier gate just refused. Now sets None + re-tones
surviving 'pass' checks to info.

P1 — _kill_corroborated false-clear vectors at the cheap gate:
  * root_cause_in_nonship_code used any() — one stray test file alongside real
    ship-code cleared the finding. Now requires ALL candidates non-ship (+ empty
    guard).
  * duplicate_of_known cleared on any prior issue in history. Now requires the
    kill's dedup_match to name a prior issue that actually exists.

Eval gate hole — check_citation_grounding skipped checks[].detail without a '/',
but the disproof guard_location / rule_out kill_evidence (a CLEAR verdict's
load-bearing citation) is a bare file:line (e.g. auth.py:10). A fabricated guard
at a nonexistent file passed the HARD grounding gate. Now grounds every detail;
line 0 no longer resolves.

Eval robustness — run_deep_dive_eval now raises on 0 cases (no silent PASS) and
isolates per-case infra failures (one bad checkout no longer aborts the run);
the live pipeline threads traced_sha for provenance.

Runner now degrades httpx.TransportError to needs_review (never crash). Frontend:
render docker_compose in the repro recipe; don't render an empty provenance panel
on the incomplete path. New/updated tests for every fix; backend (changed code) +
frontend green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…e UI

Cyberdeck /impeccable polish pass: green is the safe/status-good accent, used
sparingly. Removed it from two spots where it was decorative or semantically
backwards on a real finding — the 'Primary' exploit-hypothesis badge (the main
attack path is not 'safe' → neutral fg-2, first position carries the emphasis)
and the 'Verdict held' challenge icon (confirmatory, not safe → neutral fg-3;
amber still flags a downgrade). 'Primary' now only labels when there's >1
hypothesis to rank (redundant on a single one). tsc + eslint + 52 tests green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Bot review of #268. The two highest findings (the _kill_corroborated dedup_match
corroboration + the empty-dataset guard) were already closed by the earlier
self-review pass. This batch:

- ReadBudget.take(): reject an over-budget request instead of letting the first
  one go negative — the off-by-one let one oversized read overflow the context
  (CodeRabbit, deps.py).
- clearing_is_trusted(): reject malformed ids (no '/') — only a real
  provider/model config can be trusted to auto-dismiss (CodeRabbit, model_tiers).
- citation grounding: constrain resolution to repo_dir (a '..'/absolute citation
  can no longer pass by matching a host file) + line 0 is out of range
  (CodeRabbit, deep_dive_evaluators).
- EvalCase: enforce the dataset-mode invariant (repo+sha together, not alongside
  files) so a partial case can't silently fall back to synthetic (CodeRabbit).
- repos profile route: InvalidRepoUrlError -> 422, not 500 (CodeRabbit).
- _run_scanner_triage: finding typed Finding, not Any (CodeRabbit, coding guide).
- report path now routes through maybe_deep_dive(source='report') so reports can
  escalate to the Deep dive like scanner findings, best-effort (Baz).

Tests updated for the corrected ReadBudget semantics + new tests for the citation
repo-escape and the EvalCase invariant. Affected backend tests + lint green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Comment on lines +92 to +95
if (self.repo is None) != (self.sha is None):
raise ValueError("repo and sha must be provided together")
if self.repo is not None and self.files is not None:
raise ValueError("a case is real (repo+sha) or synthetic (files), not both")

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

_dataset_mode_invariant() only checks is None, so blank/whitespace repo/sha still reach checkout_at_sha() via run_deep_dive_eval() and fail later — should we reject or normalize empty strings here?

Severity

Want Baz to fix this for you? Activate Fixer

Other fix methods

Fix in Cursor

Prompt for AI Agents
Before applying, verify this suggestion against the current code. In
backend/cliff/evals/cases.py around lines 87-96, update EvalCase._dataset_mode_invariant
to treat repo/sha values that are empty or whitespace-only as missing (normalize to None
or raise a clear ValueError). Right now the validator only checks for None, so
whitespace strings can pass and later trigger infra failures when run_deep_dive_eval
tries to checkout. After normalization, re-apply the invariant: repo and sha must both
be provided together, and files must be mutually exclusive with real repo+sha. Ensure
the error messages mention that repo/sha cannot be blank.

Heads up!

Your free trial ends tomorrow.
To keep getting your PRs reviewed by Baz, update your team's subscription

Comment on lines +244 to +248
# Escalate to the agentic Deep dive when warranted (ADR-0052 — the report
# variant starts at gather_facts). Best-effort: any failure keeps the report
# triager's verdict — triage never breaks.
final = quick
try:

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

The new deep-dive escalation block mirrors _run_scanner_triage, should we extract the shared best-effort pattern into a helper like _escalate_deep_dive(...) so future changes only land once?

Severity

Want Baz to fix this for you? Activate Fixer

Heads up!

Your free trial ends tomorrow.
To keep getting your PRs reviewed by Baz, update your team's subscription

@galanko galanko merged commit 0574997 into main Jun 14, 2026
8 checks passed
@galanko galanko deleted the feat/triage-agentic-workflow branch June 14, 2026 15:59
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant