Skip to content

Commit 832dd76

Browse files
authored
Merge branch 'main' into ci/claude-review-allowed-bots
2 parents e8537c7 + b521d71 commit 832dd76

94 files changed

Lines changed: 2443 additions & 725 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,3 +98,4 @@ tests/e2e/.e2e-state.db*
9898
test_audit_report.md
9999
tests/integration/.env.integration
100100
web-ui/test-results/
101+
.opencode-tmp/

codeframe/cli/app.py

Lines changed: 29 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -4017,14 +4017,19 @@ def batch_status(
40174017

40184018
if batch_id:
40194019
# Show specific batch
4020-
# Find by partial ID
4021-
all_batches = conductor.list_batches(workspace, limit=100)
4022-
matching = [b for b in all_batches if b.id.startswith(batch_id)]
4020+
# Find by partial ID (SQL LIKE — no cap, resolves batches beyond 100)
4021+
matching = conductor.find_batch_by_prefix(workspace, batch_id)
40234022

40244023
if not matching:
40254024
console.print(f"[red]Error:[/red] No batch found matching '{batch_id}'")
40264025
raise typer.Exit(1)
40274026

4027+
if len(matching) > 1:
4028+
console.print(f"[red]Error:[/red] Multiple batches match '{batch_id}':")
4029+
for b in matching[:5]:
4030+
console.print(f" {b.id[:8]} ({b.status.value})")
4031+
raise typer.Exit(1)
4032+
40284033
batch = matching[0]
40294034

40304035
# Status color
@@ -4162,14 +4167,19 @@ def batch_stop(
41624167
try:
41634168
workspace = get_workspace(path)
41644169

4165-
# Find by partial ID
4166-
all_batches = conductor.list_batches(workspace, limit=100)
4167-
matching = [b for b in all_batches if b.id.startswith(batch_id)]
4170+
# Find by partial ID (SQL LIKE — no cap, resolves batches beyond 100)
4171+
matching = conductor.find_batch_by_prefix(workspace, batch_id)
41684172

41694173
if not matching:
41704174
console.print(f"[red]Error:[/red] No batch found matching '{batch_id}'")
41714175
raise typer.Exit(1)
41724176

4177+
if len(matching) > 1:
4178+
console.print(f"[red]Error:[/red] Multiple batches match '{batch_id}':")
4179+
for b in matching[:5]:
4180+
console.print(f" {b.id[:8]} ({b.status.value})")
4181+
raise typer.Exit(1)
4182+
41734183
batch = matching[0]
41744184

41754185
if batch.status.value not in ("PENDING", "RUNNING"):
@@ -4231,19 +4241,18 @@ def batch_resume(
42314241
try:
42324242
workspace = get_workspace(path)
42334243

4234-
# Find by partial ID
4235-
all_batches = conductor.list_batches(workspace, limit=100)
4236-
matching = [b for b in all_batches if b.id.startswith(batch_id)]
4244+
# Find by partial ID (SQL LIKE — no cap, resolves batches beyond 100)
4245+
matching = conductor.find_batch_by_prefix(workspace, batch_id)
42374246

42384247
if not matching:
42394248
console.print(f"[red]Error:[/red] No batch found matching '{batch_id}'")
42404249
raise typer.Exit(1)
42414250

42424251
if len(matching) > 1:
4243-
console.print(f"[yellow]Warning:[/yellow] Multiple batches match '{batch_id}':")
4252+
console.print(f"[red]Error:[/red] Multiple batches match '{batch_id}':")
42444253
for b in matching[:5]:
4245-
console.print(f" - {b.id[:8]} ({b.status.value})")
4246-
console.print("Using the most recent match.")
4254+
console.print(f" {b.id[:8]} ({b.status.value})")
4255+
raise typer.Exit(1)
42474256

42484257
batch = matching[0]
42494258

@@ -4430,14 +4439,19 @@ def _update_progress(progress: BatchProgress, event: events.Event) -> None:
44304439
try:
44314440
workspace = get_workspace(path)
44324441

4433-
# Find batch by partial ID
4434-
all_batches = conductor.list_batches(workspace, limit=100)
4435-
matching = [b for b in all_batches if b.id.startswith(batch_id)]
4442+
# Find batch by partial ID (SQL LIKE — no cap, resolves batches beyond 100)
4443+
matching = conductor.find_batch_by_prefix(workspace, batch_id)
44364444

44374445
if not matching:
44384446
console.print(f"[red]Error:[/red] No batch found matching '{batch_id}'")
44394447
raise typer.Exit(1)
44404448

4449+
if len(matching) > 1:
4450+
console.print(f"[red]Error:[/red] Multiple batches match '{batch_id}':")
4451+
for b in matching[:5]:
4452+
console.print(f" {b.id[:8]} ({b.status.value})")
4453+
raise typer.Exit(1)
4454+
44414455
batch = matching[0]
44424456
batch_id_short = batch.id[:8]
44434457

codeframe/core/adapters/streaming_chat.py

Lines changed: 30 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -169,24 +169,40 @@ def __init__(
169169
db_repo: ``InteractiveSessionRepository`` instance.
170170
workspace_path: Absolute path used to scope file-system tool calls.
171171
model: Model identifier passed to the provider's ``async_stream()``.
172-
api_key: API key used when constructing the default
173-
``AnthropicProvider`` (when ``provider`` is ``None``).
174-
provider: LLM provider to use for streaming. When ``None``,
175-
an ``AnthropicProvider`` is constructed automatically using
176-
``api_key`` or the ``ANTHROPIC_API_KEY`` environment variable.
172+
Also threaded into ``resolve_llm_settings`` as ``model_flag``
173+
when the provider must be resolved (#861).
174+
api_key: Legacy construct — when set AND ``provider`` is ``None``,
175+
an ``AnthropicProvider`` is constructed with this key
176+
(matches the pre-#861 contract). Ignored if ``provider`` is
177+
given. Prefer passing ``provider`` directly.
178+
provider: LLM provider to use for streaming. When ``None`` and
179+
``api_key`` is also ``None``, the provider is resolved via the
180+
shared ``llm_resolution`` chain (#861):
181+
``CODEFRAME_LLM_PROVIDER`` → ``.codeframe/config.yaml`` →
182+
anthropic.
177183
178184
Raises:
179-
ValueError: If no provider is given and no Anthropic API key is
180-
available.
185+
ValueError: If no provider is given, no ``api_key`` is given, and
186+
the chain resolves to a provider whose required API key is
187+
missing from the environment.
181188
"""
182189
if provider is None:
183-
# Backward-compatibility fallback: callers that haven't been
184-
# updated to pass an explicit provider (e.g. tests using the old
185-
# api_key= constructor argument) still get an AnthropicProvider.
186-
# New callers should construct the provider themselves and pass it
187-
# in — see session_chat_ws.py for the recommended pattern.
188-
from codeframe.adapters.llm.anthropic import AnthropicProvider
189-
provider = AnthropicProvider(api_key=api_key)
190+
if api_key is not None:
191+
# Legacy path: explicit api_key always means Anthropic.
192+
# Preserves the pre-#861 contract (mirrors PrdDiscoverySession).
193+
from codeframe.adapters.llm.anthropic import AnthropicProvider
194+
195+
provider = AnthropicProvider(api_key=api_key)
196+
else:
197+
# New: resolve via the shared chain
198+
# (CODEFRAME_LLM_PROVIDER → .codeframe/config.yaml → anthropic,
199+
# per #861). New callers should construct the provider
200+
# themselves and pass it in — see session_chat_ws.py for the
201+
# recommended pattern.
202+
from codeframe.core.llm_resolution import create_provider, resolve_llm_settings
203+
204+
settings = resolve_llm_settings(workspace_path, model_flag=model)
205+
provider = create_provider(settings)
190206

191207
self._session_id = session_id
192208
self._db_repo = db_repo

codeframe/core/conductor.py

Lines changed: 50 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -119,10 +119,15 @@ def __init__(self, workspace: Workspace):
119119

120120
@property
121121
def llm(self):
122-
"""Lazy-load LLM provider."""
122+
"""Lazy-load LLM provider.
123+
124+
Resolves via the shared ``llm_resolution`` chain (#861):
125+
``CODEFRAME_LLM_PROVIDER`` → ``.codeframe/config.yaml`` → anthropic.
126+
"""
123127
if self._llm is None:
124-
from codeframe.adapters.llm import get_provider
125-
self._llm = get_provider()
128+
from codeframe.core.llm_resolution import create_provider, resolve_llm_settings
129+
130+
self._llm = create_provider(resolve_llm_settings(self.workspace.repo_path))
126131
return self._llm
127132

128133
def try_resolve_blocked_task(self, task_id: str) -> bool:
@@ -827,6 +832,48 @@ def list_batches(
827832
return [_row_to_batch(row) for row in rows]
828833

829834

835+
def find_batch_by_prefix(workspace: Workspace, prefix: str) -> list[BatchRun]:
836+
"""Resolve batches whose id starts with ``prefix`` (SQL ``LIKE``, no cap).
837+
838+
The CLI accepts partial batch ids. Doing that in Python over
839+
``list_batches(limit=100)`` meant any batch beyond the cap was unreachable
840+
(#825). This resolves at the SQL layer with no ``LIMIT`` so every matching
841+
batch is addressable regardless of how many batches exist.
842+
843+
Returns all matches (possibly >1) so callers can report ambiguity.
844+
845+
Args:
846+
workspace: Workspace to query
847+
prefix: Batch id prefix (user-typed; may be partial)
848+
849+
Returns:
850+
List of matching BatchRuns, newest first.
851+
"""
852+
# Batch IDs are UUIDs (no _/%), but the prefix is user-typed — escape LIKE
853+
# metachars so a stray wildcard can't over-match the wrong batch.
854+
escaped = prefix.replace("\\", "\\\\").replace("%", "\\%").replace("_", "\\_")
855+
conn = get_db_connection(workspace)
856+
try:
857+
cursor = conn.cursor()
858+
cursor.execute(
859+
"""
860+
SELECT id, workspace_id, task_ids, status, strategy, max_parallel,
861+
on_failure, started_at, completed_at, results, engine,
862+
isolation, stall_timeout_s, stall_action, concurrency_by_status,
863+
llm_provider, llm_model
864+
FROM batch_runs
865+
WHERE workspace_id = ? AND id LIKE ? ESCAPE '\\'
866+
ORDER BY started_at DESC
867+
""",
868+
(workspace.id, f"{escaped}%"),
869+
)
870+
rows = cursor.fetchall()
871+
finally:
872+
conn.close()
873+
874+
return [_row_to_batch(row) for row in rows]
875+
876+
830877
def cancel_batch(workspace: Workspace, batch_id: str) -> BatchRun:
831878
"""Cancel a running batch.
832879

0 commit comments

Comments
 (0)