Skip to content
This repository was archived by the owner on Sep 6, 2026. It is now read-only.

Releases: The-Masked-Bear/offset-terminal

offset 0.9.0 - a self-verifying platform

Choose a tag to compare

@github-actions github-actions released this 02 Sep 14:54

Offset stops being a coding terminal and starts being a platform that checks its own work.

Seven features. The first two change what /spec means.

Branches get audited

/spec ranked branches on verification, regressions, diff size, lint and a reviewer. None of those notice a branch that passes every test and introduces a shell injection.

A security criterion now scans every branch's worktree: hardcoded credentials, shell=True on interpolated input, eval on a non-literal, pickle.loads, yaml.load without a loader, SQL built by concatenation, verify=False, world-writable chmod, tempfile.mktemp. One high-severity finding zeroes the criterion outright rather than being averaged away, and at weight 3.0 that costs more than diff size, lint and reviewer preference combined — so a clean branch with a sixty-line diff beats a one-line branch that unpickles untrusted bytes.

Every rule is paired with a near-miss it must stay silent on, because a scanner that cries wolf gets switched off and a switched-off scanner also sells false comfort. {"password": value} is a field name. password = os.environ["PW"] is the fix. An eval inside a string literal is documentation. An XML namespace is not a plaintext request.

Running it over this repository found four false-positive classes, and each is now a regression test: a rule's own regex, an eval inside a string, the scanner's own explanatory comment, and an XML namespace. It reports 0 findings across 104 files of offset/.

/audit runs it by hand; /audit --diff over just the pending change.

Benchmarks that admit doubt

Twelve runs, a discarded warmup, min/median/stdev and peak RSS — and it refuses to name a winner it cannot distinguish. If the two sample sets overlap, the verdict is indistinguishable and the criterion is excluded from the ranking rather than scored.

Reporting "3% faster" from twelve samples on a thermally throttled Raspberry Pi is a fabricated result, and a ranking built on fabricated results moves confidently in random directions — which is worse than no benchmark at all, because it is wrong and trusted.

A command that exits non-zero is reported as a failed benchmark, not a fast one. A crash returns almost instantly, which naive timing scores as a spectacular improvement.

/bench --save records a baseline; /bench compares against it and says whether the difference means anything.

Both new criteria are excluded when they did not run, never scored zero. Whether an audit was reachable is not a property of your code.

Where the money went

Usage events used to fly past and nothing caught them.

/cost answers what a turn cost — by model, by day, by session. /cost failures lists every turn that broke and why. /trace shows the last turn's span tree: steps, tool calls, durations, which one failed.

An unknown price reports as unknown, never as zero. A model the table has not heard of yields no cost figure, and a total that omits one says so with a +. A confident zero looks like an answer, and nobody re-checks an answer. ~/.offset/pricing.json overrides any price.

GitLab, first class

Issues, merge requests, pipelines and job logs, in the same shape as the GitHub support: /issues, /mr, /mrs, /pipeline. Self-hosted via GITLAB_HOST. The token is read from GITLAB_TOKEN or glab's own config and sent as a header — never in argv, never as a query parameter.

Issue to pull request

/issue 42 reads the issue, restates what it thinks you want, plans, branches, implements, tests and opens the pull request. Checkpointed at every stage, so /issue resume picks up where a crash or a closed laptop left off. Either forge.

Watch it from your phone

/monitor start serves one self-contained page — no CDN, no framework — showing the current turn, background jobs and token spend, with a button to cancel a job.

The security is the substance here, not a wrapper. It binds 127.0.0.1 by default; a wider interface must be asked for by name and warns you when you do. Every route including the page requires a bearer token, generated with secrets.token_urlsafe, stored 0600, and compared with secrets.compare_digest — not ==, which short-circuits on the first wrong byte and leaks the token's prefix to anyone timing the responses. The one mutating route additionally requires the token in a header, which a cross-origin navigation cannot set. There is no static file route at all, so no request path ever reaches the filesystem.

Cloud and remote execution

/remote add registers a machine; /remote run executes a task on its daemon over SSH. The heavy work happens where the cores are; the transcript arrives here.

A security seat in the pipeline

ROLES gained security and researcher, so /flow and /decompose can seat an auditor that is a different model from the one that wrote the code. A model reviewing its own work is not a review.

Three bugs found while building this

  • A deadlock in the recorder. A non-reentrant lock froze the agent on the first step of a turn nobody opened explicitly. A hang is the one failure a blanket except cannot swallow, so telemetry — which must never cost a turn — could have cost every turn. Now an RLock, with a threaded regression test.
  • A command that would have deleted another. Registering telemetry as /usage would have silently replaced the existing /usage (model, key, tools, approval), because _extend replaces a colliding name. It ships as /cost.
  • RACEAI MODELSIN. The website's hero title ran its words together: a trailing space inside an inline-block collapses at the box boundary. The separator has to be a sibling text node.

Numbers

1425 → 1831 tests. 24 → 25 tools. 45 → 56 commands. Still one dependency, still Python 3.11–3.13, still ARM64.

offset 0.8.1 — restores the Lite/Plus tier

Choose a tag to compare

@github-actions github-actions released this 01 Sep 16:50

Restores the Lite / Plus tier. No feature was removed — 0.8.0's nine
features are all still here; only the licence gate is back.

/spec, /flow, /task and the GitHub commands (/pr, /review, /fix-ci,
/resolve-comments) require Offset Plus again, exactly as they did in 0.7.

Everything else stays free, including all of 0.8's new work: ghost text,
hash-anchored patching, /decompose, auto-compacting context, mid-stream
auditing, filesystem snapshots, the loopback bridge, /collab and /market.

Why

0.8.0 removed the paywall. That went further than it should have, and it is the
owner's call to make, not the tooling's. Reverted at their request.

0.8.0 shipped without the gate. It cannot be unpublished, so if you install
by version you can still get it. pip install offset-terminal resolves to 0.8.1.


1425 tests pass, 5 skipped.

pipx install --force offset-terminal

offset 0.8.0 — the remaining nine features

Choose a tag to compare

@github-actions github-actions released this 01 Sep 13:51

Superseded by 0.8.1. This build shipped
without the Lite/Plus gate; that was reverted. The nine features below are
all still present in 0.8.1 — only the paywall changed.

The remaining nine features. 10,285 new lines, all of them running locally,
none of them gated.

Ghost text

Dim completions ahead of the cursor — slash commands, workspace paths, and your
own history. Right arrow accepts, ctrl-right takes one word.

The whole design constraint is latency. The engine has a 40 ms deadline: a
suggestion that misses it is simply absent, never a stutter while typing. It
never suggests mid-word or with the cursor parked mid-line, because those are
completions you cannot coherently accept.

Hash-anchored patching

A line-number patch bets that nothing moved between reading a file and writing
it. It loses that bet — silently — whenever a formatter runs, a sibling edit
lands, or an earlier hunk shifts everything below it.

An anchor now identifies a region by the hash of its bytes and of the bytes
around it
. Two identical blocks are told apart by their surroundings. A region
that changed is refused, with the reason named, rather than applied to the
wrong place. Hunks are all-or-nothing, because half-applied is the worst
outcome: the file is in a state nobody asked for and nothing recorded which
half landed.

Found writing the tests: Result.refused() left text empty, so a caller
writing result.text without checking ok would have truncated the file to
nothing
. text now always means "what this file should contain".

Architect and coder decomposition

/decompose add rate limiting to the API

An architect plans a dependency graph — each unit naming the files it touches
and the interfaces it provides or consumes — and coders execute it in waves.

Two units naming the same file are serialised at planning time, because
concurrent edits to one file lose one of them and nothing reports it. Cycles are
refused and named rather than partially executed. A failed unit blocks its
dependents and leaves independent subtrees alone.

Infinite sessions

Compaction now happens without being asked, at turn start — never between tool
steps, where rewriting history would break tool_use/tool_result pairing.

Measured on a real session:

60,566 → 8,351 tokens    69 entries summarised    95 originals still on disk

Mid-stream auditing

The provider stream is watched as it arrives, and a generation that has plainly
gone wrong is halted before more tokens are spent — runaway repetition, or a
claim about a file that is not there. A second model can be sampled as a judge,
on a worker, so it never holds up a token.

Off by default, deliberately. These are heuristics, and a false halt costs
the user a correct answer they cannot recover — worse than the tokens it saved.
Every halt names its evidence so a human can overrule it.

Filesystem snapshots

Zero-cost workspace isolation via btrfs, zfs, APFS clonefile or reflink,
probed by trying the cheap operation rather than guessing from the mount
table. Where none works it falls back to a real copy and says so, rather than
reporting a snapshot as free when it cost a gigabyte.

Release is idempotent, needs no root, and refuses — loudly — to delete the
workspace it was pointed at.

The loopback bridge

Code the agent is executing can call the agent's own tools, over an
authenticated socket published only for the duration of that call.

The permission system still applies: a writing tool with nobody to approve
it is refused, not allowed. This is not a way around the approval prompt. The
token travels by environment because argv is world-readable in /proc, and
nesting is capped by a counter that fails closed when corrupted — restarting
the count at zero is exactly the runaway the counter exists to stop.

Terminal multiplayer

/collab host
/collab join 192.168.1.5:8792

Several people, one session, over the same framing as the editor bridge. One
driver at a time
, and the second claimant is told who holds it rather than
silently queued. Chat and prompts are separate, so an observer's aside cannot
become an instruction. A peer that stops reading is dropped rather than stalling
the room for everybody else.

MCP marketplace

/market search filesystem
/market install github

Installing records a server; it never executes anything at install time.
Anything off the built-in list is untrusted until you say otherwise, and a
server missing its environment variables is installed but reported as
unconfigured, by name.

Bugs found on the way

The failure handler killed startup. eggs.event("subsystem_failed", name=name) collided with event's own first parameter, so the guard meant to
survive a broken extension raised TypeError and took the shell with it. It had
never fired before, so nothing had noticed.

/help was deleting commands in silence. At forty-five entries the
two-column layout needed 23 rows and the pane gives 22. The pane scrolls from
the top, so the overflow removed the first commands — /model among them
— with nothing to say so. The row budget now lives in one constant both sides
read, and the layout drops summaries for a dense grid rather than dropping
commands. Where a terminal is genuinely too small, it says how many are hidden.

A background thread wrote into the real ~/.offset. The model-listing
refresh resolved settings.home() inside the worker; a daemon thread outlives
its caller, so by the time it answered, $OFFSET_HOME had moved on. Reproduced
deterministically, fixed by resolving on the calling thread. It would have hit
offset --home in production, not just the suite.


1425 tests pass, 5 skipped (was 1234). CI runs Python 3.11/3.12/3.13 and the
TypeScript client on Node 20 and 24.

pipx install --force git+https://github.com/The-Masked-Bear/offset-terminal.git

offset 0.7.0 — live model discovery, Antigravity sign-in, headless daemon, TypeScript client

Choose a tag to compare

@github-actions github-actions released this 01 Sep 07:39

Four things, three of them bugs you would have hit.

The model list is asked for, not remembered

The catalogue was a hardcoded table, and it had gone stale exactly as designed.
Measured against a real key on the machine this was written on:

38 Google models live      4 in the shipped table

So gpt-5.x, gemini-3.x and everything else recent was usable by typing
the id and invisible in every list offset shows. offset now asks each provider
what it has, caches it, and merges it over the curated table.

/models                 what you can run, plus the curated set
/models gpt-5           search everything, live included
/models --all           the lot
/models --refresh       re-ask now, and say where each list came from

The curated table is kept rather than replaced: a listing endpoint reports an
id, not that a model is the one you want planning your work, so role hints
and context windows survive the merge.

Three rules keep it honest. The refresh is a background thread, so startup
waits for nothing
and a cold cache shows exactly what shipped before. A
provider with no credential is never asked — an unauthenticated listing
call earns a 401 per launch and teaches nobody anything. And a provider that
fails keeps the models it last reported, because one flaky launch should
not empty your picker. OFFSET_NO_MODEL_FETCH=1 disables it entirely.

The shipped table also had duplicate o3 and o4-mini entries. Nothing had
noticed.

Sign in to Google Antigravity

It asked for an API key when you wanted to link an account, because
google-antigravity had been wired into the key paths only — the provider was
the plain Gemini one wearing an Antigravity label, pointed at
generativelanguage, which does not accept an account token at all.

Now it is a real sign-in against Cloud Code Assist: PKCE on the registered
redirect, the scopes it requires, the project discovered once per token, the
request wrapped in the envelope the backend expects, and every SSE frame
unwrapped from its response field.

/login          pick google-antigravity, sign in with your browser

Google does not publish the client id its own binary carries, and no community
client embeds it — a credential lifted out of a shipped binary gets rotated and
takes every user with it. So offset asks for yours and prints the exact
redirect URI to register
, which is the omission that otherwise fails as
redirect_uri_mismatch only after you have already signed in.

An API key still works and takes the ordinary Gemini path. That is the mode
Antigravity itself calls modelProvider: gemini, and it is the right answer on
a headless box.

Two bugs found on the way. OAuthApp declared its entire field list twice,
which is how redirect_port nearly went missing. And the "configure this"
message named GOOGLE-ANTIGRAVITY_CLIENTID — a hyphen cannot appear in a shell
variable name, so the instruction was one nobody could follow. That affected
every hyphenated provider, including claude-pro and openai-chatgpt.

Headless daemon

The bridge could always drive a real turn, but it only existed while a TUI was
open — so using the editor integration meant keeping a terminal open beside it.

offset daemon                          # unix socket, this machine
offset daemon --listen 127.0.0.1:8791  # a port, for a client across an SSH hop
offset daemon --idle 900               # exit after 15 minutes with no client

Same session, same toolbox, same bridge, with the terminal removed and a signal
handler in its place. It prints a descriptor naming where to connect, saves the
session on the way out rather than dropping it, and stops on SIGINT, SIGTERM or
SIGHUP.

Loopback unless told otherwise, and the token is mandatory on every transport.
Prefer an SSH tunnel to binding 0.0.0.0.

A TypeScript client

The agent is Python and stays Python — 37,000 lines and 1,200 tests do not get
rewritten for a language preference. But everything on the other side of the
socket is better served by TypeScript, so @offset/client is a real package
with zero runtime dependencies, matching the Python side's own policy.

import { OffsetClient } from "@offset/client";

const offset = new OffsetClient({ name: "my-editor" });
await offset.connect();

offset.on("tool.started", ({ name }) => console.log("running", name));

const { text } = await offset.prompt("add a test for the parser");

The VS Code extension is its first consumer and no longer carries its own copy
of the protocol. CI typechecks the extension against the client, so a type that
drifts from bridge.py breaks the build rather than somebody's editor.

Writing it immediately caught four protocol types that had been guessed wrong:
additions/deletions rather than added/removed, original/current rather
than before/after, state rather than busy, and jobs as a declared shape
rather than unknown. All four verified against a running daemon carrying a
real diff.


1203 tests pass, 5 skipped (was 1130), plus 19 TypeScript tests — none of
which sleep. CI runs Python on 3.11/3.12/3.13 and the client on Node 20 and 24,
the latter because the first CI run built cleanly and then found no tests at
all: node --test only learned to expand glob patterns itself in Node 22.

pipx install --force git+https://github.com/The-Masked-Bear/offset-terminal.git

offset 0.6.0 — packaging, release pipeline, curl installer

Choose a tag to compare

@github-actions github-actions released this 01 Sep 02:49

Packaging, an automated release pipeline, and installing without pipx.

One line, whatever you have

curl -fsSL https://raw.githubusercontent.com/The-Masked-Bear/offset-terminal/main/install.sh | sh

Prefers uv, falls back to pipx, and failing both builds a private virtualenv with a shim on your PATH — so it works on a machine with nothing but a Python interpreter.

It picks the newest interpreter that is new enough rather than the first one on PATH, names the profile file you actually have when the shim directory is not on it, and falls back to the venv method if the chosen installer is present but broken. It never uses a bare pip install --user, which on a PEP 668 system fails with a wall of text about externally managed environments.

POSIX sh, shellcheck clean, and verified end to end for all three methods into an isolated HOME.

Or pick your own

uv tool install offset-terminal
pipx install offset-terminal
python3 -m venv ~/.offset-venv && ~/.offset-venv/bin/pip install offset-terminal

# the development tip
pipx install git+https://github.com/The-Masked-Bear/offset-terminal.git

The site's install modal now has tabs for each. The pip line contains &&, so the copy button reads textContent rather than innerHTML — copying the HTML entity would hand you a command your shell cannot run.

The package is offset-terminal on PyPI because offset is taken there by an unrelated project. The command is still offset.

The rename reaches further than the manifest

installed_version() checks the old name too, because anyone who installed from git before this has their metadata filed under it and would otherwise look like a fresh checkout. detect_install() builds its upgrade command from whichever distribution the environment actually answers to — asking pip to upgrade a name it has never heard of fails with nothing useful.

Release pipeline

Driven by the tag. git tag -a v1.2.3 && git push --tags is the whole ritual.

Before building it refuses a tag that disagrees with pyproject.toml or __version__: a mislabelled wheel cannot be corrected afterwards, because PyPI will not let a version be reused. Then the full suite on 3.11, 3.12 and 3.13; twine check --strict, which catches a README that renders badly on PyPI; and an install of the built wheel into a fresh venv to prove the command runs and detects its own install method.

The first tagged run proved the pipeline and found a flaw in it: publish-github depended on publish-pypi, so a PyPI misconfiguration withheld the artefacts from the GitHub release as well. Wrong coupling — the tag exists and the wheel is tested. They are now independent.

Still to do, and it needs your PyPI account

PyPI publishing is wired up with Trusted Publishing (no API token to store or rotate), but the project must be registered as a pending publisher on PyPI first. Until then that one job fails with invalid-publisher and everything else in the release succeeds.

RELEASING.md has the exact field values, including the environment name — the part that silently fails to match if left blank.


1130 tests pass, 5 skipped. The wheel and sdist are attached below; both install cleanly.

offset 0.5.2 — audit fixes: debug crash, browser leak

Choose a tag to compare

@The-Masked-Bear The-Masked-Bear released this 01 Sep 02:17

Two bugs found by auditing every tool and command, rather than by a test failing.

debug_inspect crashed instead of refusing

SessionBook.current is a method, and the tool read it without the call. A bound method is truthy, so the is None guard never fired and every inspection on a shell with no live session died on .client with an AttributeError.

That is the common case for these tools — a model has to launch a session before it can inspect one — so it was the first thing anyone would hit.

before:  debug_inspect stack  ->  AttributeError: 'function' object has no attribute 'client'
after:   debug_inspect stack  ->  no debug session; launch one first

All six inspect actions and all five movement actions now refuse cleanly. tests/test_debug_tool.py pins it, including the specific mistake: _current() must return None, never a bound method.

A killed offset leaked eleven chromium processes

start_new_session=True is what lets close() signal the browser's whole process group without signalling offset too. It also detaches the browser — so a shell killed outright, rather than exiting through its finally, left a headless browser running that nobody could see. On a Raspberry Pi that was eleven processes and most of the free memory.

PR_SET_PDEATHSIG closes the gap: the kernel kills the child the moment its parent goes, however it went. Linux only, best effort, and the ordinary teardown path is untouched.

Verified twice:

run 1: alive=12  after_sigkill=0  orphans=0
run 2: alive=12  after_sigkill=0  orphans=0

What the audit checked and found clean

Modules importing 87 / 87
Stubs, TODOs, NotImplementedError none
Dependency policy stdlib + prompt_toolkit only, no violations
Tool schemas across 4 provider dialects 23 / 23
Commands dispatching without exception 42 / 42, 7 correctly licence-gated
Duplicate or unreachable commands none
Fresh install from the public git URL works
Session / task / job durability across a process exit survives
Auto-update refusal memo 1 attempt across 3 launches
VS Code extension typecheck clean
Live website serving the current build

One thing that looked like a bug and was not: the builtin tools raise KeyError on a missing required argument, but Runtime validates before calling them, so a model only ever sees missing required argument 'path'.


1125 tests pass, 5 skipped.

pipx install --force git+https://github.com/The-Masked-Bear/offset-terminal.git

offset 0.5.1 — auto-update no longer retries a doomed upgrade

Choose a tag to compare

@The-Masked-Bear The-Masked-Bear released this 31 Aug 18:02

A bug in 0.5.0's auto-update, and a flaky test it exposed.

An advertised release that cannot be installed here was retried forever

autoupdate() re-ran the upgrade on every launch when the version could not advance.

The trigger is specific to how this ships: the version feed is GitHub releases, while the upgrade runs through pip or pipx. Those can disagree — a release that never reached PyPI, a pipx install pinned to a local path, a permission the user does not have — and the installed version then never moves.

Because the check cache stays valid for a day, every single start re-ran a multi-second failing upgrade and printed the same error. Reproduced before fixing:

launch 1: could not update itself: the upgrade reported success but offset is still 1.0.0
launch 2: could not update itself: the upgrade reported success but offset is still 1.0.0
launch 3: could not update itself: the upgrade reported success but offset is still 1.0.0
   upgrade attempted 3 times across 3 launches

A version that fails to apply is now recorded in $OFFSET_HOME/update-refused.json and skipped until the feed advertises a strictly newer one. The user is told once and then left alone; a landed upgrade clears the memo, so one bad release cannot poison every later one.

After the fix — 2 attempts across 5 launches, and launches 2–4 print nothing:

launch 1: attempts=1  could not update itself: ...
launch 2: attempts=1  9.9.9 could not be installed here; waiting for a newer release
launch 3: attempts=1  (silent)
launch 4: attempts=1  (silent)
--- a strictly newer release appears ---
launch 5: attempts=2  trying again

A flaky test, fixed rather than tolerated

The same run turned up two import-time ceiling failures. They were not a regression — import_cost() timed the whole subprocess, interpreter startup and scheduler noise included, so on a machine loaded by the suite itself it reported 0.87s for an import that actually costs 0.29s.

It now reads the cumulative figure from -X importtime, which measures the thing the test is about. Verified under deliberate load: passes, reporting 0.169s and 0.289s against 0.34s and 0.50s ceilings.

Also: a test helper added in 0.5.0 monkey-patched update.installed_version without restoring it, leaking into every later test in the process. Replaced with monkeypatch.


1104 tests pass, 5 skipped.

pipx install --force git+https://github.com/The-Masked-Bear/offset-terminal.git

offset 0.5.0 — self-updating, and the new capabilities made visible

Choose a tag to compare

@The-Masked-Bear The-Masked-Bear released this 31 Aug 17:35

Offset keeps itself current

It checks for a release in the background, and installs a waiting one on the next launch, before the shell opens — then re-executes, so you are already running the new version rather than being told to restart.

$ offset
offset 0.5.0 is available; updating 0.4.0 -> 0.5.0
offset updated itself: 0.4.0 -> 0.5.0

Three things make that safe enough to do unattended:

  • It runs before the shell, never during. Modules are already imported once a session is live, so replacing files mid-turn would leave half the program on the old version. Applying early and re-executing is the only honest way to do this in a running interpreter.
  • It reads a cache, not the network. The previous run's background check left an answer on disk. Measured: 0.014s when already current, 0.02s offline with no cache, and the CLI still starts in 0.24s. An offline start costs nothing.
  • It refuses what it cannot prove. A git checkout is skipped with the reason. apply() verifies the version actually moved rather than trusting an exit code. Every failure is swallowed — a program that will not start because it could not upgrade itself is worse than one running last week's build.

Opt out with OFFSET_NO_AUTO_UPDATE=1, or {"update": {"auto": false}} in ~/.offset/config.json. Switching update checks off switches auto-update off with it.

A re-entrancy bug, found by testing

/update was being registered twice. update_commands() imports the shell registry, whose module body asks this module for its COMMANDS in order to register them — so the first access re-entered __getattr__ while the list was still empty, and both the inner and outer call extended it. Re-checking after the build is what makes the list built once rather than once per level of re-entry. tasks.py had the same shape and is fixed the same way.

The new capabilities, now actually visible

The last few releases added a lot that was never shown anywhere.

README — a What's New section with worked examples for each: code intelligence, debugging, the browser agent, code search, the GitHub workflow, persistent tasks. Command matrix and tier table brought up to date, tool list extended.

Website — six new cards in the existing neubrutalist style:

CODE INTELLIGENCE Follows imports, re-exports and shadowing — finds the callsites grep misses
REAL DEBUGGING Breakpoints and live variables, instead of print and guessing
BROWSER AGENT Drives real Chromium, reads the accessibility tree, reports console errors
SEARCH THAT KNOWS CODE getUserAuth finds get_user_authentication
GITHUB NATIVE /pr writes it, /fix-ci excerpts the error from megabytes of log
SURVIVES THE TERMINAL Background agents, resumable tasks, session resume, self-update

The stats now say something true — 23 tools, 1 dependency, 1097 tests — and the demo terminal on the site learned debug, browser, search, /task, lsp and /pr, so a visitor can try them without installing anything.

Verified by rendering the page in real headless Chromium: all ten cards present, zero console errors, all four new demos animate correctly.


1097 tests pass, 5 skipped.

pipx install --force git+https://github.com/The-Masked-Bear/offset-terminal.git

Already on 0.4.0? It will update itself.

offset 0.4.0 — VS Code companion

Choose a tag to compare

@The-Masked-Bear The-Masked-Bear released this 31 Aug 17:15

The last of the fourteen features. Still stdlib-only on the Python side.

VS Code companion

editors/vscode — offset keeps its terminal identity. The extension does not reimplement the agent UI; there is one conversation and it stays in your terminal. It surfaces the three things an editor genuinely does better:

  • Status bar — model, agent state, running background jobs
  • Pending changes — the agent's uncommitted edits, openable as a real side-by-side diff
  • Send selection — hand the agent a selection with its file and line, which a terminal cannot see for itself

Commands: offset.connect, offset.prompt, offset.showDiff, offset.cancel, offset.openSession, offset.refresh.

The bridge is authenticated

It can apply edits, so an unauthenticated local socket would be a real vulnerability. The client performs the same handshake any client must: read ~/.offset/bridge.json, read the token from the path it names (both 0600), connect, then hello. Every other method is refused until that succeeds — confirmed by an unauthenticated probe getting status was sent before hello; authenticate first.

Two bugs found by actually driving it

Verified by running the compiled TypeScript client against a real Python bridge rather than assuming the shapes matched:

  • The descriptor writes transport: "unix" with a path, not a socket field. Reading the wrong name fell through to the TCP branch and connected to port 0 — whose failure carries no message at all, so the symptom was a rejected promise with an empty string.
  • Promise.withResolvers needs lib: ES2024. ES2022 typechecked everything else and failed only on that one call.

End to end against a real ShellState: connect, authenticate, read model and state, and diff correctly reporting an actual uncommitted git change. tsc --noEmit clean, tsc -p emits, node_modules and out/ ignored rather than committed.

Building

cd editors/vscode
npm install && npm run compile

Then F5 for an Extension Development Host, with offset running in a terminal.

Also

Fixed two flaky tests I had written. Both assumed ULID prefixes were discriminating — but ULIDs are monotonic within a millisecond, so two minted in the same one differ only in their final characters, and even a 20-character prefix was ambiguous. One now separates the sessions in time; the other sets mtimes explicitly so it asserts the ordering rule rather than the filesystem's timestamp resolution.


1083 tests pass, 5 skipped. 23 tools, 42 commands.

pipx install --force git+https://github.com/The-Masked-Bear/offset-terminal.git

offset 0.3.0 — retrieval, persistent tasks, GitHub workflow

Choose a tag to compare

@The-Masked-Bear The-Masked-Bear released this 31 Aug 12:42

Completes the four features that shipped as foundations only in 0.2.0. Still stdlib-only.

Code retrieval

search getUserAuth              # finds get_user_authentication
symbols defines get_user_authentication
symbols importers pkg/auth.py   # what breaks if I change this

An incremental SQLite index. Content hash decides re-parsing, not mtime — mtime alone re-parses a file a checkout merely touched and misses a change made inside the same second. A warm refresh parses nothing, so it can run before every query and is never stale in a way you have to think about.

Ranking is BM25 blended with symbol definitions, path affinity and import proximity. BM25 saturates term frequency, so a file repeating an identifier forty times does not automatically beat one using it four times in the right place. Structural signals are weighted below the lexical score so they break ties rather than invent matches.

Identifier splitting is what makes getUserAuth match get_user_authentication. That is lexical expansion, not embeddings, and the docstrings say so.

Persistent tasks

/task implement auth
/tasks
/task resume 01M1BW

plan → implement → test → (fix → retest)* → report, with every transition written to disk before it is acted on. A task interrupted between stages resumes at the boundary; a stage the process died inside is retried rather than assumed done. The fix loop is bounded and the bound is visible in the record.

Two bugs the tests caught while being written: the fix counter summed attempts on stages that had not run yet, so the ceiling never tripped and the loop inserted fix/test pairs forever; and the skip loop relabelled the one stage carrying the reason the task stopped, because FAILED is not a settled state.

GitHub workflow

/pr                    # summarise the branch, open the PR
/review 12             # prints
/review 12 post        # publishes
/fix-ci
/resolve-comments 12 reply

Reviewing prints by default — posting to someone's repository is a side effect other people see, so it takes the extra word. CI logs are excerpted to the failing region, not forwarded: an Actions run is routinely megabytes to reach twenty relevant lines. A dirty tree refuses to open a PR that would not contain the work.

Plus a read-only github tool for PR details, diffs, checks and comments.


23 tools, 42 commands. 1083 tests pass, 5 skipped (was 1041).

Also fixed: /help no longer assumes the command set. Its truncation test asserted that width 80 always cut a summary — a fact about which commands existed, not about the renderer. A longer command name widened the name column until everything fitted, and the test failed while the layout was correct. It now asserts the invariant: no line exceeds the pane, and a cut is marked.

pipx install --force git+https://github.com/The-Masked-Bear/offset-terminal.git