641 lines
29 KiB
Markdown
641 lines
29 KiB
Markdown
# 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)
|
||
|
||
```bash
|
||
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:
|
||
|
||
```bash
|
||
# 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
|
||
|
||
```bash
|
||
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-run` harness) always starts
|
||
immediately.
|
||
- Set `"confirm": false` in `.kilocode-loop/config.json` to 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`:
|
||
|
||
```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_finish` event
|
||
- **context size** — `tokens.total` of 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):
|
||
|
||
```jsonc
|
||
{
|
||
"title": "Ship the parser",
|
||
"stages": [
|
||
{ "id": "s1", "title": "Rebuild the parser", "verify": "pnpm test", "acceptance": "no failures" }
|
||
]
|
||
}
|
||
```
|
||
|
||
Markdown goals take directives after `::`:
|
||
|
||
```markdown
|
||
- [ ] 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:
|
||
|
||
1. **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 child `kilo run` via `KILO_CONFIG_CONTENT`, so
|
||
`--auto` still denies them. Skipped when the project already sets a scalar
|
||
`permission.bash`.
|
||
2. **Runtime** — every `tool_use` is scanned; a match aborts the session
|
||
immediately and stops the loop with status `stopped-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 status `stopped-budget`.
|
||
- `--max-stale-iterations <n>` — stop with status `stalled` after 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-retry` disables 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 …` (event `model-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
|
||
|
||
```bash
|
||
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:
|
||
|
||
```bash
|
||
# 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`:
|
||
|
||
```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:
|
||
|
||
```markdown
|
||
# 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.
|
||
|
||
```bash
|
||
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 under `plans/` 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: cmd` for markdown, `stage.verify` for JSON) or once for
|
||
all plans (stored in `.kilocode-loop/config.json`);
|
||
- **Mark 100%** all stages done (`- [x]` for markdown, `done: true` for 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 shows `custom instruction` while it is active.
|
||
It maps to the same `promptExtra` as 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:
|
||
|
||
```jsonc
|
||
{"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 `sessionID` from the first event and passes
|
||
`--session <id>` on later iterations, so `--session-mode continue` keeps one
|
||
growing context (and `--session-mode fresh` starts 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"). `--auto` is on
|
||
by default; deny destructive permissions in the project `kilo.json` if needed.
|
||
- `--thinking` is required for `reasoning` events to be emitted.
|
||
|
||
## Safety
|
||
|
||
- The runner never commits or pushes; the agent is instructed not to either.
|
||
- `--auto` approves permissions the project does not explicitly deny — review
|
||
`kilo.json` / `.kilo` permissions before unattended runs. The command guard adds
|
||
deny rules for destructive bash patterns on top of them.
|
||
- `--dry-run` performs no API calls and no file changes (beyond its own
|
||
`progress.json`); it also skips verify commands and the reviewer pass.
|
||
- Logs, state and reports are redacted before they are persisted; use
|
||
`--no-redact` only when you trust the target. `--no-guard` and
|
||
`--no-checkpoint` trade safety for speed.
|
||
- `report` and `--json` are 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:
|
||
|
||
```bash
|
||
# 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)
|
||
```
|