-
Notifications
You must be signed in to change notification settings - Fork 0
feat: triage agentic workflow (IMPL-0025) — WIP #268
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
34 commits
Select commit
Hold shift + click to select a range
ddd02bc
feat(repos): repo entity + canonicalizer + per-repo profile store (AD…
galanko 684830e
feat(repos): RepoKnowledge declared-consumption access layer (ADR-005…
galanko efa7ccd
feat(repos): credential-less cached clone + clone GC (ADR-0053 P1.4)
galanko 6848570
feat(repos): ProfileRunner — per-repo profile build orchestration (AD…
galanko a882b21
feat(repos): the three Project-profile builder agents (ADR-0053 P1.6)
galanko 0a33b78
feat(repos): eager profile build at scan time (ADR-0053/PRD-0009 P1.7)
galanko 5bd47d4
feat(api): repo Project-profile endpoints (ADR-0053/PRD-0009 P1.8 bac…
galanko fe4ffc9
feat(dashboard): Project profile card (ADR-0053/PRD-0009 P1.8 frontend)
galanko b615d25
feat(triage): deep-dive schemas + additive TriageOutput blocks (ADR-0…
galanko b2243ba
feat(triage): grep tool + model tiering + escalation gate (ADR-0052 P…
galanko 686b791
feat(triage): the five Deep dive agents + adversarial challenge panel…
galanko 7d0316a
style(triage): ruff autofix on deep-dive agent test
galanko babe17a
feat(triage): DeepDiveRunner orchestration with fail-cheap exits (ADR…
galanko b84118e
style(triage): ruff format + TC003 on DeepDiveRunner
galanko 83af024
feat(triage): wire escalation → Deep dive into the triage path (ADR-0…
galanko 4b5837a
feat(evals): Deep dive eval harness — deterministic hard gates (ADR-0…
galanko 083a6bd
feat(evals): export deep-dive eval entrypoints from cliff.evals (ADR-…
galanko 47356a1
feat(evals): real repo@sha checkout for the Deep dive live lane (ADR-…
galanko ee7e87f
fix(triage): real-model robustness — grep symlink crash + graceful bu…
galanko 13146e5
fix(triage): context-budget the Deep dive + conservative rule_out (AD…
galanko bf928ef
fix(triage): rule_out kills require structural corroboration — kill t…
galanko 2c9ba5f
fix(triage): retry transient provider errors (429/503) in the Deep di…
galanko b388e41
fix(triage): tougher retry/backoff for Gemini 503s (attempts 4->6, ca…
galanko 9b55926
fix(triage): sequential challenge panel to cut Gemini 503 bursts (ADR…
galanko ed7229e
fix(triage): accuracy R&D — guard-hunt in trace, refute-on-concrete i…
galanko d5c14f3
fix(triage): challenge the disproof — close the false-clear hole (ADR…
galanko 691b783
fix(triage): tighten disproof bypass lens — hold on correct guards (A…
galanko c60da5d
fix(triage): majority disproof + temp=0 + trace guard-recognition (AD…
galanko b4499a5
fix(triage): bypass-veto disproof resolution — close the majority fal…
galanko 14e96ae
feat(triage): clearing gate — weak judge tiers can detect+flag but ne…
galanko 2c3f73e
feat(triage): Phase 4 — surface the Deep dive in the triage UI (ADR-0…
galanko 46b0f2f
fix(triage): code-review pass — close 2 false-clear vectors + a groun…
galanko 33fd7fb
polish(triage): keep sage-mint for safe outcomes only in the deep-div…
galanko b27affb
fix(triage): address CodeRabbit + Baz review (ADR-0052/0054)
galanko File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,71 @@ | ||
| """Deep dive model tiering (ADR-0052 §4, amends ADR-0037). | ||
|
|
||
| ADR-0037 keeps one canonical provider + one model. The Deep dive needs three | ||
| tiers — cheap (volume), strong (the moat: reachability + exploit planning), and | ||
| judge (the adversarial challenge, which must out-rank the generator per | ||
| ADR-0050's anti-self-preference rule). We *derive* the tier map from the | ||
| configured provider's own lineup, so there's still one credential. | ||
|
|
||
| Thin-lineup providers (ollama / custom) fall back to the single configured model | ||
| for every tier — the judge is then not independent (the caller logs that). | ||
| """ | ||
|
|
||
| from __future__ import annotations | ||
|
|
||
| #: (cheap, strong, judge) model ids per provider, without the provider prefix. | ||
| _LINEUP: dict[str, tuple[str, str, str]] = { | ||
| "anthropic": ("claude-haiku-4-5", "claude-sonnet-4-6", "claude-opus-4-8"), | ||
| "openrouter": ( | ||
| "anthropic/claude-haiku-4.5", | ||
| "anthropic/claude-sonnet-4.6", | ||
| "anthropic/claude-opus-4.8", | ||
| ), | ||
| "openai": ("gpt-5-mini", "gpt-5", "gpt-5"), | ||
| "google": ("gemini-2.5-flash", "gemini-2.5-pro", "gemini-2.5-pro"), | ||
| } | ||
|
|
||
| TIERS = ("cheap", "strong", "judge") | ||
|
|
||
|
|
||
| def resolve_tier_model_ids(model_full_id: str) -> dict[str, str]: | ||
| """Derive ``{cheap, strong, judge}`` ``<provider>/<model>`` ids. | ||
|
|
||
| Falls back to the single configured model for every tier when the provider | ||
| has no known lineup (ollama / custom) or the id isn't ``provider/model``. | ||
| """ | ||
| provider, sep, _ = model_full_id.partition("/") | ||
| lineup = _LINEUP.get(provider) | ||
| if not sep or lineup is None: | ||
| return {tier: model_full_id for tier in TIERS} | ||
| cheap, strong, judge = lineup | ||
| return { | ||
| "cheap": f"{provider}/{cheap}", | ||
| "strong": f"{provider}/{strong}", | ||
| "judge": f"{provider}/{judge}", | ||
| } | ||
|
|
||
|
|
||
| def judge_is_independent(model_full_id: str) -> bool: | ||
| """True when the judge tier out-ranks the strong tier (a real second opinion).""" | ||
| ids = resolve_tier_model_ids(model_full_id) | ||
| return ids["judge"] != ids["strong"] | ||
|
|
||
|
|
||
| def clearing_is_trusted(model_full_id: str) -> bool: | ||
| """True when the config has a known strong-judge lineup, so the Deep dive may | ||
| emit a DISMISSAL verdict (``unexploitable`` / ``false_positive``). | ||
|
|
||
| Clearing is the only verdict that can HIDE a real vuln, so it requires a | ||
| capable judge tier. A thin-lineup config (ollama / custom / unrecognized id) | ||
| collapses every tier to one possibly-weak model — there the deep dive may | ||
| DETECT (``real``) and FLAG (``needs_review``) on any tier, but must never | ||
| auto-dismiss (the flash-judge false-clears were exactly this weak-judge | ||
| failure). Known-lineup providers always derive a strong judge (opus / gpt-5 / | ||
| pro), so dismissal is trusted.""" | ||
| provider, sep, model = model_full_id.partition("/") | ||
| # A malformed id (no '/', e.g. "openai") isn't a valid provider/model config — | ||
| # never trust dismissal on it. | ||
| return bool(sep and model) and provider in _LINEUP | ||
|
|
||
|
|
||
| __all__ = ["TIERS", "clearing_is_trusted", "judge_is_independent", "resolve_tier_model_ids"] |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,88 @@ | ||
| """``grep`` tool — read-only regex search over the workspace (ADR-0052 §3). | ||
|
|
||
| The Deep dive's code-walkers (``trace_path`` / ``challenge``) need to find | ||
| references without a shell. This is pure-Python regex search scoped to the | ||
| workspace (the cached clone), no subprocess, so it's safe (read-only, can't | ||
| escape) and deterministic (gradeable by the eval). Auto-tier, like ``read``. | ||
| """ | ||
|
|
||
| from __future__ import annotations | ||
|
|
||
| import asyncio | ||
| import re | ||
| from pathlib import Path | ||
|
|
||
| from pydantic_ai import RunContext | ||
|
|
||
| from cliff.agents.runtime.deps import WorkspaceDeps | ||
| from cliff.agents.runtime.tools.permissions import escapes_workspace | ||
|
|
||
| _MAX_MATCHES = 100 | ||
| _MAX_FILE_BYTES = 1024 * 1024 | ||
| _SKIP_DIRS = frozenset( | ||
| {".git", "node_modules", ".venv", "venv", "__pycache__", "dist", "build", ".mypy_cache"} | ||
| ) | ||
|
|
||
|
|
||
| def search_workspace(workspace_dir: str, pattern: str, path: str = ".") -> str: | ||
| """Search files under *workspace_dir*/*path* for *pattern* (regex). | ||
|
|
||
| Returns up to 100 ``relpath:line: text`` matches, or a bracketed status | ||
| ([refused] / [invalid regex] / [no matches]). Pure + synchronous so it's | ||
| unit-testable without a RunContext; the tool wraps it on a thread. | ||
| """ | ||
| if escapes_workspace(workspace_dir, path): | ||
| return f"[refused: {path} resolves outside the workspace]" | ||
| try: | ||
| rx = re.compile(pattern) | ||
| except re.error as exc: | ||
| return f"[invalid regex: {exc}]" | ||
|
|
||
| # Resolve the root too: on macOS a temp dir like /var/folders/... canonicalizes | ||
| # to /private/var/folders/..., and base.resolve() below follows that symlink — | ||
| # so an unresolved root would break relative_to() on every match. | ||
| root = Path(workspace_dir).resolve() | ||
| base = (root / path).resolve() | ||
| if not base.exists(): | ||
| return f"[path not found: {path}]" | ||
|
|
||
| matches: list[str] = [] | ||
| candidates = [base] if base.is_file() else base.rglob("*") | ||
| for f in candidates: | ||
| if len(matches) >= _MAX_MATCHES: | ||
| break | ||
| if not f.is_file() or any(part in _SKIP_DIRS for part in f.parts): | ||
| continue | ||
| try: | ||
| if f.stat().st_size > _MAX_FILE_BYTES: | ||
| continue | ||
| with f.open("r", encoding="utf-8", errors="replace") as fh: | ||
| for lineno, line in enumerate(fh, 1): | ||
| if rx.search(line): | ||
| rel = f.relative_to(root) | ||
| matches.append(f"{rel}:{lineno}: {line.rstrip()[:200]}") | ||
| if len(matches) >= _MAX_MATCHES: | ||
| break | ||
| except OSError: | ||
| continue | ||
|
|
||
| if not matches: | ||
| return f"[no matches for {pattern!r}]" | ||
| out = "\n".join(matches) | ||
| if len(matches) >= _MAX_MATCHES: | ||
| out += f"\n[... capped at {_MAX_MATCHES} matches ...]" | ||
| return out | ||
|
|
||
|
|
||
| async def grep(ctx: RunContext[WorkspaceDeps], pattern: str, path: str = ".") -> str: | ||
| """Search the workspace for *pattern* (regex), under *path* (default: root).""" | ||
| out = await asyncio.to_thread( | ||
| search_workspace, ctx.deps.workspace_dir, pattern, path | ||
| ) | ||
| budget = ctx.deps.read_budget | ||
| if budget is not None and not budget.take(len(out.encode("utf-8", errors="ignore"))): | ||
| return "[grep budget exhausted for this analysis]" | ||
| return out | ||
|
|
||
|
|
||
| __all__ = ["grep", "search_workspace"] |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.