Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 31 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,36 @@ jobs:
run: for f in scripts/*.sh; do bash -n "$f"; done
- name: Unit tests (no backend CLI, no network, live tests skipped)
run: npm run test:unit
- name: Integration/live tests are skipped without a backend CLI
- name: Integration tests with the test-only stub backend (real daemon, scripted worker; git + python3)
run: npm run test:stub
- name: Remaining test files (Kilo/OpenCode integration + live tests skip without a backend CLI)
env:
WH_SKIP_INTEGRATION: "1"
run: node --test test/*.test.mjs
WH_SKIP_STUB: "1"
run: node --test --test-timeout=600000 test/*.test.mjs

pins:
name: Installer pins resolve to an existing binary
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@v7
- uses: actions/setup-node@v7
with:
node-version: 22
- name: npm ci --ignore-scripts on each scripts/pins/* lockfile and check the install.sh link target
# scripts/pins/<name>-cli pins one package whose executable is <name> (kilo-cli -> kilo,
# opencode-cli -> opencode); the target comes from the package's package.json "bin" field,
# exactly as scripts/install.sh resolves it.
run: |
set -euo pipefail
for pin in scripts/pins/*/; do
name="$(basename "$pin")"; exe="${name%-cli}"
pkg="$(node -p "Object.keys(require('./${pin}package.json').dependencies)[0]")"
tmp="$(mktemp -d)"; cp "$pin/package.json" "$pin/package-lock.json" "$tmp/"
(cd "$tmp" && npm ci --ignore-scripts --no-audit --no-fund --loglevel=error)
rel="$(node scripts/pin-bin.mjs "$tmp" "$pkg" "$exe")"
mkdir -p "$tmp/bin" && ln -sfn "../node_modules/$pkg/$rel" "$tmp/bin/$exe"
test -e "$tmp/bin/$exe" || { echo "::error::$name: bin/$exe -> $rel is dangling"; exit 1; }
echo "$name: $pkg bin $exe -> node_modules/$pkg/$rel (exists)"
done
4 changes: 2 additions & 2 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ node_modules/
config/*.json
config/*.bak.*
# pinned CLIs installed by scripts/install.sh when installing in place
kilo-cli/
opencode-cli/
/kilo-cli/
/opencode-cli/
.install-info
# test/runtime artifacts
*.log
Expand Down
73 changes: 73 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,79 @@ All notable changes to this project are documented here. The format follows

## [Unreleased]

## [0.3.0] - Unreleased

Token savings. Workhorse exists to reduce supervisor (for example Grok) usage by letting smaller,
user-chosen models do bounded work; this release cuts what the supervisor reads and how often it has to
act, and adds opt-in savers for the workers. Builds on 0.2.0. See [docs/token-savings.md](docs/token-savings.md).

### Added
- **`wait_task`** (MCP, RPC, `workhorse wait`): long-poll one task (`task_id`) or several (`task_ids`,
`mode: "any" | "all"`) until they finish or park, up to `max_wait_s` (default 45, cap 55 s to stay
under common 60 s MCP request timeouts). Returns the brief result.
- **`view: "brief"`** for `task_result` and `wait_task` (~0.5-1 KB: verdict, short summary, files,
tests, concerns, `next` with the suggested call, review, automatic trail, tokens). `full` stays the
default for `task_result`.
- **`delegate_tasks`**: up to 10 tasks in one call, with per-entry errors.
- **Presets and size routing** in `profiles.json`: `presets` (profile, size, mode, timeout, test
command, standing instructions, follow-up flags) and `routing` (`small`/`medium`/`large` -> profile).
`list_models` shows them.
- **Automatic follow-ups** (off by default; per call, per preset or `profiles.json` `auto`):
`auto_fix_rounds` (same-session fix rounds after failing tests or a missing/partial RESULT, max 3),
`escalate` along each profile's new `escalate_to` chain (fresh session, same worktree), hard caps
`max_auto_runs` (default 3, cap 6, total per round), `max_tokens`, `max_cost_usd` (both include the
task's auto-review tasks; `0` = explicit zero budget (no follow-ups or reviews), not unlimited; checked between runs, so one run
can overshoot). `continue_task` restarts the run counters but not the token/cost budgets. The trail
is in `result.auto`.
- **`auto_review`**: an advisory read-only review on a (cheap) profile after a successful run; new
transient status `reviewing`. `request_changes` makes the handoff `needs_review`. The verdict is read
from the first line of the reviewer's summary (negations such as "not approved" count as
`request_changes`; anything else is `unclear`). Review tasks are hidden from `list_tasks` unless
`include_auto_reviews` is set and never need attention themselves; the daemon recovers or cancels an
orphaned review after a crash.
- **`usage_report`** (MCP, RPC) and **`workhorse stats`**: worker tokens and estimated list cost by
profile and day, plus a clearly labelled, conservative ESTIMATE of supervisor tokens avoided (only
successful workers' output tokens minus the supervisor's own I/O; worker input is not counted;
formula documented; optional `daemon.json supervisor.price_per_mtok` for a net USD figure). The daemon now records per-run tokens and
cost, and the characters each supervisor call sent and received.
- **Worker token savers** (`token_savers` in daemon.json, per-profile override; all off by default):
`terse` and `minimal_code` instruction fragments (`lite`/`full`, our own wording inspired by Caveman
and Ponytail) and `rtk` (Kilo/OpenCode: the guard plugin rewrites worker bash commands through
`rtk rewrite`, with a minimal environment and a 1 s timeout, falling back to the original command
if the rewrite fails or does not pass the guard; only the rtk binary itself is bound into the
sandbox). `workhorse token-savers`, installer `--token-savers` / `--rtk-bin`. The daemon's own test
run never goes through them.
- **`approvals.require_operator`**: MCP `approve_task` only records an approval request (with a
request id); a human confirms with `sudo workhorse approve <id>`, which prints the pending request
and sends a separate operator token (the daemon stores its SHA-256) plus the displayed request id
(refused if the request changed). A task that parked stays gated until the operator answers, even if
it is closed or cancelled meanwhile; without the token, reject may only close the task. This gates
the parked-task flow; it is not a capability boundary (a supervisor can still delegate a new task
asking for the same thing). `workhorse operator-token init [--enable]`.
- **Per-profile `stall_minutes`.**
- **Audit log rotation** (`audit.max_mb`, `audit.keep`).
- **Test-only stub backend** (`adapters/stub`), registered only when the daemon runs with
`WH_ENABLE_STUB_BACKEND=1` and limited to its bundled script, so the approval, continue, retry,
fallback, restart, handoff, auto-fix, escalation and review flows run end to end in GitHub Actions
(`npm run test:stub`). `workhorse health` warns if a daemon runs with the flag.

### Changed
- The MCP shim returns compact JSON (9-18% fewer characters on typical responses).
- The full `task_result` view is unchanged from 0.2.0 apart from compact JSON (the complete handoff,
including `handoff.context`, is kept for compatibility; use `view: "brief"` for the short form).
- `delegate_task`'s `next_step` and the MCP instructions point to `wait_task`; the delegation skill draft
prefers `wait_task`, the brief view, presets/size, batches and automatic follow-ups.
- The installer installs Kilo CLI and OpenCode from committed lockfiles (`scripts/pins/`, `npm ci`,
integrity-checked) when the pinned version is requested, and falls back to `npm install -g` with a
warning otherwise.
- The installer resolves the pinned CLI's executable from the package's own `package.json` `bin` field
(opencode-ai 1.18.32 ships `bin/opencode.exe`, which left a dangling link before) and fails if the
link target is missing.
- CI runs the unit tests, then the stub end-to-end suite, and a job that installs every
`scripts/pins/*` lockfile with `npm ci --ignore-scripts` and checks the link target exists.
- This branch is rebased onto the 0.2.0 review fixes (restart socket race, closed-on-parked semantics,
approval source).

## [0.2.0] - Unreleased

Task handoff records and a human-approval state, based on community feedback. A supervisor should keep a
Expand Down
13 changes: 13 additions & 0 deletions NOTICE
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,20 @@ This product includes third-party material:
adapters/kilo/config/skills/SUPERPOWERS-LICENSE. Files are unmodified except that
skill-authoring fixtures were removed (see adapters/kilo/config/skills/SOURCES.md).

Ideas credited (no code or text copied; our own wording, see docs/token-savings.md):

2. The `terse` worker-instruction fragment (lib/savers.mjs) is inspired by Caveman,
https://github.com/JuliusBrussee/caveman (MIT License, (c) 2026 Julius Brussee, except its
engine-linked directories such as engine/ and proxy/, which are under BSL-1.1 and are not used).
3. The `minimal_code` worker-instruction fragment (lib/savers.mjs) is inspired by Ponytail,
https://github.com/DietrichGebert/ponytail (MIT License, (c) 2026 DietrichGebert), and by its
adaptation in 9router, https://github.com/decolua/9router (MIT License, (c) 2024-2026 decolua and contributors).
4. The optional RTK integration calls the RTK binary, https://github.com/rtk-ai/rtk (Apache License
2.0), which is not bundled and must be installed separately. The guard-plugin code that calls
`rtk rewrite` is our own.

Not included, installed separately at install time (each under its own license):
- Kilo CLI (@kilocode/cli) and @kilocode/plugin, by Kilo Code
- OpenCode (opencode-ai) and @opencode-ai/plugin, by the OpenCode authors (optional)
- @modelcontextprotocol/sdk and zod (npm dependencies)
- RTK (rtk), optional, only when token_savers.rtk is enabled
Loading
Loading