Skip to content

Releases: Wouterbeets/self

v1.1.0 — intents, coordination, a clean tree

Choose a tag to compare

@github-actions github-actions released this 24 Sep 19:31

The README now leads with the one line that matters: put use self in
AGENTS.md. The tree is trimmed to the kernel, two lessons (journal,
memory), the claude and opencode mind wrappers, the offline stub, and the
optional ask adapter. experiments/, results.md, lessons/chat and
examples/mind-grok are removed; they remain in git history.

self hear --after <head> commits only if the log head still matches, and
self watch waits for matching events without appending. Learning an account
twice appends nothing new. The log refuses to truncate events.jsonl while it
still holds complete lines.

The optional ask adapter (examples/ask) ranks capabilities and recursively
reads relevant views for a task, locally or through an opt-in hosted backend.
It runs outside the kernel and never invokes commands.

A Unix and simplicity pass removes automatic script examples from pending
prompts, shares event validation and completion loading, and shortens loop
guidance. Pending declarations and refusal evidence still ride the prompt.
brief preserves multiline descriptions and accepts unqualified nested names.
Fixed-form verbs reject extra arguments before acting; unknown verbs suggest a
working self prompt invocation. self-serve now requires
POST for /run/; GET and HEAD cannot execute commands. Existing HTTP clients
that use GET for actions must switch to POST.

All event-producing entry points now share internal ingestion: hear, commands,
and accounts use the same locked commit and kernel event handling. Commands keep
strict batch validation, provenance, automatic recording, and existing stdout.
Command-emitted retirements now clean up capability links immediately, as hear
already did.

intent.declared and intent.closed carry desired outcomes between minds.
Open summaries appear in the brief; self brief intent/<name> reads the full
intent and closure evidence, and self brief intent/ lists open intentions.
intent.md remains the account's purpose. Learning declares a named intent to
interpret it, so an interrupted learning pass can resume in a later loop.
Imported intentions remain lineage until adopted locally. Old work.* events
and brief work/ selectors still replay; old unnamed account intent receipts
do not become pending. Replay uses local append order, without ownership,
leases, or cross-instance conflict resolution.

Prompts now lead with desired outcomes, keep capability construction conditional,
and leave detailed design guidance in self help.

Runnable examples for a printer hobbyist, household, and standalone script
author live in testdata/intent/.

Bare self reconnects an agent with its persistent identity and capability
index while it continues the user's task. Pass instructions and pending
script authoring belong to self prompt, self learn, and self loop.
Use self prompt | mind | self hear for a pass without an ask; existing
self "<ask>" pipelines remain supported.

v1.0.0 — one fixed-point loop

Choose a tag to compare

@github-actions github-actions released this 12 Sep 17:55
ddb40fa

The first stable release. Everything below this heading and under the next one
is what changed since v0.2.0; the log format is compatible with v0.x, nothing
else is. See Migrating at the bottom of the next section.

Docs: the README is a third of its size and opens with how to use it

Install with go install github.com/wouterbeets/self@main, put one line in the
agent instructions or one SessionStart hook that runs self, work as usual.
Both routes were run against Claude Code in print mode: with the hook alone and
no tools, the agent reports the instance's event count; with neither, it does
not know it. @main rather than @latest because the last tag, v0.2.0,
predates self brief and the current CLI.

Gone: AGENTS.md, a long card that restated statically what the brief tells
the agent from the log; KERNEL_AUDIT.md, the record of one refactor;
instance-sync.plan.md, instance-sync.prompt.md and examples/instance-sync,
one personal two-machine deployment with its build output and patched copies
committed. The README keeps how to use it, how it works, and the limits.

Changed: every rung of the CLI unfolds to the one below it

self run and self view already printed the capability index when called
bare. Nothing else did. self view peers --nonsense ended at view "peers" exited: exit status 1 — the reader knows the view exists, knows it rejected
something, and has no path from there to what it wanted. An unknown name sent
the reader to self brief rather than printing what it would have found there.
An unknown verb listed the verbs but not their usage.

Now: an unknown name prints the index of names that do exist. An unknown verb
prints the usage block. A capability that ran and refused its arguments prints
its declaration — the whole thing if the script wrote nothing to stderr, and
otherwise only the pointer to self brief <name>, since a capability that
documents itself has already answered and does not need a second answer stapled
to it. peer prints its own verb table and exits 2; it gets one line, not six
hundred characters of rationale it did not ask for.

All of it is stderr and the exit code still says failure. Stdout is the wire —
a command's stdout is event JSONL the kernel appends, a view's is the page a
pipeline reads — so a diagnostic printed there would be parsed as events. To
read what a script said, runCommand takes an optional diagnostic writer and
runViewDiag takes one outright; every other caller still gets os.Stderr and
never has to know.

Changed: a declaration carries a terse summary, and the brief prints only that

A declaration was one prose field, and the protocol told its author to fill it:
usage, argument order, then the consequence of skipping the capability. That is
the right instruction for the mind choosing among tools, and it was the only
instruction, so every author followed it and the brief grew to hold all of it.
On a thirty-seven-capability instance that came to 19.7 kB — a surface read in
full by every waking, of which a waking typically uses one entry.

Nothing there was indiscipline. One field was serving two readers with opposite
budgets: choosing needs the rationale and happens once, orienting needs only
enough to tell two names apart and happens on every pass.

So decl gains summary, one terse line, and the brief prints that and nothing
else. The kernel clips it at 110 characters rather than asking for brevity,
because the mind writing a declaration sees its own line and never the aggregate
it lands in; a bound with no feedback signal is one nobody can tell they crossed.
description is unchanged and now has a reader that wants all of it: self brief <name> prints one declaration whole, with a view's consumes and its install
and refusal history. Declarations older than the field still map — the opening
sentence of the description stands in, clipped — so nothing already in a log
falls off the surface.

The drill-down is announced where the need for it appears: the brief says its
lines are clipped, directly above the first clipped line, and only on a surface
that actually clipped something. self brief <TAB> completes declaration names,
bare and deduplicated, marking a name held by both a command and a view. It is
also in self --help, in the CLI block, and in the brief's ## where footer.

On this instance the brief went 19.7 kB → 4.5 kB and bare self 21.0 kB → 5.7 kB.

Changed: the built-in log view answers "lately" and takes --all

self view log replayed every event and rejected every argument. It is the read
a cold mind reaches for first, the one the brief recommends, and the only read in
the kernel whose cost grows without bound — on this instance, 1.0 MB. It now
prints the last ten events, and --all lifts the bound to exactly the old
output, byte for byte.

Elision is announced on a leading # line, with the true event count and the
flag that shows the rest. A bounded read that did not say so would leave a mind
confidently wrong about what the instance has done, which is worse than the
context it saves; the # keeps the remainder the same tab-separated stream, so
anything piping it is unaffected.

Changed: self run answers with what exists

Bare self run now prints the commands this log holds — descriptions and
pending marks included, exit zero — instead of a bare usage error, and a name
the log does not know is answered with the same list instead of an error that
sends the reader to self brief for it. A view by the missing name still falls
through, so materialize offers self view <name>. self view has behaved this
way since it landed; the two verbs now answer the same question the same way.

Changed: self loop wakes a body instead of polling a fixed point

Driving the loop on an empty instance showed it settling in two passes every
time, and not because the model was timid: four forces in the kernel all pointed
at silence. The default ask ended "act only if something warrants durable
action. Silence is valid" — the fixed point stated as an instruction. --ask
was shown on pass one only, so a nudge evaporated after the turn it caused. The
growth layer said "author and test each script, then print", telling the mind to
close every declaration in the same breath and leave nothing pending — and
pending work is the only thing the kernel treats as unfinished. And nothing
changed between passes except the mind's own writes.

Each pass is now a waking. The kernel writes a line of facts it alone knows
— waking number, wakings left, what woke the body, whether the last waking was
quiet — and then splices a prompt:loop layer from PROTOCOL.md that invites the
mind to leave the next waking something: a declaration it has not built, a view
half-formed, a note. The ask stands on every waking. The growth layer now says a
declaration without a script is an intention carried forward, not a failure.

--settle N (default 2, SELF_LOOP_SETTLE) is how many quiet wakings in a row
rest the body; the last of them is asked plainly whether there is anything else.
A refused script no longer ends the loop: hear reports it as a typed error the
loop recognises, the refusal lands as script.rejected, and its reason rides the
next waking. Reaching --max-passes on a quiet waking is a rest, not a failure.

Killing self loop now kills the waking. Each mind runs in its own process
group and the whole group is signalled on timeout or on SIGINT/SIGTERM to the
loop; before, an interrupted loop left the mind running as an orphan, still
writing to the body through its own self run calls with its answer going to a
closed pipe, and a grandchild holding stdout could keep the timeout from ever
returning.

install now refuses a script whose first bytes are not #!, with a reason,
instead of signing it and letting the first self run fail with "exec format
error".

examples/mind-claude is claude -p as a mind with a live trace: it asks for
the stream-json trace, renders text, tool calls and tool results to stderr as
they happen — which the loop already routes to the terminal — and hands only the
final answer to stdout, so the wire is unchanged and a waking is watchable.
examples/mind-opencode is the same seam for opencode run, which takes its
prompt as an argument: the JSON event stream is rendered live, and only
event-shaped lines from the final step reach the wire. examples/mind-grok is
the grok CLI in the same seam, decoding its byte-array tool results for the
trace. All three wrappers forward only event-shaped lines, so hear no longer
echoes the mind's prose a second time as pass-through.

Changed: the repository is only the kernel and its demo

experiments/ (two finished measurement harnesses, results preserved in git
history), loop.sh (the pre-fixed-point compatibility wrapper) and
lessons/faces are gone. What remains is what demo.sh and the quick start
need: the kernel, the two sidecars, examples/mind-stub, and three small
accounts (journal, chat, memory). The README is rewritten to say plainly
what the prompt already says in one sentence: you are this self, for a bit.
The kernel history it used to narrate lives here, in the changelog.

New: shell completion, and the instance completes its own domain

source <(self completion zsh)     # bash and fish too

The shim is dumb and stable: every candidate comes from self __complete,
which replays the log — verbs, then installed capability names for run and
view (pending ones annotated, so the tab key doubles as a status surface),
selectors for give. Grown capabilities complete without reinstalling the
shim.

Argument positions are domain state the kernel cannot know, so it delegates:
self view context <TAB> replays an installed view named complete.context —
declared consumes on stdin, the typed words as argv, its stdout lines offered
as candidates, under a deadline and with stderr dis...

Read more

v0.1.0 — the strange loop, batteries included

Choose a tag to compare

@Wouterbeets Wouterbeets released this 07 Jul 20:03

First tagged release. One binary, no dependencies: an append-only event log as the only state, capabilities grown by the coding agent you already have (claude -p, opencode), every script installed under a locally-signed receipt, and the whole instance rebuildable offline from events.jsonl + .secret with self rehydrate.

Quick start for agent workflows: put self on PATH, paste the card in AGENTS.md into your project's agent instructions, and your sessions gain memory that outlives them.

This week's kernel: orientation briefs instead of log dumps, nested capabilities (finances/bills), self revise, honest adopt receipts, a site nav, and a shrinking main.go.