| Age | Commit message (Collapse) | Author |
|
Closes the three items left on the deferred queue after the post-epic
cleanup.
GeminiRunner.execOnce now actually executes the gemini binary instead
of writing hardcoded stream data. Mirrors ClaudeRunner.execOnce:
- exec.CommandContext with the same env vars (CLAUDOMATOR_API_URL etc.)
- process group SIGKILL on context cancel
- stdout piped through parseGeminiStream → stdoutFile
- stderr to file
- exit codes captured, stderr tail surfaced on failure
Test infrastructure bug uncovered in passing: testServerWithGeminiMockRunner's
mock script used double-quoted echo with literal triple-backticks, which
bash interpreted as command substitution. The script always produced
empty output. The bug was invisible until now because GeminiRunner
ignored the script entirely. Switched to a single-quoted heredoc.
Frontend: index.html dropdown gains a "Local" option. No JS branching
needed — the value flows through to agent.type verbatim and downstream
display reads the type string as-is.
storage/db.go: removed stale debug-comment scaffolding (the "TODO:
Replace with proper logger" block) that was tracking a dead
`fmt.Printf` call. The path it commented on is fine without logging —
unmarshal errors are returned wrapped.
Test status: `go test -race ./...` green across every package, zero
skips, zero excluded tests.
https://claude.ai/code/session_017Edeq947TpSm1vQTxMhi1J
|
|
Addresses the cleanup queue captured in docs/plans/local-oss-runner.md
after the local-OSS-models epic landed. After this commit
`go test -race ./...` is green across every package with zero `t.Skip`
calls and no excluded tests.
Real bugs fixed:
- claude.go setupSandbox callsites used `sandboxDir, err := ...` which
shadowed the outer variable, so BlockedError.SandboxDir was always
empty. Resume-after-block was broken for both new and stale-sandbox
paths. TestBlockedError_IncludesSandboxDir now exercises the right
invariant.
- TestPool_ActivePerAgent_DeletesZeroEntries flake under -race: the
cleanup defer in execute()/executeResume() runs AFTER
handleRunResult sends on resultCh, so consumers observing a result
could see a still-counted activePerAgent entry. Extracted
decActiveAgent(agentType, *cleaned) helper; called explicitly before
every resultCh send, defer becomes a no-op via the cleaned flag.
Verified clean over `go test -race -count=10`.
Test infrastructure made hermetic:
- gitSafe now also passes -c commit.gpgsign=false / -c tag.gpgsign=false
so sandbox tests pass on hosts whose global config requires signing.
- Bare repos in tests initialized with `-b main` (HEAD symbolic ref
matched to the branch we push) so `git log` after push works.
- TestSandboxCloneSource_FallsBackToOrigin uses a local-FS origin URL,
matching sandboxCloneSource's intentional filter against network URLs.
- TestGeminiLogs_ParsedCorrectly URL fixed to the actual log route
(/api/executions/{id}/log).
GeminiRunner gap closed (partial):
- parseGeminiStream now walks lines for `result` events, surfacing
is_error as an error and total_cost_usd as the float return value.
- GeminiRunner.Run propagates parsed cost to Execution.CostUSD.
- TestParseGeminiStream_ParsesStructuredOutput unskipped.
Notes:
- GeminiRunner is still simulated end-to-end (Run writes hardcoded
stream data instead of execing the binary). The result/cost parser
now exists; finishing the runner is a smaller, contained follow-up.
Kept on the deferred queue.
- Frontend "Local" agent option and a minor storage.db.go logger TODO
remain on the deferred queue, both intentionally — neither blocks
anything in flight.
https://claude.ai/code/session_017Edeq947TpSm1vQTxMhi1J
|
|
Phase 4 of "local OSS models as agents" plan. Closes the epic.
When an execution finishes and the agent did NOT write a "## Summary"
heading in its stdout (so the existing extractSummary path returns
empty), and the Pool has a local LLM configured, we now synthesize a
2-4 sentence summary from the assistant text content of the log tail.
Behavior:
- Primary path unchanged: if the agent wrote "## Summary", that wins
byte-for-byte (TestPool_HandleRunResult_ExtractSummaryWins guards).
- Fallback path: empty extractSummary + Pool.LLM != nil → synthesize.
- All-empty path: when no LLM is configured, summary stays empty —
identical to pre-Phase-4 behavior.
Implementation:
- Pool gains an LLM *llm.Client field, wired in serve.go and run.go
alongside Classifier.LLM (same localClient used everywhere).
- New synthesizeSummary in internal/executor/summary.go:
* 6s timeout so a slow local model can't stall finalization
* 16 KB tail cap on the stdout log
* readAssistantTextTail seeks to the last 16 KB and skips the
first (likely partial) line, parses each line as a stream-json
event, joins assistant `text` blocks (skips system/result/etc).
* Returns "" on any error so the caller's behavior never regresses.
- handleRunResult: 3-tier summary resolution — exec.Summary set by
runner → extractSummary → synthesizeSummary → empty.
- minimalMockStore now records UpdateTaskSummary calls (additive;
existing tests unaffected) so integration tests can assert.
Tests (9 new):
- synthesizeSummary nil client / empty path / missing file all
return "" without HTTP calls.
- empty assistant content short-circuits without LLM call.
- success path returns trimmed body, with both assistant texts in
the user prompt.
- LLM 500 returns "" (caller handles same as no-summary).
- readAssistantTextTail seeks past early content in a large file.
- Pool integration: ## Summary present → LLM not called, agent text
used. ## Summary absent + LLM set → LLM called, synthesized summary
recorded against the right task ID.
Plan: docs/plans/local-oss-runner.md.
Epic complete. Post-epic deep cleanup queue captured in the same plan
file for follow-up.
https://claude.ai/code/session_017Edeq947TpSm1vQTxMhi1J
|
|
Phase 3 of "local OSS models as agents" plan. When the webhook handler
creates a task for a failed CI run AND a local LLM is configured on
the server, the hardcoded 4-step investigation template is replaced
with a project-aware investigation plan generated by the LLM.
Scope adjustment from the original sketch: the original plan said
"summarize fetched workflow logs", but fetching logs requires GitHub
API auth that isn't wired. Narrowed to project-context triage —
recent git log + CLAUDE.md content + webhook metadata, fed to the
LLM with a system prompt asking for 6-12 lines of concrete next
steps. Deferred GitHub log fetching to post-epic cleanup.
Implementation:
- New internal/api/webhook_llm.go holds enrichCIInstructions and its
helpers (readRecentCommits via `git log`, readProjectDoc).
- enrichCIInstructions is truly additive: any failure mode (no client,
HTTP error, empty body, 10s timeout) returns the original fallback
template unchanged. Existing webhook tests pass byte-for-byte.
- Always preserves a metadata header (repo/branch/SHA/check/URL)
ahead of the LLM body so investigators don't lose context if the
LLM is terse.
- Reuses s.llm (set via Server.SetLLM in Phase 2) — no new config
knob, no per-feature gating. Asymmetric opt-out (yes-elaborate,
no-CI-triage) deferred until there's actual demand.
Tests:
- enrichCIInstructions: nil client, LLM 500, empty body all return
fallback unchanged.
- enrichCIInstructions: success path produces enriched body with
metadata header preserved; user prompt contains repo/branch/SHA.
- enrichCIInstructions: real git repo (init + 2 commits) → recent
commits appear in user prompt.
- Webhook handler regression guard: no-LLM path produces the exact
legacy template substrings.
- Webhook handler with LLM stubbed: task instructions contain LLM
body + metadata header.
Plan: docs/plans/local-oss-runner.md.
https://claude.ai/code/session_017Edeq947TpSm1vQTxMhi1J
|
|
Phase 2 of "local OSS models as agents" plan. Adds a third elaboration
path that calls the local OpenAI-compatible LLM via the internal/llm
client, and reorders dispatch so the cheap path is tried first:
local → claude → gemini, with each next attempt only on hard failure
of the prior.
Wiring is opt-out, not opt-in: when [local_model].endpoint is set,
elaboration prefers local by default. Users with a slow or low-quality
local model can disable just elaboration via:
[local_model]
endpoint = "..."
prefer_for_elaborate = false
without giving up the runner or the classifier path.
Implementation:
- Server gains an optional *llm.Client field via SetLLM (matches the
existing SetNotifier/SetWorkspaceRoot setter pattern, no NewServer
signature break).
- elaborateWithLocal() reuses buildElaboratePrompt verbatim and asks
for response_format=json_object so we skip markdown-fence cleanup.
- handleElaborateTask reorders try chain; existing Claude-first
behavior is preserved exactly when SetLLM is not called.
- LocalModel.UseForElaborate() encapsulates the default-true gating
with a *bool so explicit-false survives TOML parse.
Tests:
- elaborateWithLocal: parses valid response, errors on nil client,
errors on bad JSON.
- handler: local preferred when wired; falls back to claude when
local fails; unchanged behavior when no LLM is configured.
- config: UseForElaborate gating across empty/default/explicit-true/
explicit-false cases.
Pre-existing test failures noted in docs/plans/local-oss-runner.md
(post-epic cleanup): TestGeminiLogs_ParsedCorrectly returns 404 for
gemini execution log fetch — predates this change.
Plan: docs/plans/local-oss-runner.md.
https://claude.ai/code/session_017Edeq947TpSm1vQTxMhi1J
|
|
Phase 1 of "local OSS models as agents" plan. Adds a third Runner
backed by any OpenAI-compatible HTTP server (Ollama, vLLM, LM Studio,
llama.cpp), and migrates the Gemini-CLI classifier to route through
the same client when configured.
Two-layer split: internal/llm.Client is the workhorse (HTTP, no Pool,
no DB) used directly by the classifier and any future internal helper
that needs cheap reasoning. internal/executor.LocalRunner is a thin
adapter implementing Runner for user-facing tasks. This avoids
Pool reentrancy/deadlock when sub-second internal calls fire from
inside Pool.execute().
Highlights:
- internal/retry: relocated runWithBackoff/IsRateLimitError/ParseRetryAfter
into a shared package reused by executor and llm.
- internal/llm: Chat (non-streaming) and ChatStream (SSE) over
/chat/completions with optional bearer auth, json_object response
format, retry on 429/503, Retry-After parsing.
- internal/executor/LocalRunner: streams deltas into stdout.log in the
same stream-json envelope ClaudeRunner emits, then writes one
consolidated assistant block plus a result terminator so existing
parsers (extractSummary, ParseChangestatFromOutput) work unchanged.
- internal/executor/Classifier: gains optional LLM field; uses
json_object response format (no markdown-fence cleanup needed).
Falls back to Gemini-CLI subprocess when LLM is nil.
- Pool.skipClassification: now skips only when the requested agent
type is registered, so unknown types still reach the load balancer.
- Storage: additive tokens_in/tokens_out ALTERs on executions; CLI
runners record cost_usd as before, LocalRunner records 0 + tokens.
- Config: [local_model] section (endpoint, model, timeout_seconds,
default_temperature, api_key). Empty endpoint = no LocalRunner
registered, classifier falls back to Gemini.
Pre-existing test issues fixed in passing:
- claude_test.go setupSandbox callsites updated to current signature.
- gemini_test.go TestParseGeminiStream skipped (asserts unimplemented
GeminiRunner stream-error parsing; tracked separately).
Plan: docs/plans/local-oss-runner.md.
https://claude.ai/code/session_017Edeq947TpSm1vQTxMhi1J
|