29 KiB
kilocode-loop
Goal-driven autonomous loop runner for the Kilo CLI (kilo run), with a live
Express dashboard, per-iteration reports (tokens / cost / context / topic / % /
next stage), streamed section headings for every action, terminal controls and
human-in-the-loop between sessions.
It also enforces its own safety net: independent verification gates (a stage
is not done until the loop's --verify command exits 0), a destructive-command
guard, per-iteration git checkpoints with revert, budget/stall limits,
secret redaction, notifications and an optional reviewer pass.
By default every iteration runs in its own fresh session (small context, cheaper,
more targeted): the loop carries only a curated handoff — goal, current stage,
previous summary, operator answers and a handoff packet the agent prepares. Turn on
Use shared context between iterations (dashboard checkbox, --shared-context, or
S in the terminal) to keep one growing session instead.
It is a self-contained mini-app: one Node process spawns kilo run --format json
for every iteration, parses the event stream, and turns the agent's own
progress.json into the operator report.
┌─ loop ───────────────────────────────────────────────────────────────────┐
│ for i in 1..N: │
│ ┌─ section: Iteration i/N — <stage> ───────────────────────────────┐ │
│ │ ▸ Read src/foo.vue (tool → section heading) │ │
│ │ ▸ Edit src/foo.vue │ │
│ │ ▸ Run pnpm typecheck │ │
│ └──────────────────────────────────────────────────────────────────┘ │
│ report: tokens · cost · context · topic · % · next stage │
│ human-in-the-loop: ask the operator if the agent needs a decision │
└──────────────────────────────────────────────────────────────────────────┘
Requirements
- Node.js ≥ 20 (developed on Node 24)
- The Kilo CLI on
PATH(kilo/kilocode), authenticated - A git repository to work in (the agent never commits)
npm install
Quick start
Installed as a submodule (recommended)
The launcher resolves the git root of the current directory and installs
node_modules on first use. Run it with no --goal to open the dashboard
launcher and pick a plan in the UI:
# from anywhere inside the repo (or a submodule — it scopes to that repo)
tools/kilocode-loop/kilo-loop # open the dashboard launcher (pick a plan in the UI)
tools/kilocode-loop/kilo-loop --list-goals # print discovered plans + state, then exit
tools/kilocode-loop/kilo-loop --goal .kilocode-loop/goal.json -n 5 # run one directly
tools/kilocode-loop/kilo-loop --dry-run --goal examples/goal.example.json # offline rehearsal
# same command from anywhere, once the launcher is on PATH
kilo-loop
kilo-loop --goal plans/another-goal.md -n 5
Per-repo defaults live in <repo>/.kilocode-loop/config.json (agent, iterations,
session mode, HITL, port, context threshold, default verify). No goal is injected
by default — pass --goal to run one directly, or pick it in the dashboard.
Standalone
npm install
# Real run: 5 iterations of the code-design agent against ./your-repo
node bin/kilocode-loop.mjs \
--goal /path/to/your-repo/.kilocode-loop/goal.json \
-n 5 \
--project /path/to/your-repo \
--agent code-design \
--port 7999
# Offline rehearsal: no API calls, exercises the loop, reports, dashboard and HITL
node bin/kilocode-loop.mjs --dry-run \
--goal examples/goal.example.json -n 5 --project /tmp/demo --port 7999
Then open the dashboard at http://127.0.0.1:7999.
Pre-flight confirmation
On an interactive terminal a direct run (kilo-loop --goal …) does not start
immediately: it prints a summary of the resolved scenario and waits for
confirmation. Launcher mode (no --goal) skips this — nothing runs until you
press Start run in the dashboard.
╭─ About to run ───────────────────────────────────────────────────────╮
│ Goal Workflows module — review, simplification & hardening │
│ Source /home/.../.kilocode-loop/goal.json │
│ Project /home/.../wiz4apps │
│ Agent code-design │
│ Iterations 5 │
│ Context fresh session per iteration (handoff) │
│ HITL on-question │
│ Auto-approve yes │
│ Dashboard http://127.0.0.1:7999 │
│ Mode live │
╰──────────────────────────────────────────────────────────────────────╯
1. [ ] Close Phase 1 residuals
2. [ ] Phase 2 — architecture convergence residuals
...
Start this run? [Y/n]
Enter/y starts; anything else cancels without starting anything (no run dir, no
dashboard, no sessions). A missing/broken goal file fails here, before anything is
created.
--no-confirm/--yes— skip the prompt and start immediately.- Non-interactive stdin/stdout (pipes, CI,
--dry-runharness) always starts immediately. - Set
"confirm": falsein.kilocode-loop/config.jsonto make auto-start the default for a repo.
Controls
| Where | Key / button | Effect |
|---|---|---|
| Terminal | X |
Abort the loop after the current session finishes |
| Terminal | P |
Pause / resume before the next iteration |
| Terminal | S |
Toggle shared context (fresh sessions ⇄ one growing session) |
| Terminal | Q |
Stop now (kills the current kilo session) |
| Terminal | ? |
Print shortcut help |
| Terminal | typing + Enter |
Answer a pending question (number keys pick an option) |
| Terminal | Ctrl+C (SIGINT) |
Stop the run and exit. It works whether or not the dashboard is open; a second Ctrl+C force-exits immediately |
| Dashboard | Pause / Abort after session / Stop now | Same controls from the browser |
| Dashboard | Use shared context between iterations | Off (default) = fresh session + handoff per iteration; on = one growing session |
| Dashboard | Revert before #N | Restore the checkpoint taken before iteration N (confirmation required) |
| Dashboard | Report | Download report.md for the active run |
| Dashboard | question modal | Answer the pending questions (option buttons become editable answers); answers go into the next session |
Top-bar activity indicator
The header doubles as a status light, driven purely by CSS animations (JS only
toggles a topbar--<state> class):
- running / starting — a glowing blue→green segment sweeps along the bottom edge over a faint travelling sheen, so it is obvious work is in progress;
- waiting-answer — pulsing purple, to draw attention to a pending question;
- paused — a static amber line (deliberately not animating);
- error / done-with-errors / stale — the bar is tinted light red with a pulsing red line;
- aborted / stopped-budget / stalled / stopped-guard — a light amber tint (warning, not failure);
- done — a static green line; idle — plain header.
All movement is disabled under prefers-reduced-motion: reduce (the state colour
stays). See public/styles.css (topbar--*) and topbarState() in
public/app.js.
Fresh sessions vs shared context
Default (sharedContext: false / checkbox off) — each iteration is an independent
kilo run with no session history. To keep the work coherent, the runner
carries forward only:
- the goal, its stages and the current stage;
- the previous iteration's
progress.json(topic, summary, next stage, percent); - all operator answers collected so far;
- the list of files changed so far this run;
- the previous iteration's handoff packet.
The handoff packet is a bounded markdown file (.kilocode-loop/handoff.md, ~120
lines max) that the agent writes at the end of every iteration: current state,
decisions & constraints, next steps, prepared data, open questions. The runner
embeds it into the next prompt and then deletes the file, so a forgotten packet can
never leak into a later iteration. Older packets are archived per iteration in
runs/<runId>/iterations/<NN>-handoff-in.md.
Shared context (sharedContext: true / checkbox on) — one growing session is
continued across iterations (--session <id>), so the whole history is available
but the context (and cost) grows every iteration. The handoff sections are then
skipped because the session already has the history.
The toggle is live: the dashboard checkbox (POST /api/control { "action": "set-shared-context", "value": true|false }) and S in the terminal apply from the
next iteration onward.
How a report is produced
Each iteration the runner writes a prompt that embeds a contract: at the end
of the session the agent must write <project>/.kilocode-loop/progress.json:
{
"iteration": 2,
"topic": "Rework SettingsByokSection",
"stageId": "stage-2",
"stageStatus": "in_progress",
"percent": 40,
"nextStage": "Verify layout and dark theme",
"summary": "Replaced the card grid with a q-select + config panel. Typecheck passes.",
"filesChanged": ["src/components/settings/SettingsByokSection.vue"],
"needsInput": false,
"questions": []
}
From that + the CLI event stream the runner derives:
- iteration number — loop counter
- tokens / cost — summed from every
step_finishevent - context size —
tokens.totalof the last step (how full the window is) - topic —
progress.topic - % complete —
progress.percent, or the ratio of completed goal stages - next stage —
progress.nextStage, or the first not-done stage - files changed — git delta since the iteration started, plus
filesChanged
If the agent forgets the contract, the runner falls back to the goal stages and scrapes question-looking lines out of the agent's text.
Reports on disk
<project>/.kilocode-loop/
progress.json # the agent's machine-readable report
answers.json # operator answers from HITL
handoff.md # context packet for the next fresh iteration
review.json # reviewer verdict (when --review is on)
runs/<runId>/
state.json # full run state (for the dashboard)
events.jsonl # every log line (secrets redacted)
report.md # end-of-run summary
checkpoints/
iter-01.json # HEAD + untracked list for iteration 1
iter-01.patch # `git diff HEAD` at the checkpoint
iterations/
01-prompt.md # exact prompt sent
01-review-prompt.md # reviewer prompt (when --review is on)
02-handoff-in.md # handoff packet consumed by iteration 2
01-report.json
01-report.md
Verification gates, guards, budgets and checkpoints
Since v0.2 the loop no longer trusts the agent's own "done". Every safety feature is opt-out, and all of them apply to the loop, not the agent's prompt.
Independent verification (the loop runs the check itself)
A stage is not marked done until a verify command the loop itself runs exits 0.
Set it globally, per stage, or both (the stage wins):
{
"title": "Ship the parser",
"stages": [
{ "id": "s1", "title": "Rebuild the parser", "verify": "pnpm test", "acceptance": "no failures" }
]
}
Markdown goals take directives after :::
- [ ] Rebuild the parser :: verify: pnpm test :: acceptance: no failures
When verify fails, the stage is recorded as blocked, the failing output is fed
back into the next iteration's prompt, and completedStages is not advanced. Set
--no-sync-goal to stop the loop from ticking [x] in a markdown goal file.
Destructive-command guard
Two layers:
- Pre-execution — the loop injects deny rules for destructive bash patterns
(
git commit,git push,git reset --hard,git clean -f,npm|pnpm publish, history rewrites, raw disk writes, recursive deletes of system/home paths, …) into the childkilo runviaKILO_CONFIG_CONTENT, so--autostill denies them. Skipped when the project already sets a scalarpermission.bash. - Runtime — every
tool_useis scanned; a match aborts the session immediately and stops the loop with statusstopped-guard.
rm -rf is path-aware in both layers: recursive deletes whose targets are
all strictly inside a temp directory (os.tmpdir(), /tmp, /var/tmp) are
allowed, so routine scratch cleanup (rm -rf /tmp/<run>) no longer blocks a run.
Deleting the temp root itself, a relative/project path, or any non-temp absolute
path is still blocked. Mixed commands (one temp target plus one unsafe target)
are blocked. Pre-execution denies only the catastrophic recursive targets
(~, $HOME, /home/*, /etc*, /usr*, /opt*, /root*, /boot*, /srv*,
.git/), while the runtime scanner remains the catch-all for everything else.
Checkpoints and revert
Before each iteration the loop captures a checkpoint under
runs/<runId>/checkpoints/: HEAD, git diff HEAD and the untracked-file list.
Nothing is committed. Revert before #N on the dashboard (or
POST /api/revert { iteration }) restores tracked files to HEAD, re-applies the
checkpoint's own diff and deletes untracked files created afterwards. Revert is
destructive and always behind a confirmation; it does not touch submodule
contents or committed history. Disable with --no-checkpoint.
Budgets and stall detection
--max-cost <usd>/--max-tokens <n>— stop with statusstopped-budget.--max-stale-iterations <n>— stop with statusstalledafter N iterations that change no files and advance no stage.--max-iteration-minutes <n>— per-iteration hard timeout (already existed).
Auto-recovery from model/network errors
If kilo run dies because the provider connection is reset, the iteration is
retried instead of failing and losing the turn. Detection covers
Connection reset by [peer|server], ECONNRESET/ETIMEDOUT/EPIPE,
socket hang up, fetch failed, premature close, HTTP 429/5xx, and
rate limit/overloaded/timed out.
- Each retry continues the interrupted session (
--session <id>), so work already done in the turn is preserved (in shared mode the run session; in fresh mode the session the failed attempt created). - Exponential backoff with jitter:
--retry-delay(default 2000 ms), doubling per attempt up to--retry-max-delay(default 60000 ms). --max-retries <n>(default 3);--no-retrydisables it entirely.- Timeouts (
--max-iteration-minutes), a command-guard hit, or a user Stop are not retried. - The dashboard marks the iteration
retrying(auto-retry ×N) and the report records the attempt count and the last transient reason. - Optionally notify on retries with
--notify …(eventmodel-retry).
Secret redaction
Every log line, state.json, per-iteration report and the run report is scrubbed
before it is written: bearer/Basic headers, sk-/ghp_/xox/AWS/JWT tokens,
token=/password: values and URL userinfo. Disable with --no-redact.
Notifications
--notify <targets> (space/comma separated) sends out-of-band alerts for
question, verify-failed, guard, budget, stalled and run-done:
| Target | Behaviour |
|---|---|
https://hooks.slack.com/... |
Slack incoming webhook ({ text }) |
https://ntfy.sh/<topic> or ntfy://<host>/<topic> |
ntfy POST + Title header |
telegram://<botToken>/<chatId> |
Telegram sendMessage |
any https:// URL |
generic JSON payload { event, title, text, sentAt } |
Reviewer pass
--review runs one extra reviewer session (agent --review-agent, default the
run agent) after a stage passes verify. The reviewer inspects the working tree and
writes .kilocode-loop/review.json with { verdict: "pass"|"block", … }; a
block un-advances the stage and raises the reviewer's questions. Skipped in
--dry-run.
Hybrid context
In shared-context mode, --auto-fresh-tokens <n> switches to a fresh session (with
a synthesized handoff) once the context passes that size, instead of only warning.
Report subcommand & exit codes
kilo-loop report # most recent run's report.md
kilo-loop report <runId> --json # redacted run state
kilo-loop report --last -C /repo
report is read-only: it starts nothing and makes no API calls.
Exit codes: 0 done, 1 done-with-errors, 2 aborted, 3 budget,
4 stalled, 5 stopped-guard.
Kilo vs opencode
The loop drives a CLI, not a library: it spawns <bin> run --format json and
parses the line-delimited JSON event stream. opencode is the same lineage as the
Kilo CLI (near-identical run surface), so the wire format is compatible:
| Shared with opencode | |
|---|---|
/run --format json NDJSON events |
tool_use, step_finish, text, reasoning, error; part.tool, part.state.input, part.tokens{cache}, part.cost, sessionID |
| Flags | --agent, --model, --variant, --thinking, --session, --continue, --title, --format json |
Two things differ, and the tool adapts to them automatically:
| kilo | opencode | |
|---|---|---|
| Auto-approve flag | --auto |
--dangerously-skip-permissions |
| Inline config env (guard) | KILO_CONFIG_CONTENT |
OPENCODE_CONFIG_CONTENT |
Project config for permission.bash |
kilo.json, .kilo/kilo.json |
opencode.json, .opencode/opencode.json |
Usage:
# point the loop at opencode (runtime auto-detected from the binary name)
KILO_BIN=opencode kilo-loop --goal goal.md -n 5 \
--agent build --model anthropic/claude-sonnet
# or be explicit
kilo-loop --runtime opencode --agent build ...
--runtime defaults to auto, which detects opencode from the binary name.
Agent names and model IDs are CLI-specific: set --agent/--model to something
defined in your opencode config (the Kilo default code-design is not an
opencode agent). Everything the loop itself provides — verification gates,
checkpoints, budgets, redaction, notifications, the dashboard and report — is
runtime-independent.
Goal files
--goal-text "..." — a single implicit stage.
--goal file.json:
{
"title": "Stabilise the workflows module",
"description": "…",
"stages": [
{
"id": "stage-1",
"title": "Remove legacy libs",
"details": "…",
"verify": "pnpm test",
"acceptance": "test suite is green",
"model": "provider/fast-model",
"agent": "code",
"variant": "high"
}
]
}
Stage fields: id, title, details, done, and optional verify,
acceptance, model, agent, variant (per-stage routing overrides the run
defaults). --goal file.md — the first # heading is the title, each - [ ] /
- [x] item is a stage, and :: verify: … :: acceptance: … adds directives:
# Stabilise the workflows module
- [ ] Remove legacy libs :: verify: pnpm test
- [x] Rewire the builder
Dashboard launcher (pick a plan in the UI)
Run kilo-loop without --goal and the dashboard opens in launcher mode: no
run is started until you pick one.
kilo-loop # launcher only
kilo-loop --list-goals # print every discovered plan + its state, then exit
The launcher lets you:
- pick a plan from a dropdown — plans are discovered under
.kilocode-loop/goals/**, the legacy.kilocode-loop/goal.{json,md}, and any markdown plan underplans/or.kilo/plans/that contains- [ ]items; - see each plan's state (done/total, %, type, last run status/iterations/cost);
- set the verify command per stage (kept in the goal file;
- [ ] Title :: verify: cmdfor markdown,stage.verifyfor JSON) or once for all plans (stored in.kilocode-loop/config.json); - Mark 100% all stages done (
- [x]for markdown,done: truefor JSON) for a plan that was finished outside the loop, or Clear done to reopen it; - Delete plan — moves the plan file to the gitignored
.kilocode-loop/trash/<timestamp>-<name>(recoverable; nothing is committed). The UI asks for confirmation first; - set iterations / agent / HITL / dry-run and Start run;
- write an optional custom instruction (textarea). It is added to the top of
every iteration prompt (under
## Operator instructions for this run, before the goal) and the run header showscustom instructionwhile it is active. It maps to the samepromptExtraas the CLI--prompt-extra; leaving the field empty keeps any instruction coming from the CLI or config.
After a run finishes, New run returns to the launcher (the plan list is
refreshed with the new completion state). POST /api/start refuses to start a
second run while one is active.
CLI reference
Goal
--goal <file> .json goal or .md checklist
--goal-text "<text>" inline goal
-n, --iterations <N> loop count (default 5)
Execution
-C, --project <dir> repository the agent works in (default cwd)
-a, --agent <name> Kilo agent (default code-design)
-m, --model <id> model override (provider/model)
--variant <name> reasoning variant (e.g. high, max)
--thinking capture reasoning parts
--auto / --no-auto auto-approve permissions (default: auto)
--shared-context one growing session across iterations (costly)
--no-shared-context fresh session per iteration + handoff (default)
--session-mode <mode> legacy alias: continue = shared, fresh = no shared
--hitl <mode> always | on-question | off
--runtime <mode> auto | kilo | opencode (auto-detects from the CLI binary)
--max-iteration-minutes <n>
--context-warn-tokens <n>
--prompt-extra <file> extra instructions for every iteration
Verification & safety
--verify "<cmd>" loop-run check; a stage is not done until it exits 0
--verify-timeout-minutes <n>
--checkpoint / --no-checkpoint
--guard / --no-guard
--redact / --no-redact
--max-cost <usd>
--max-tokens <n>
--max-stale-iterations <n>
--retry / --no-retry retry an iteration on a model connection reset (default on)
--max-retries <n> retry attempts per iteration (default 3)
--retry-delay <ms> first backoff delay (default 2000, doubles)
--retry-max-delay <ms> backoff cap (default 60000)
--review / --review-agent <name>
--auto-fresh-tokens <n>
--sync-goal / --no-sync-goal
--force run even when every stage is marked done
--list-goals list discovered plans + state, then exit
--notify <targets>
Dashboard
-p, --port <n> default 7999
--host <host> default 127.0.0.1
--keep-open keep the dashboard after the loop
Utility
--dry-run simulate the agent (no API calls)
--resume <runId> reuse a previous run's session + stage progress
--run-id <id>
--quiet suppress the live console stream
--no-color
--no-confirm, --yes start immediately (skip the pre-flight confirmation)
Config can also live in <project>/.kilocode-loop/config.json (CLI wins).
Dashboard API
| Method | Path | Purpose |
|---|---|---|
GET |
/api/state |
Current run snapshot ({status:"idle", idle:true} when no run is active) |
GET |
/api/events |
SSE stream (state + log messages) |
GET |
/api/goals |
Discoverable plans/goals with per-stage completion and last run |
POST |
/api/goals/verify |
{ "file": "…", "stageIndex": N, "verify": "cmd" } — set a stage verify (stageIndex omitted = global, JSON goals only) |
POST |
/api/goals/verify-global |
{ "verify": "cmd" } — default verify for all runs (.kilocode-loop/config.json) |
POST |
/api/start |
{ "file": "…", "iterations": N, "agent": "…", "hitl": "…", "dryRun": bool } — start a run from the launcher |
GET |
/api/runs |
List saved runs |
GET |
/api/runs/:id |
Load a saved run (with its log) |
GET |
/api/runs/:id/report/:iter |
Iteration report (markdown) |
GET |
/api/runs/:id/export |
Full run report (markdown) |
GET |
/api/runs/:id/diff/:iter |
Checkpoint patch captured before iteration N |
GET |
/api/checkpoints |
Active run's checkpoints (?run=<id> for a saved run) |
GET |
/api/runs/:id/checkpoints |
Saved run's checkpoints |
POST |
/api/control |
{ "action": "pause"|"resume"|"abort-after-current"|"stop" } and { "action": "set-shared-context", "value": true|false } |
POST |
/api/revert |
{ "iteration": N } — restore the checkpoint before iteration N |
POST |
/api/answer |
{ "answers": [{ "question": "…", "answer": "…" }] } |
How it drives the Kilo CLI
kilo run --format json emits one JSON object per line:
{"type":"step_start","timestamp":…,"sessionID":"…","part":{…}}
{"type":"tool_use","part":{"tool":"edit","state":{"status":"completed","input":{…}}}}
{"type":"text","part":{"type":"text","text":"…"}}
{"type":"step_finish","part":{"cost":0.0021,"tokens":{"total":14411,"input":14025,"output":2,"reasoning":0,"cache":{"read":384,"write":0}}}}
{"type":"error","error":…}
- The runner captures
sessionIDfrom the first event and passes--session <id>on later iterations, so--session-mode continuekeeps one growing context (and--session-mode freshstarts a new one each time). - Unattended runs need
--auto: without it, non-interactive permission requests are auto-rejected by the CLI ("pass --auto for autonomous use").--autois on by default; deny destructive permissions in the projectkilo.jsonif needed. --thinkingis required forreasoningevents to be emitted.
Safety
- The runner never commits or pushes; the agent is instructed not to either.
--autoapproves permissions the project does not explicitly deny — reviewkilo.json/.kilopermissions before unattended runs. The command guard adds deny rules for destructive bash patterns on top of them.--dry-runperforms no API calls and no file changes (beyond its ownprogress.json); it also skips verify commands and the reviewer pass.- Logs, state and reports are redacted before they are persisted; use
--no-redactonly when you trust the target.--no-guardand--no-checkpointtrade safety for speed. reportand--jsonare read-only.
Adding it to the main project as a submodule
This repo is standalone and has its own history. To pull it into another repo:
# 1. create an empty repo on Forgejo (e.g. w4c-agent/kilocode-loop), then:
cd /path/to/main-repo
git submodule add ssh://git@localhost:13022/w4c-agent/kilocode-loop.git tools/kilocode-loop
git commit -m "chore: add kilocode-loop submodule"
Because it is a submodule, node_modules/ stays local and is never committed.
Layout
bin/kilocode-loop.mjs entry point (wires config → orchestrator → dashboard)
src/config.mjs CLI/config parsing
src/orchestrator.mjs the loop, controls, HITL, report assembly
src/kilocode.mjs `kilo run` driver + JSONL event parser
src/goal.mjs goal files, agent contract, progress/percent/questions
src/state.mjs run state, persistence, log stream
src/reports.mjs console/markdown reports, formatting
src/console.mjs live console renderer
src/terminal.mjs TTY controls
src/server.mjs Express + SSE dashboard API
src/verify.mjs independent verify command runner
src/goals.mjs goal/plan registry, summaries, verify editing
src/controller.mjs run lifecycle (start multiple plans from the dashboard)
src/guard.mjs destructive-command guard (pre-execution + runtime)
src/checkpoint.mjs per-iteration git checkpoints + revert
src/redact.mjs secret redaction for persisted logs/state/reports
src/notify.mjs webhook/ntfy/telegram notifications
src/reportcli.mjs `kilo-loop report` subcommand
src/simulator.mjs --dry-run agent
public/ dashboard UI
examples/ sample goals
test/ node:test unit tests (npm test)