Interactive coding agents are designed for a person at a keyboard. CI is not. A pipeline needs a process it can start, observe, constrain, and classify without waiting for a human to approve the next tool call.
SolonCode has a separate run entry point for that boundary. It is a one-shot, non-interactive execution mode: give it a prompt, let the agent work to a terminal result, consume text or structured output, and use the exit code to decide what the pipeline should do next.
The useful mental model is not “a smaller chat UI.” It is “an agent process with a machine-readable contract.”
The smallest useful CI invocation
soloncode run "Review the changes in this pull request. Focus on security and correctness." \\
--output-format json \\
--allowedTools "Read,Grep,Glob,Bash(git log *),Bash(git diff *)" \\
--disallowedTools "Bash(rm *)" \\
--permission-mode dontAsk \\
--max-turns 15 \\
--max-budget-usd 2.0 > review.json
This command makes several choices explicit:
-
rundoes not start the interactive UI. -
jsonproduces one result object for a script to parse. - The tool allowlist describes what the reviewer may inspect.
- The command rule blocks destructive
rmcalls while allowing selected Git reads. -
dontAskis appropriate for an unattended job: an operation requiring approval is not turned into a hidden prompt. -
max-turnsprovides a runtime bound. -
max-budget-usdgives the result a cost threshold.
A CI wrapper should check the process status before trusting the output:
set +e
soloncode run "Review the changes in this PR" \\
--output-format json \\
--allowedTools "Read,Grep,Glob,Bash(git diff *)" \\
--permission-mode dontAsk \\
--max-turns 15 \\
--max-budget-usd 2.0 > review.json
status=$?
set -e
jq -r '.error // .result' review.json
case "$status" in
0) echo "review completed" ;;
2) echo "agent reached the maximum number of turns" >&2; exit 1 ;;
4) echo "agent exceeded the configured budget" >&2; exit 1 ;;
*) echo "agent execution failed" >&2; exit 1 ;;
esac
The distinction between the exit code and the JSON body matters. A result can be valid JSON and still represent an incomplete or over-budget execution.
Three output modes, three consumers
soloncode run supports three output formats.
text: a human-oriented result
The default writes the final answer as plain text. It is useful when a shell script only needs to display a report or append it to a log, but it is not a stable envelope for downstream automation.
json: one terminal object
JSON is the natural choice for a job that waits for completion. The object can contain the result, error state, session ID, metrics, estimated cost, and—when a schema is supplied—structured_output.
soloncode run "List public API methods in src" \\
--output-format json \\
--allowedTools "Read,Grep,Glob" \\
--permission-mode dontAsk \\
--json-schema '{"type":"object","properties":{"methods":{"type":"array","items":{"type":"object","properties":{"name":{"type":"string"},"file":{"type":"string"}}}}}}' \\
| jq '.structured_output.methods'
The schema is an output constraint, not a permission policy. Keep the two concerns separate: json-schema describes what the answer should look like; tool and permission options describe what the agent may do while producing it.
stream-json: an observable event stream
For longer tasks, waiting silently for one final object is a poor operational experience. With stream-json --verbose, SolonCode emits one JSON object per line. The event stream includes initialization, assistant text, tool calls, tool results, and a terminal result or error.
soloncode run "Inspect the build and explain the first failing test" \\
--output-format stream-json --verbose \\
| tee run.jsonl \\
| jq -r 'select(.type=="result") | .result'
Because each event is a line, a pipeline can retain the complete trace and independently project the part it needs:
# Observe tool calls without parsing the final prose
jq 'select(.type=="assistant")
| .message.content[]?
| select(.type=="tool_use")
| {name, input}' run.jsonl
Without --verbose, stream-json emits only the final result event. That behavior is useful when the caller wants the stream-shaped terminal record but does not need intermediate activity.
Prompt input: argv or stdin
A prompt can be the positional argument or come from a redirected stdin stream. If both are present, the command-line prompt wins.
cat build-error.log | soloncode run \\
--output-format json \\
"Analyze this build failure and identify its root cause"
This is more than a convenience. Keeping large or generated input out of shell interpolation avoids quoting surprises and makes it possible for a CI step to pass a report directly into the agent.
run is deliberately one-shot. It does not accept the persistent JSONL input mode. If a process must remain alive and receive multiple user messages from stdin, the separate soloncode stream entry point is the clearer lifecycle contract.
Tool restrictions are part of the job definition
A prompt saying “do not change files” is not an enforcement mechanism. In unattended execution, restrictions should be represented by options.
For a read-only review:
soloncode run "Review this repository" \\
--allowedTools "Read,Grep,Glob" \\
--permission-mode dontAsk
For a plan-only task:
soloncode run "Propose a migration plan for the authentication module" \\
--permission-mode plan
For a narrowly scoped automatic edit, acceptEdits allows file-oriented tools while other operations remain rejected by the permission rules. It should still be combined with a specific allowlist and a turn limit.
The ToolName(pattern) form is useful when a whole tool is too broad:
--allowedTools "Read,Grep,Bash(git diff *),Bash(git log *)"
--disallowedTools "Bash(rm *)"
The pattern is matched as a tool command rule. This is preferable to allowing arbitrary shell execution merely because the task needs git diff.
Bound the agent in three dimensions
A robust CI invocation usually has three independent bounds:
- Capability bound — which tools and commands can be called.
- Work bound — how many reasoning/action turns may run.
- Cost bound — how much estimated usage the result may report.
--max-turns is the runtime guard. --max-budget-usd is currently checked after execution completes, so it should not be treated as a hard mid-run kill switch. Use both, and make the pipeline’s policy explicit for exit code 4.
--fallback-model can make a scheduled job more tolerant of primary-model unavailability, but it does not remove the need for bounded work and a clear output contract:
soloncode run "Run the nightly code-health inspection" \\
--output-format json \\
--allowedTools "Read,Grep,Glob,Bash(git log *)" \\
--permission-mode dontAsk \\
--max-turns 25 \\
--max-budget-usd 3.0 \\
--fallback-model haiku \\
> reports/health.json
Two phases with a resumable session
Some jobs are easier to reason about when analysis and modification are separate phases. The JSON result exposes a session ID that can be used by a later invocation:
set -euo pipefail
soloncode run "Analyze src/auth and list the risks" \\
--output-format json --max-turns 10 > phase1.json
session=$(jq -r '.session_id' phase1.json)
soloncode run "Using that analysis, write unit tests for src/auth" \\
--resume "$session" \\
--output-format json \\
--permission-mode acceptEdits \\
--max-turns 20 > phase2.json
The session is an agent conversation boundary, not a replacement for Git branches, worktrees, or artifact storage. If the second phase edits files, the CI job still needs its normal diff and test checks.
Remote execution: the same contract behind HTTP
A local shell is not always the right integration point. A build service may already run SolonCode on a worker, or a Java application may want to submit work without installing the CLI.
The /web/run endpoint carries the same one-shot execution contract over HTTP:
{
"prompt": "Analyze this module",
"options": {
"output_format": "stream-json",
"max_turns": 15,
"allowed_tools": ["Read", "Grep", "Bash(git diff *)"],
"permission_mode": "dontAsk"
},
"workspace": "my-project",
"metadata": {"request_id": "ci-001"}
}
The HTTP representation uses snake_case, while the CLI keeps its flag spelling. Unknown option fields are rejected instead of silently ignored. That failure mode is important: a typo must not make a caller believe that a safety option was applied.
For streaming requests, the response is SSE. Each data: line contains the same JSON event that the CLI would write as one JSONL line, so an existing event parser can be reused:
curl -N -X POST http://127.0.0.1:18080/web/run \\
-H "Authorization: Bearer $TOKEN" \\
-H "Content-Type: application/json" \\
-H "Accept: text/event-stream" \\
-d '{"prompt":"Analyze code quality","options":{"output_format":"stream-json"}}' \\
| grep '^data:' \\
| sed 's/^data: *//' \\
| jq -r 'select(.type=="result") | .result'
The server implementation starts a child App run process in the selected workspace. That choice preserves the CLI’s argument parsing and execution semantics while keeping per-request engine options from mutating the web process’s interactive engine. It also means a request pays a JVM startup cost; the isolation and zero-drift properties are the more important trade-off for this boundary.
The endpoint adds network-specific semantics:
- Bearer authentication is required.
- The server accepts registered workspace identifiers, not arbitrary paths.
-
bypassPermissionsis rejected for/web/run. - A session already running receives a conflict response rather than interleaving messages.
-
/web/run/interruptreturns202and destroys the active child process. - A client disconnect is treated as a reason to stop work, not as permission to keep an orphaned agent running.
Do not expose this endpoint as an unauthenticated general-purpose shell. It drives an agent with file and command capabilities. Loopback binding, token authentication, workspace restrictions, permission-mode narrowing, and audit logging are security requirements, not optional deployment polish.
Exit codes are an API
The local and remote forms intentionally distinguish execution conclusions from transport failures.
| Exit code | Meaning |
|---|---|
| 0 | Completed successfully |
| 1 | Agent or API runtime error |
| 2 | Maximum turn limit reached |
| 3 | No prompt was supplied |
| 4 | Estimated cost exceeded the configured budget |
For /web/run, success is HTTP 200. A runtime error maps to HTTP 500, and a missing prompt to HTTP 400. Reaching the turn or budget limit remains HTTP 200 with an error state in the result, because the request was accepted and executed; the client should inspect the payload rather than blindly retrying at the HTTP layer.
That distinction prevents a common automation bug: retrying an expensive, already-completed-but-incomplete agent run as if the network had failed.
A practical adoption sequence
Start small:
- Run a read-only task locally with
jsonoutput. - Add
max-turnsand an explicit tool allowlist. - Make the CI step archive the JSON result and check the exit code.
- Add a schema when another program needs fields, not prose.
- Use
stream-jsonwhen operators need progress or an audit trace. - Split analysis and edits with
--resumeonly after the single-phase job is reliable. - Move to
/web/runwhen execution must live on a service worker, and retain the same parser and policy checks.
The central design decision is to treat the agent invocation as a typed, bounded job. Prompts provide intent; options define capability and limits; events provide observability; exit codes provide control flow. That is what makes SolonCode useful in CI rather than merely runnable from a CI shell.
Top comments (0)