An in-process OMP extension that drives the freebuff CLI's
terminal UI through a PTY. It runs one machine-wide freebuff Instance in one
shared Workspace and exposes six tools, all loadMode: "essential". It is the
in-process replacement for the freebuff-supervisor MCP server/daemon: no
daemon, no named pipe, no process of its own. Windows-only.
The domain language (Instance, Lock, Phase, Verdict, Owner session, Detached Task, ...) is defined in CONTEXT.md; this README uses those words with those meanings. Decisions live in docs/adr/.
The repo checkout is the extension package (ADR-0005): package.json declares
omp.extensions: ["./src/index.ts"], and OMP loads the extension straight from
the source tree — no build step. Both installs below register the package
through the plugin manager, which loads package.json#omp.extensions and scans
the package's skills/ — so the bundled skill is discovered without config
edits. Either way, reload = OMP restart; there is no hot reload.
Local, from a clone (development: edit the checkout, the link points at it):
git clone https://github.com/vadash/pi-freebuff
omp plugin link C:\path\to\pi-freebuff
From GitHub, no clone (installs a copy under ~/.omp/plugins):
omp plugin install github:vadash/pi-freebuff
Full git URLs work too (https://github.com/vadash/pi-freebuff,
git@github.com:vadash/pi-freebuff.git). Updates: git pull + OMP restart for
a link, omp plugin upgrade pi-freebuff + OMP restart for a git install.
omp plugin uninstall pi-freebuff removes either.
The old surface is gone as of the 2026-10-20 cutover:
- The
freebuff-supervisorentry was deleted from%USERPROFILE%\.omp\agent\mcp.json(backup:mcp.json.bak-cutovernext to it). OMP drives freebuff only through this extension. - The registered
Freebuff Streak Keeperscheduled task runs this checkout'sscripts\keeper.tsunder bun, same daily trigger with-StartWhenAvailable, working directory the repo root. - The prototype repo
C:\projects\_llm\freebuff-mcpis read-only, kept as the port source; its README points here.
A session opened before the cutover still mounts the old MCP server from its startup config: restart OMP to observe the new surface — there is no hot reload.
- Windows (the Workspace junction,
taskkill, and the PTY host are Windows-specific). - The freebuff CLI, installed and signed in once.
- Bun for development (
bun install,bun test,bun run typecheck).
All six tools return errors as
{isError: true, content: [<message>], details: {code, message, ...}}; the
details extras are listed per error below.
Submits one Task and returns at the Ack, never with the answer (ADR-0006):
{taskId, phase: "working"}— the Task is in flight;{taskId, phase: "queued", position}— another Task holds the Instance.
The answer arrives later as an asynchronous aside push into the submitting
conversation and stays pollable via freebuff_progress. One Task runs at a
time; the queue holds four. If the queue is full the call fails with
{code: "busy", busy: true, position: 5}. A Target that cannot be mounted
(filesystem root, the Workspace itself or an ancestor) fails with bad_target
before anything is allocated. A user Esc aborting the tool call detaches a
working Task and cancels a queued or submitting one (ADR-0003).
Polls one Task. Omitted taskId means the active Task, else the newest. The
details carry {taskId, phase, phaseSource, elapsedMs, verdict?, answer?, message?, archivePath?, screenExcerpt?, warnings?, position?}. While the Task
is working, the per-step feed rides the same details: steps (the newest
End agent … step N line, tailed at 200 ms), lastToolCall/lastToolResult
(that step's tool names), partialAnswer (its running fullResponse) and
blocks (the rendered chat store's block count, re-read at the 5 s cadence).
A done Task
returns the verbatim answer as the tool text; a failed Task is an
isError result carrying the Verdict, the reason and a screen excerpt.
Errors: unknown_task, not_owner.
Blocks until one Task settles and returns at the Verdict, so a turn holds a
long Task without polling (ADR-0008): a done Task returns the verbatim
answer, a failed Task the same isError line freebuff_progress renders,
and an already-settled Task returns its stored result immediately. Omitted
taskId waits for the oldest unsettled owned Task — the active one first,
then the queue head — so consecutive waits drain the queue stepwise.
timeoutSec uses the bounds freebuff_wait declares: the tool's description
states the default and the clamp, and the code reads those same constants. A
lapsed bound returns a progress snapshot with "Still settling — call
freebuff_wait again to keep waiting.", and no unsettled owned Task returns
"Nothing in flight" as a normal result. While it blocks, the call emits a progress snapshot about
every five seconds. Owner-gated like freebuff_progress; errors:
unknown_task, not_owner. A user Esc aborts the call, not a working Task —
it detaches (ADR-0003) — while a queued Task is cancelled.
Stops the active Task through the cancel ladder (Esc, a wait, Ctrl-C up to
three times, then a forced kill) and waits for it to settle, up to about five
seconds, before returning {taskId, verdict, archivePath?}. It targets only
the active Task, takes no taskId, and stops the queue not at all. If the Task
finished inside the ladder's window the verdict is done. The Instance exits;
the next Task spawns fresh; queued Tasks behind keep running. Errors:
no_active_task, not_owner. A queued Task is dequeued only by a user
interrupt of the tool call (ADR-0003), never by this tool.
Diagnostics, readable from any session — the one tool not owner-gated. The
details combine the machine facts with this process's queue:
lock {state, ownerPid?, instancePid?, kind?, startedAt?}, instancePid?,
hourMinutesLeft? (our Instance's Hour-session remainder from its per-pid
record), screenDrift (an unrecognized-frame dump exists for the installed
version the shipped corpus does not cover), target? (the junction's current
destination), foreign.live
({pid, model, expiresAt}[], freebuff's own live-slot records), archive {count, totalBytes, newestId?}, abandoned[], plus queue {depth, taskIds}
and activeTask.
Reads the Instance's flattened terminal screen. Owner-gated. Errors:
not_owner, no_instance.
A slash command, not a tool: the human runs it after a freebuff CLI update,
before a real Task. It checks the keystroke whitelist and the screen
classification over the recorded fixture corpus, and reads the live screen too
when an Instance is readable. The report is deterministic — same inputs,
byte-identical text — with the shape {level: "pass" | "degraded" | "fail", checks: [{name, status, detail}], lines, text}, and is delivered as an aside
message.
A standalone scheduled script (ADR-0002 — not an in-extension daemon) that keeps freebuff's Hour-session login streak alive: one small read-only Task a day. Register it once, by hand:
powershell -File scripts\register-keeper-task.ps1
The task registers as Freebuff Streak Keeper and fires daily at 21:00 local
(18:00 UTC on TRT); the allowance itself resets at 21:00 UTC.
-StartWhenAvailable fires a missed slot on the next boot. -Unregister
removes the task; -WhatIf dry-runs the registration.
Each run (scripts/keeper.ts) probes before it touches anything: a chat dir
or a Task archive started at or after 21:00 UTC means the day already had
activity, and a live freebuff Instance from freebuff's own per-pid records
means the slot is taken — the run exits without spawning. Otherwise it takes
the Lock with kind keeper, points the Workspace's repo junction at the
Target, drives ONE read-only Task ("In one sentence, what does README.md do?
Read only — do not edit anything."), then kills only the Instance it started.
A Lock held past the claim deadline skips the run (never stolen); Abandoned
Tasks are reported in the log, never resumed.
PI_FREEBUFF_KEEPER_JSON overrides the run — the same JSON shape as
PI_FREEBUFF_RUNNER_JSON plus target, e.g. to point a manual run at a test
rig. Progress lines tee to the console and to keeper.log in the repo root.
Two gated test modules drive the real installed freebuff CLI. They never run in CI and skip green unless their flag is set:
PI_FREEBUFF_LIVE=1 bun test tests/live.test.ts
PI_FREEBUFF_CAPTURE=1 bun test tests/liveCapture.test.ts
Run the live tier before a release, when the installed freebuff version changes, or when a port defect is suspected; it consumes one real Hour-session task, so run it deliberately, not in a loop. It proves the version gate, Lock obedience and one full Task end to end. Run the capture mode when the corpus needs new screen dumps — it is the re-measure path for a drifted version.
Both build their rig in temp directories (plugin dir, Workspace, a disposable
git init Target): %LOCALAPPDATA%\freebuff-plugin\ is never touched. Only
freebuff's own config dir is real — login state, freebuff-metadata.json, and
the capture dumps land there. The Lock is obeyed: with a foreign record held,
the submit waits out its claim deadline and refuses with a message naming the
holder pid; nothing spawns and the record is untouched.
The screen wordings are version-bound: MEASURED_FREEBUFF_VERSION in
src/protocol/recorded.ts records the version they were measured against, and
a live run against any other version fails before spawning, naming the
PI_FREEBUFF_CAPTURE=1 re-measure path.
Capture samples the live screen across one Task, writes every unique frame to
~/.config/manicode/screen-dumps/<version>/ (one file per freeze key), and
prints a promotion-ready table of dump file → identity/level. Promote a dump:
- Copy it into
tests/fixtures/screen/<version>/, keeping the hash file name. - Add its
<version>/<file>row toRECORDEDinsrc/protocol/recorded.ts, naming the screen it shows. bun test tests/corpus.test.tsenforces the corpus contract.- Review the captures for private data before committing.
Every Task ends under exactly one Verdict, a closed set of nine:
ack_missing, deadline, frozen, crashed, no_answer, driver_error,
slot_held, displaced, cancelled. A Task dropped before its first submit
(queue dequeue, session death) archives nothing or no Verdict; an archive
without a terminal Verdict reads as an Abandoned Task.
Caller mistakes and freebuff failures are distinct. Refusals (busy,
bad_target, unknown_task, no_active_task, not_owner, no_instance,
lock_busy, torn_down) are structured tool errors naming what to change;
a Task failure is an isError result carrying the Verdict, the reason, and a
screen excerpt with the last lines the Instance showed.
Between submit and settle a Task moves through the Phases: queued,
submitting, working, settling, then done or failed.
Everything lands in the plugin directory %LOCALAPPDATA%\freebuff-plugin\:
lock.json— the turn-scoped Lock,{ownerPid, instancePid, kind, startedAt}(kindisextensionorkeeper); taken before a Task's spawn, deleted on every exit path.errors.jsonl— error lines the run watcher saw on the screen.tasks/<taskId>/— the Task archive, written on every settle, success included:meta.json(phase log, Verdict, timings, model, target),answer.md(the full answer),chat-messages.json,screen.txt, andstderr.logwhen non-empty. Never auto-pruned.freebuff-ws\— the Workspace, the Instance's fixed working directory, with therepojunction pointing at the Target (the session's cwd resolved to its git root).
Screen dumps are the one thing outside the plugin directory:
~/.config/manicode/screen-dumps/<version>/ — under freebuff's own config dir
— holds the degraded and unrecognized frames the settle loop dumped, one file
per freeze key, write-only.
src/index.ts— extension factory: the six tools,/freebuff-doctor, per-session owner bindings.src/singleton.ts— the process-scoped singleton: queue, Task registry, ownership gate, Instance drive.src/turnRunner.ts— one Task from Lock to Verdict: submit, Ack, watchdog, archive.src/driver.ts— the PTY driver for one Instance: spawn, whitelisted keystrokes, screen reads, cancel ladder.src/settle.ts— the settled-screen policy behind one function.src/lock.ts— thelock.jsonrecord: take, classify, reclaim.src/workspace.ts— Workspace directory,repojunction, Target resolution and safety.src/status.ts—freebuff_statusdata assembly.src/archive.ts— Task archive writes and Abandoned-Task inference.src/config.ts— screen geometry (48×160), the size every fixture captures.src/doctor.ts— the deterministic doctor core.src/protocol/— screen emulator and classifier, screen signatures, chat store reader, keystroke whitelist, fixture corpus, recorded classification table, markers.src/host/pty.ts— the only host-internals seam (@oh-my-pi/pi-natives,@oh-my-pi/pi-utils/vterm); enforced bytests/hostSeam.test.ts.src/keeper.ts— the Keeper core: activity window, probe, one keeper Task.scripts/keeper.ts— the headless Keeper entry the scheduled task runs;PI_FREEBUFF_KEEPER_JSONconfig, log tokeeper.login the repo root.scripts/register-keeper-task.ps1— registers the daily 21:00 Task Scheduler entry (bun +scripts\keeper.ts);-Unregisterremoves it,-WhatIfdry-runs.skills/freebuff/SKILL.md— the bundled skill;disable-model-invocation: truekeeps it invisible to the model, reachable only via/skill:freebuff/skill://freebuff.tests/— unit tests per module, corpus tests over the recorded screen fixtures, the host-seam policy guard, stub end-to-end tests overtests/helpers/stub-freebuff.mjs, and the SDK harness tier that drives the extension factory in a real agent session (tests/helpers/sdkHarness.ts); the gated live modules over the real CLI (tests/live.test.ts,tests/liveCapture.test.ts) share the rig intests/helpers/live.ts; the runner config env (PI_FREEBUFF_RUNNER_JSON) is what points a run at a real plugin directory instead of a test rig.
- CONTEXT.md is the single domain reference — the glossary and the prototype vocabulary this repo inherits; this README deliberately does not duplicate it.
- docs/adr/ records the decisions: ADR-0001 one turn-scoped Lock in one shared Workspace; ADR-0002 the Keeper stays a scheduled script; ADR-0003 three endings — cancel, detach, abandon; ADR-0004 Task state is process-scoped; ADR-0005 the repo checkout is the extension package; ADR-0006 the run tool returns at the Ack; ADR-0007 the directory-line guard is dropped; ADR-0008 the bounded Wait tool.