Skip to content

Releases

Superself 0.13.1: a refusal from the wrong server is one sentence

Choose a tag to compare

@tonite31 tonite31 released this 02 Sep 04:47
770488c

Superself 0.13.1 is one bug fix. In 0.13.0, a --cloud command pointed at a host that does not serve the workspace API printed error: followed by up to 2 KB of that host's own web page — an HTML 404, a proxy banner, a CDN challenge — and never said which host, which path or which status it came from. You could not tell "the server is not there" from "I am signed out" from "the API changed", and there was nothing in the output to act on. You now get one sentence naming the server, the status, the media type, the size of the body and the path, and the body itself is never quoted.

Nothing else changed. No verb, no flag, no file format. Every command below was run against the CLI built from this branch, and every text block is pasted from that run.

A non-JSON answer is never quoted, and the refusal names the host

Against a stub HTTP server on loopback answering 404 text/html with a page:

$ SUPERSELF_DEV=1 SUPERSELF_API_BASE=http://127.0.0.1:43161 self login --no-open --timeout 1
error: the server at 127.0.0.1:43161 answered 404 with text/html (142 bytes) at /api/device/start, which is not a workspace API response
    --api-base <url> or SUPERSELF_API_BASE chooses the server
(exit 1)

Against one answering 502 text/plain, the same sentence about a different answer — and the banner it sent is not echoed either:

$ SUPERSELF_DEV=1 SUPERSELF_API_BASE=http://127.0.0.1:42515 self login --no-open --timeout 1
error: the server at 127.0.0.1:42515 answered 502 with text/plain (11 bytes) at /api/device/start, which is not a workspace API response
    --api-base <url> or SUPERSELF_API_BASE chooses the server
(exit 1)

(SUPERSELF_DEV=1 is what allows a loopback http base at all; a workspace base is https only. It is on these lines because the stub is a plain HTTP server, and for no other reason.)

Under --json the same answer is fields rather than prose, and the body is absent from those too:

$ SUPERSELF_DEV=1 SUPERSELF_API_BASE=http://127.0.0.1:43161 self login --no-open --timeout 1 --json
{"error":{"code":"device_login_failed","message":"the server at 127.0.0.1:43161 answered 404 with text/html (142 bytes) at /api/device/start, which is not a workspace API response","status":404,"content_type":"text/html","bytes":142,"host":"127.0.0.1:43161","path":"/api/device/start","hint":"--api-base <url> or SUPERSELF_API_BASE chooses the server"}}
(exit 1)

A JSON error body is untouched. When the answer is this API's envelope, the message is still the server's own, exactly as before — against a stub answering 400 {"code":"device_flow_disabled","message":"device login is turned off for this workspace"}:

$ SUPERSELF_DEV=1 SUPERSELF_API_BASE=http://127.0.0.1:46263 self login --no-open --timeout 1
error: device login is turned off for this workspace
(exit 1)

That is the whole of the change a person sees. The runs above are self login's device start; the sentence itself is written in one function that every rail refusal path reads — device poll, refresh, the terminal 401 and every ordinary rail call included — so the same server is described the same way whichever command reached it.

Bounds and refusals

  • The content type is rendered as the media type alone. text/html; charset=utf-8 reads as text/html: the parameters are dropped, the value is sanitized like any other server string, and it is capped short, because a header of any length must not become the sentence.

  • The byte count is of the body actually received, under the response cap the rail already enforces — not a number the server claimed in a header.

  • The host is the URL's host and the path is its pathname, never the whole URL. A base can carry a query string, and a query string can carry a token; "where did the request go" is answered without one:

    $ SUPERSELF_DEV=1 SUPERSELF_API_BASE="http://127.0.0.1:43161/?token=not-a-real-token" self login --no-open --timeout 1
    error: the server at 127.0.0.1:43161 answered 404 with text/html (142 bytes) at /, which is not a workspace API response
        --api-base <url> or SUPERSELF_API_BASE chooses the server
    (exit 1)
    
  • JSON that is not an object is described rather than read. A body of "oops" parses, but nothing can be read off it; in 0.13.0 reading body.details from it was a TypeError rather than a refusal. It is now the same sentence, and the answer is what it is — application/json, 6 bytes:

    $ SUPERSELF_DEV=1 SUPERSELF_API_BASE=http://127.0.0.1:43309 self login --no-open --timeout 1
    error: the server at 127.0.0.1:43309 answered 400 with application/json (6 bytes) at /api/device/start, which is not a workspace API response
        --api-base <url> or SUPERSELF_API_BASE chooses the server
    (exit 1)
    

    An empty body on a non-2xx status is described the same way, and in 0.13.0 it was the same TypeError:

    $ SUPERSELF_DEV=1 SUPERSELF_API_BASE=http://127.0.0.1:43391 self login --no-open --timeout 1
    error: the server at 127.0.0.1:43391 answered 404 with an empty body (0 bytes) at /api/device/start, which is not a workspace API response
        --api-base <url> or SUPERSELF_API_BASE chooses the server
    (exit 1)
    
  • The test harness cannot reach a live host. This defect was found because a documentation run went to the default base. Every CLI the suite spawns is now pinned to a closed loopback — https://127.0.0.1:9 — unless the case sets a base of its own, so a test that names no base can no longer reach anything real.

Mixed versions

No store format changed in this release: no new file, no new field, no migration. A 0.13.0 CLI and a 0.13.1 CLI read the same store identically, and the only difference between them is the refusal text on an answer that is not this API's. Read off a real pair rather than assumed — the v0.13.0 tree was checked out into its own worktree, installed and built, and pointed at a git-backed store this release's verbs wrote:

$ self status     # 0.13.1
demo — goal: the refusal from a wrong server is one sentence
work: 0 active, 0 blocked, 1 next, 0 done
objectives: 0 open, 0 of 0 milestones reached, 0 of 0 exit criteria covered
waiting on you: 0
unshipped: nothing waiting to ship
health: ok
(exit 0)

The 0.13.0 CLI printed those same bytes, exit code included. Each read verb was run on both CLIs and the answers compared byte for byte with their exit codes — nine of nine identical:

IDENTICAL  self status
IDENTICAL  self context
IDENTICAL  self work
IDENTICAL  self state
IDENTICAL  self log
IDENTICAL  self artifact list
IDENTICAL  self objective
IDENTICAL  self milestone
IDENTICAL  self lang

Nothing refused on either side, and there is no row to write.

Two packages from one tag

apps/fold did not change in this release, and its version is raised to 0.1.1 anyway: one tag publishes both packages, and the gate refuses a version the registry already holds — @superself/fold@0.1.0 went out with v0.13.0. The CLI pins that version exactly, "@superself/fold": "0.1.1", in the same commit that raises it.

Upgrade after publication

After the v0.13.1 release workflow finishes, install the published package:

npm install -g superself@0.13.1

Superself 0.13.0: a store can keep its records on a server, and the calculation that reads them is a package

Choose a tag to compare

@tonite31 tonite31 released this 02 Sep 04:48
66e2cad

Superself 0.13.0 gives a workspace store a second place to keep its records. Until now a store was a git repository this machine commits and pushes, and that was the only shape there was. A store can now keep its records on a workspace server this machine is signed in to: appends land in a queue the machine owns, the server's copy arrives back as its own file, and one read makes them one log. One store is one or the other for its whole life, and self init is where that is decided — with --git, with --cloud, or by asking. Underneath both, the fold — the calculation that turns an event log into the state a reader sees — is now @superself/fold, a published package, so a server rendering that state and this CLI answer from one calculation rather than from two implementations of it.

Nothing about a git-backed store changes. Every command below was run against the CLI built from this branch, and every text block is pasted from that run.

The fold is a package: @superself/fold

The state you read is never stored. It is calculated from the event log, every time — and until now that calculation lived inside the CLI. A Workspace API server rendering the same state from the same logs would have had to reimplement it, and two implementations of one fold are two answers to "what does this project owe".

The fold is now its own package. It takes an event array and returns a project's state, and it reaches for nothing else: no clock, no session, no git, no terminal. The four machine-local inputs the fold used to reach for itself are arguments now, in a second layer:

foldEvents(events, project)             // what the log alone decides
applyLocalOverlay(model, events, local) // what one machine adds

For a reader of a store, that means the log is now readable by something that is not the CLI. Eighteen lines against the package, pointed at a store's own log.jsonl:

$ cat read-the-fold.mjs
// A reader that is not the CLI: no clock, no session, no git — just the log.
import { readFileSync } from "node:fs";
import { FOLD_VERSION, foldEvents } from "@superself/fold";

const events = readFileSync(process.argv[2], "utf8")
    .trim().split("\n").map((line) => JSON.parse(line));

const model = foldEvents(events, { slug: "demo" });

console.log(`fold version ${FOLD_VERSION}, ${events.length} events`);
for (const unit of model.works)
{
    console.log(`${unit.id}  ${unit.status}  ${unit.outcome}  [${unit.criteria?.filter((c) => c.covered).length ?? 0} of ${unit.criteria?.length ?? 0} criteria covered]`);
}
for (const decision of model.decisions)
{
    console.log(`decision: ${decision.text ?? decision.outcome}`);
}

$ node read-the-fold.mjs .../projects/demo/log.jsonl
fold version 1, 10 events
w-q42pa  active  the release notes are read off a real run  [1 of 2 criteria covered]
decision: the fold ships as @superself/fold so a server answers as the CLI does

and the CLI, over the same log, saying the same thing about the same unit:

$ self work
w-q42pa  active  the release notes are read off a real run  [toward o-3e2mm]  [1 of 2 criteria covered]  — 1 report(s), see `self work show w-q42pa --project 'demo'`
1 open work unit

FOLD_VERSION is the fold's own number, and it is what tells two readers their answers were produced by the same code. It is bumped when the fold's output changes for an unchanged log — a new reducer, a changed lifecycle rule, a new derived field. A refactor that moves the same calculation does not bump it, which is why this release, whose whole fold change was a move, ships FOLD_VERSION 1.

This changed nothing a reader sees, and that is the claim to check rather than take. It was checked: thirteen read commands were run against one git-backed store by the 0.13.0 CLI and by the 0.12.0 CLI, and every one of the twenty-six answers was byte-identical, exit code included. The list and the method are in the mixed-version section below.

A workspace store keeps its records in git or on a server

A store's records live in a git repository this machine commits, or on a workspace server this machine is signed in to. .superself/workspace.json is what says which: present means server-backed. Existence is the whole test, on purpose — a git-backed store has never written that file, so no store that exists today can be read as the new kind by accident.

A server-backed store keeps two files per project, both append-only:

projects/<slug>/log.jsonl      the server's copy, arrival order
projects/<slug>/pending.jsonl  this machine's queue of unsent appends

log.jsonl is written only by what comes back from the server. pending.jsonl records every change to itself as a line added — an append, a mark that one was sent, a mark that one may not be retried — so nothing is ever rewritten and no crash between two writes can lose a record.

That makes every event the property of exactly one of the two files: the queue owns it until the server has it, the copy owns it afterwards. There is no window in which a machine has written half a record. A store connected to a workspace and never yet synced holds a queue and no server copy at all — recording into one and then listing the store directory shows exactly that:

$ self goal add "the workspace holds what every machine recorded"
entity.confirmed recorded [01m1fztsvqdd3v6n35ft5yrwk2]
  the workspace holds what every machine recorded — verify; wrong? self undo 01m1fztsvqdd3v6n35ft5yrwk2

$ self work add "a second machine reads what this one queued"
entity.confirmed recorded [01m1fztszx622qn2pxwgpz0tb4]
  w-zdy4b a second machine reads what this one queued — verify; wrong? self undo 01m1fztszx622qn2pxwgpz0tb4
w-zdy4b
no objective yet? `self objective add "<outcome>" --proposed`, then `self objective confirm <id>`

$ ls .superself/projects/demo
pending.jsonl
state.md
work

No log.jsonl exists, because nothing has come back yet — and the store reads anyway, off the queue alone:

$ self work
w-zdy4b  next  a second machine reads what this one queued
1 open work unit

$ self status
demo — goal: the workspace holds what every machine recorded
work: 0 active, 0 blocked, 1 next, 0 done
objectives: 0 open, 0 of 0 milestones reached, 0 of 0 exit criteria covered
waiting on you: 0
unshipped: nothing waiting to ship
health: ok

No commits are added. The nine callers that say "the store has changed, record that" are unchanged and say what they always said; a server-backed store has already recorded it by queueing the append, so the commit step returns having done nothing. Counted across the two commands above, the store's git history stayed where it was:

before: commits=2  log.jsonl=absent  pending.jsonl=absent
after:  commits=2  log.jsonl=absent  pending.jsonl=2

readEvents is where the two files become one log, and it is the one place they do: the server's copy first, in the order the server put it in, then this machine's unsent tail. A record the workspace has already agreed on stands ahead of one only this machine has made, and an append made offline lands after everything that arrived while it was offline. An append with a sent or a blocked mark against it is out of the read, and an event held by both files is counted once.

The verbs that stop meaning anything

Five verbs — store size, store compact, sync, remote add and clone — are about a git history a server-backed store does not have, and each is refused by name — naming the verb the caller ran and what it is for, rather than only what it is not:

$ self store size
error: `self store size` measures the git history a git-backed store keeps; this workspace store is server-backed and has none

$ self store compact
error: `self store compact` packs the git history a git-backed store keeps; this workspace store is server-backed and has none

$ self sync
error: `self sync` pushes a git-backed store to its git remote; this workspace store is server-backed and has none

$ self remote add https://example.com/store.git
error: `self remote add` points a git-backed store at a git remote; this workspace store is server-backed and has none

$ self clone https://example.com/store.git
error: `self clone` copies a git-backed store from a git remote; this workspace store is server-backed and has none

In a git-backed store beside it, store size answers as it always did:

$ self store size
store 208.4 KB — working tree 154.1 KB, .git 54.3 KB
artifacts: 1 recorded, 1 distinct contents, 28 B in 1 stored file
largest projects: demo 28 B
largest files: artifacts/demo/a-n2x56-note.md 28 B
orphan bytes: none — every stored file is named by a record
git objects: 96 loose (384.0 KB), pack 0 B — run `self store compact`
compacting packs the history; it never rewrites it, so bytes already in the history stay there

self alias, lang, theme, timezone and tokens write config.json, which is machine-local. In a server-backed store they take effect on this machine and stay there.

A server-backed store sends what it queued and receives what the workspace agreed

The queue above is emptied by a transport with a shape worth knowing before you use it:

catch up  →  the command  →  send

The catch-up cannot refuse anything. Being offline is an ordinary state rather than a failure, and the local files are a complete log of what this machine knows — so every path through the catch-up ends in the command running against what this machine holds. Pointed at a workspace that does not answer — here, an address nothing is listening on — a read command says nothing about it and answers from the queue:

$ SUPERSELF_API_BASE=https://127.0.0.1:9 SUPERSELF_SYNC=inline self work
w-zdy4b  next  a second machine reads what this one queued
1 open work unit
(exit 0)

The send cannot say anything. By default it is a process of its own, outliving the command that started it, so nobody waits ...

Read more

Superself 0.12.0: a work unit declares what done means

Choose a tag to compare

@tonite31 tonite31 released this 28 Aug 10:02
8b5dbb7

Superself 0.12.0 lets a unit of work carry its own completion conditions. A unit used to state one outcome and one judgment — done, evidenced by a report — and everything between them lived in a session's head or in prose nobody could address. This release gives that middle a grammar: ordered criteria addressed c1..cN, each with a recorded verification method, each covered one at a time, one of them able to wait on something without stopping the unit around it, and one of them able to belong to a person rather than to the session that wrote it down. work done is refused until every one is covered. Around that: a milestone page that reads its linked units instead of naming their ids, an artifact that can be a URL, and self log finally saying who wrote each event.

A unit declares its criteria, and is not done until each is covered

--criteria is repeatable and ordered; --verify says how one of them is checked, keyed by its position. The verification text is recorded prose — nothing here executes it, ever.

self work add "<outcome>" [--criteria "<text>" …] [--verify "cN <how it is checked>"] [--owner "cN person"]
self work criteria add <id> "<text>" [--verify "<how it is checked>"] [--owner person]
self work cover  <id> --criterion cN --why "<how it is covered>" [--evidence <commit>] [--work <id>]
self work block  <id> [--criterion cN] --on decision|dependency|external [--why w]
self work unblock <id> [--criterion cN]
$ self work add "the release notes state what 0.12.0 changed" --criteria "every command line in the notes is run against the built CLI" --criteria "the mixed-version section is read off the fold" --criteria "a maintainer approves the wording" --verify "c1 the pasted output is a real run" --verify "c2 the older CLI is checked out and folded" --owner "c3 person"
entity.confirmed recorded [01m13w9jyyxy0sx6ss10pnemhp]
  w-f4v81 the release notes state what 0.12.0 changed — verify; wrong? self undo 01m13w9jyyxy0sx6ss10pnemhp
w-f4v81

Covering one is a claim with a reason, and blocking one names what it waits on. Each is its own event, so self undo takes back exactly the one it names:

$ self work cover w-f4v81 --criterion c1 --why "every line in the notes was pasted from a run"
entity.covered recorded [01m13w9q7q2rfkc3hbtdbf6zyh]
  w-f4v81 every command line in the notes is run against the built CLI every line… — verify; wrong? self undo 01m13w9q7q2rfkc3hbtdbf6zyh

$ self work block w-f4v81 --criterion c2 --on dependency --why "the 0.11.0 tree has to be built first"
entity.criterion-blocked recorded [01m13w9qc7kcp1nsmd2k6vd00n]
  w-f4v81 the mixed-version section is read off the fold — verify; wrong? self undo 01m13w9qc7kcp1nsmd2k6vd00n

The unit's page carries a - Criteria: block, each row naming the criterion's own words and then how it is checked. A blocked criterion never changes the unit's own status — the unit below is next, not blocked, which is the whole reason a criterion's block is its own event type rather than a field on entity.blocked:

$ self work show w-f4v81
# w-f4v81 — the release notes state what 0.12.0 changed

- Status: next
- Criteria: 1 of 3 covered (1 blocked)
  - c1 covered — every line in the notes was pasted from a run (agent 2026-08-28)
  - c2 blocked on dependency — the 0.11.0 tree has to be built first
  - c3 open — a maintainer approves the wording (person)
- Not done yet: w-f4v81 declares 3 criteria and 2 are not covered:
    c2  blocked on dependency — the 0.11.0 tree has to be built first
    c3  open — a maintainer approves the wording (person)
  cover each with `self work cover w-f4v81 --criterion c3 --why "<how>"` — a covered criterion's block ends with it — or retire the unit if the outcome was given up

work done is refused with that same list, so the refusal is the page's own sentence rather than a second wording of it. The cover each with … line it hands back prefers a criterion that is neither blocked nor somebody else's, so the pasted command is usually one this session can run outright; when every remaining criterion is one or the other, as here, it names one rather than naming none:

$ self work done w-f4v81 --report "the notes were written and every command run"
error: w-f4v81 declares 3 criteria and 2 are not covered:
    c2  blocked on dependency — the 0.11.0 tree has to be built first
    c3  open — a maintainer approves the wording (person)
  cover each with `self work cover w-f4v81 --criterion c3 --why "<how>"` — a covered criterion's block ends with it — or retire the unit if the outcome was given up

k of n is one sentence, composed once and read by the four surfaces that mention a unit — its own page above, the listing, self context, and milestone show's linked work — so none of them can describe one unit differently. The listing:

$ self work
w-f4v81  next  the release notes state what 0.12.0 changed  [1 of 3 criteria covered]
1 open work unit

and the work-in-progress row of self context, which carries the blocked criterion and the owned one by name:

$ self context
## Work in progress

- w-f4v81 the release notes state what 0.12.0 changed — 1 of 3 criteria covered · c2 blocked on dependency · c3 (person)

A criterion a person owns waits on them

--owner "cN person" on work add and work propose, or --owner person on work criteria add, says the criterion is somebody else's task rather than this session's. An open, person-owned criterion on an open unit lists under Waiting on you with the command that closes it:

$ self context
## Waiting on you

- w-f4v81 c3 a maintainer approves the wording — yours (cover with `self work cover w-f4v81 --criterion c3 --why "<how>"`)

One row per uncovered person-owned criterion, in cN order — two on one unit are two things to do, and a merged row would carry two --criterion values in one command. A proposal's owned criteria do not list: the review already has a row of its own, and asking somebody to cover a criterion of a plan they have not accepted asks for the wrong act. Ownership marks the criterion (person) wherever criteria render, and changes nothing else — not the unit's status, not its blocked count, not k of n.

The one-sentence distinction, carried on self help work and in the agent instruction block: by is who wrote the record; --owner is whose task the criterion is — a session records a criterion a person will do, and neither implies the other.

milestone show reads its linked units

The page printed the exit criteria, the coverage claims and one line naming the linked ids, so finding out where any of them stood meant running self work show once per id. A ## Linked work section now answers that on the page, between ## Exit criteria and ## Coverage:

$ self milestone show m-yn1ac
# m-yn1ac — the release PR is ready to merge

- Objective: o-4d5z7 0.12.0 ships with notes a reader can act on
- State: on-track — 0 of 2 exit criteria covered across 2 work unit(s)
- Revision: 1 (objective revision 1)
- Critical path: no
- Work: w-f4v81, w-bf87w

## Exit criteria

- c1 — the notes are written _(open)_
- c2 — the gates are green _(open)_

## Linked work

- **w-f4v81** the release notes state what 0.12.0 changed — active, 1 of 3 criteria covered · c2 blocked on dependency · c3 (person)
  - report 2026-08-28: the criteria render was checked on all four surfaces
- **w-bf87w** the golden fixture carries the new version — next, 0 of 1 criteria covered

Each entry is the unit's standing, then its k of n sentence when the unit declares criteria, then who holds it when a session does, then its latest report's first line beneath. Units appear in the milestone's own record order. The section is on self milestone show alone: it is read at command time, and the holder clause is this machine's judgment of another machine's session, so objective show and the folded objective/<id>.md that syncs to every clone are byte-identical to before.

An artifact can be a URL, with a kind, attached to a unit or a milestone

self artifact add <path|url> [--kind brief|pr|resource|doc] [--for <work-id|milestone-id>]

A URL is recorded as a link. No bytes are stored and nothing is fetched — not at add, not at open, not by the health check:

$ self artifact add https://github.com/fxylabs/superself/pull/412 --kind pr --for w-f4v81
artifact.linked recorded [01m13wadhae6p0z4121bfr5z2e]
  a-3afgk https://github.com/fxylabs/superself/pull/412 — verify; wrong? self undo 01m13wadhae6p0z4121bfr5z2e
a-3afgk

A path keeps today's behaviour and takes the same two flags. Both shapes list, search and filter alike, with the kind in the row:

$ self artifact list
a-3afgk  2026-08-28  demo  w-f4v81  https://github.com/fxylabs/superself/pull/412 [pr]
a-h07g4  2026-08-28  demo  -  https://github.com/fxylabs/superself/issues/415 [brief]
a-0mca6  2026-08-28  demo  w-f4v81  draft.md [doc]
a-8fd6r  2026-08-28  demo  w-f4v81  https://example.com/res [resource]
a-bjfms  2026-08-28  demo  w-f4v81  https://example.com/brief [brief]
5 artifacts

The work column is the unit a --for attached it to, and - where nothing did. a-0mca6 is a path: bytes copied into the store, listed beside the links with no distinction a reader has to make.

--for attaches either shape to a work unit or a milestone, and both pages then list what is attached, ordered brief, pr, resource, doc:

$ self work show w-f4v81
## Attached artifacts

- a-bjfms  brief  https://example.com/brief
- a-3afgk  pr  https://github.com/fxylabs/superself/pull/412
- a-8fd6r  resource  https://example.com/res
- a-0mca6  doc  draft.md
$ self milestone show m-yn1ac
## Attached artifacts

- a-h07g4  brief  https://github.com/fxylabs/superself/issues/415

artifact open on a link prints the address and launches nothing — a log travels between machine...

Read more

Superself 0.11.0: consent given in chat reaches the store

Choose a tag to compare

@tonite31 tonite31 released this 28 Aug 06:51
21fc698

Superself 0.11.0 lets an agent session record the decisions a person already made in the conversation it is having with them. Seven verbs used to refuse and print "a person runs this in their own terminal"; the refusal did not withhold consent, it withheld the record of consent already given, and the command came back for the person to paste — which is where a long --supersedes … --why "…" line drifts from what was agreed, or never gets run at all. The rule that replaces it is one sentence: a verb whose record self undo can take back needs no person at a keyboard; it records who wrote it instead. One verb keeps its gate, because nothing takes back a deletion.

An undoable verb runs from the session that made the decision

work add, work confirm, decide --supersedes, the retracting and retiring family, report confirm, and runbook approve now record from an agent session. Each one writes an event self undo reverses, which is the whole justification: the gate was written before self undo existed, and it bought nothing a person cannot buy with one undo.

self work add "<required outcome>" [--supersedes <work-id> --why w]
self decide retract <event-id> --why "<reason>"
self work retire <id> --why w
$ self work add "the render gate holds"
entity.confirmed recorded [01m13hffrd1xwgebkk1kqg44zj]
  w-372yh the render gate holds — verify; wrong? self undo 01m13hffrd1xwgebkk1kqg44zj
w-372yh

What replaces the gate is attribution. Every event these verbs write carries one new payload field:

"by": { "kind": "person" | "agent", "session": "a1b2c3d4", "name": "rayim" }

kind reads the same marker that already answered "is anybody there". session is the session token, written for an agent only — a person's call has no session to name. name is what runbook approve --by already recorded as a bare string, folded into the same field, so who approved and what kind of process recorded it are one statement.

<verb> show <id> --history is where a reader sees it:

$ self state show e-3ryjc --history
e-3ryjc  live  3 events
2026-08-28T06:39:17.476Z  entity.proposed  [01m13hg6z49sgvzesvp4wqavxt]  every render is checked against the golden · by agent (session a1b2c3d4)
2026-08-28T06:39:17.605Z  entity.placed  [01m13hg735m7fge8pcvmkty29p]   · by agent (session a1b2c3d4)
2026-08-28T06:39:17.730Z  entity.confirmed  [01m13hg77266vpd58heb3qmzx0]   · by agent (session a1b2c3d4)

self log does not carry the column. It is the workspace timeline and a machine contract, and a column every reader of it has to skip is one it does not owe; the writer is read from the record's own history page instead. An event carrying no by — every record written before this release — prints its line and says nothing about a writer, rather than being read as a person.

The disclosure survived the gate. Every retiring verb still prints what it destroys — the record's own words, its age, what still points at it, the reason the call gives — before it writes. Only the challenge prompt at the end is gone:

$ self work add "the render gate holds for piped output too" --supersedes w-3vhw2 --why "the original outcome was too narrow"
this retires a confirmed work unit — `self undo` takes it back

  w-3vhw2  work  confirmed 2026-08-28T06:41:01.027Z  (just now)
  the render gate holds
  referenced by: nothing
  replaced by this new work: the render gate holds for piped output too

entity.confirmed recorded [01m13hkc7bqt77bzshbnp6h0xc]
  w-ck5gg the render gate holds for piped output too — verify; wrong? self undo 01m13hkc7bqt77bzshbnp6h0xc
entity.retired recorded [01m13hkc7aq45sdpb3ssrpazk7]
  w-ck5gg the render gate holds for piped output too — verify; wrong? self undo 01m13hkc7aq45sdpb3ssrpazk7
w-ck5gg

A person at a terminal reads the same disclosure and answers nothing: no prompt is printed and no answer is read. self apply still collects a reviewed set, still refuses a repeat, and still lands the whole set as one append.

work accept is now work confirm, and work propose means review

The verb is renamed to match decide confirm, state confirm and objective confirm.

self work propose "<plan>" [--supersedes <work-id> --why w]
self work confirm|decline <proposal-id> [--why w]
$ self work --help
usage: self work [--project <slug>] [--pretty|--plain]
       …
       self work propose "<plan>" [--supersedes <work-id> --why w] [--milestone m --value v]
       self work confirm|decline <proposal-id> [--why w]

accept stays as a hidden alias dispatching the identical handler — same event type, same payload shape, same by — so a script or a document written against it keeps working. It appears on no help page, because a page naming it would advertise the spelling this retires, and the unknown-verb refusal lists the new one:

$ self work bogus
error: unknown work subcommand "bogus" — use add|show|start|started|exited|block|unblock|done|retire|link|unlink|propose|revise|confirm|decline

After the rename, work propose means "I want this reviewed", not "I am an agent". Both forms are open to a session: propose a plan when it wants review, and work add it when it does not, exactly as decide "<text>" is to decide --proposed.

A proposal can be placed before it is confirmed

Placing a record that is still proposed used to be refused outright. A session that proposes a workspace-wide rule can now say where it belongs in the same breath, and the confirm lands it there.

self state place <id> --scope workspace
self state confirm <id>
$ self state place e-3ryjc --scope workspace
entity.placed recorded [01m13hg735m7fge8pcvmkty29p]
  e-3ryjc every render is checked against the golden — verify; wrong? self undo 01m13hg735m7fge8pcvmkty29p

$ self state confirm e-3ryjc
entity.confirmed recorded [01m13hg77266vpd58heb3qmzx0]
  every render is checked against the golden — verify; wrong? self undo 01m13hg77266vpd58heb3qmzx0

Until the confirm the proposal renders only in the waiting band, never as a placed workspace record, and a proposal placed past the retention cap still takes its seat at the confirm, which is where the cap is judged. --priority is allowed for the same reason: priority hides nothing.

A placement that would demote a proposal is still refused, because there is nothing to demote from:

$ self state place e-yzqe2 --exposure search --why "not for context"
error: e-yzqe2 is still proposed, so it renders nowhere to be demoted from — confirm it with `self state confirm e-yzqe2` and demote it then, or propose it at search instead

A design approval is recorded by a session, and taken back like anything else

report confirm binds an approval to the design artifact's hash. Its old refusal read "a person's ruling on a design report", which stops being a distinguishing fact the moment a session can record the same ruling.

self report <work-id> "<summary>" --design --implements <id> --artifact <path>
self report confirm <report-id>
$ self report confirm 01m13hhke7qpgp21vptmzy1mcw
report.confirmed recorded [01m13hhkphsa0ft8enf4c3330v]
  w-x1tkm design 01m13hhke7qpgp21vptmzy1mcw approved — verify; wrong? self undo 01m13hhkphsa0ft8enf4c3330v
01m13hhke7qpgp21vptmzy1mcw approved for w-x1tkm, bound to artifact 12e54cc862cd

report.confirmed leaves the list of types self undo refuses by name. With nothing built on it, taking it back leaves the design unapproved again, and the dispatch gate closes behind it:

$ self undo 01m13hhkphsa0ft8enf4c3330v
entity.annulled recorded [01m13hhkx89s4zkahxwz4fdfja]
w-x1tkm is open again — its report.confirmed was taken back

$ self work start w-x1tkm
error: w-x1tkm cannot be picked up yet — its design 01m13hhke7qpgp21vptmzy1mcw is waiting on an approval: `self report confirm 01m13hhke7qpgp21vptmzy1mcw`

What guards the approval instead of a gate is the dependent chain. Once a unit has started on it, the undo is refused with the list, so a dispatched unit is never left with a design nobody approved:

$ self undo 01m13hhm74xvjqxnkz6zrxb1jc
error: 01m13hhm74xvjqxnkz6zrxb1jc was built on — 1 event names it
    01m13hhmcx3dw8h8mzcaj6ypk1  entity.started   w-x1tkm
  take them back first, newest first:
    self undo 01m13hhmcx3dw8h8mzcaj6ypk1

runbook approve moves the same way. A held run is released from a session, and --by records a name beside the kind and the session — it authorizes nothing, and nothing here checks a name:

self runbook approve <key> [--by <person>]
$ self runbook approve E001 --by "rayim"
entity.unblocked recorded [01m13hk0j1trt47f43a6z1vppk]
  E001 — content loop — verify; wrong? self undo 01m13hk0j1trt47f43a6z1vppk
E001 is approved and moving again

artifact prune is the one verb that keeps its gate

artifact.pruned removes bytes from the working tree, and nothing takes back a deletion. That is the whole reason, and the refusal now states it in one line:

self artifact prune <id> --why "<reason>" [--project slug]
$ self artifact prune a-gj1v9 --why "superseded by the new guide"
error: removing stored bytes cannot be taken back by `self undo`, so a person types the id — this process has no terminal to type it at, and nothing was removed

  a-gj1v9  guide.md  registered  attached 2026-08-28
  the render guide
  stored at artifacts/demo/a-gj1v9-guide.md (8 bytes)
  those bytes are removed from the working tree
  the record itself is kept — what was attached stays auditable, and a done claim resting on it still holds
  history is never rewritten, so the copy this artifact left in .git stays there

  a person runs this in their own terminal:
    self artifact prune a-gj1v9 --why "…"

self help artifact is the one help page allowed to keep the gate's sentences, and it states why it is the only one. Every other self help <topic> and `s...

Read more

superself 0.10.0: reuse what repeats, and take back what was a mistake

Choose a tag to compare

@tonite31 tonite31 released this 27 Aug 15:18
edec159

Superself 0.10.0 lets a project record the things it does more than once — a procedure with stages, a command it keeps retyping, a document that is too long to be a rule — and reuse them from any session. It also gives a person a way to take a record back when it was simply wrong, instead of dressing the correction up as a replacement. Around those, the store learns to say how large it is and to give bytes back, and a report learns to say what differed from expectation.

Register a procedure the team repeats, and run it

A procedure is a list of stages a project works through, such as plan, draft, publish. Register it once and start a run of it whenever the work comes round again.

self runbook add "content loop" --stage plan --stage draft --stage publish
self runbook start <id|name> --instance E001
self runbook advance E001 --why "the assets are made"

self runbook lists what is registered here, self runbook show <id|name> prints a procedure with its stages and its runs, and self runbook revise <id> --stage … --why "<what changed>" proposes a new edition that a person lands with self state confirm. Stages can be read from a markdown list with --file <path> instead of one --stage per step.

self context gains one section, so a session that has just started reads the resume point without being told:

## Runbooks

- E001 · content loop v1 (the definition is on v2) · 2/3 draft · next: publish · `self runbook show e-8mwfx --project 'demo'`
- E003 · content loop v2 · 1/4 plan · next: draft · `self runbook show e-8mwfx --project 'demo'`

A run copies the stages it started under, so a later edition never changes what a run already in flight means. Where the run and the current edition differ, context says which edition the run is following and the run keeps going.

Registering a procedure schedules nothing and starts nothing. No verb calls another, none advances a stage on its own, and there is no timer.

Park a run on a person, stop it, resume it, and name its work

A run can wait for someone to look at it before it goes on.

self runbook hold E001 --why "the final cut needs a look"
self runbook approve E001 --by <person>
self runbook stop E001 --why "the story was dropped"
self runbook resume E001
self runbook link E001 --work <work-id>

A held run reaches the part of context that lists what is waiting for the person reading it:

## Waiting on you

- E001 E001 — content loop waits on your approval: the final cut needs a look (approve with `self runbook approve E001`)

The approval is a person's call at their own terminal, and the challenge is the run's key, so typing it back states which run is being approved. --by records who approved and gates nothing. Giving a run up goes through the same confirmation every other withdrawal goes through.

Register a command or recipe once and reuse it

Some know-how is neither a rule nor a procedure: the exact line that deploys, the flags that run one test file against the right environment, the recipe for a task that comes round every few weeks.

self skill add "<name>" --command "<line>" --purpose "<what it is for>" [--workspace]
self skill add "<name>" --file <path> --purpose "<what it is for>"
self skill show <id|name> [--project <slug>]
self skill drop <id|name> --why "<why it no longer helps>"

self context gains a section carrying each one's name, its one-line purpose, and the command that prints it. The command line itself never enters context, so a session discovers what exists and asks for the body when it needs it:

## Skills

- deploy — push the built image to staging · `self skill show e-xxxxx --project 'demo'`

--workspace registers one for every project. A project's own entry of the same name takes the row and the workspace one takes none, and the listing, the page, and the receipt all say that it is standing in front of a shared one.

A registered line is printed, never run. self skill run is a refusal by name and appears on no help page: the store syncs between machines, so a line it holds could be appended anywhere and would then execute everywhere.

Registering a name that already stands proposes a replacement rather than displacing what is there; self state confirm lands it, and the earlier version stays in the list the page prints.

Take back a record that was a mistake

A record that is a plain mistake — the wrong id, the wrong text, the wrong link — is now taken back rather than replaced with ceremony.

self undo [<event-id>] [--supersession] [--why <text>]

With no id it takes back the newest append, the one whose receipt was just printed. No reason is owed: "this was a mistake" is the whole statement, and the record of the undo already names the event it reversed.

$ self work link <w-id> --milestone <m-id>
entity.linked recorded [<event-id>]
  <w-id> stage 1 lands the render gate and its pilot — verify; wrong? self undo <event-id>

$ self undo <event-id> --why the checkpoint was the wrong one
entity.annulled recorded [<event-id>]
<w-id> no longer carries the link — the linked event was taken back

Every command that records something now prints the record it resolved and the exact self undo line for it, as the receipt above shows. --meant "<what you meant>" optionally records the caller's own restatement beside a write; it is printed and never judged.

One append is one undo. Where a single command wrote several events about the same record, they come back together; where it wrote unrelated ones, they come back one at a time. --supersession narrows the undo of a record that replaced another, so the record stands and only its claim to replace the older one is taken back.

Nothing is rewritten. Both halves stay in self log, the row is marked · undone, and a record whose creation was undone still answers to self work show <id> as Status: undone with the date, so an id followed out of a commit message is answered rather than reported unknown.

A work unit is proposed by an agent and recorded by a person

Creating a confirmed work unit is now a person's own command. self work add, self work add --supersedes, and self work accept are refused where no person is at the terminal, and each refusal hands back the exact line to run instead.

self work propose "<plan>" [--supersedes <work-id> --why w]

self work propose learns the correction spelling, so an agent can propose a replacement for an existing unit and the retirement lands with the acceptance, in one append under one confirmation.

A person at a terminal loses nothing: work add and work accept stay one command with no prompt to answer. The check is presence only — there is no challenge to type — and it reads the terminal alone, so piping a command's output changes nothing about who is running it.

Say what differed from expectation in a report

A report can now state the friction it met as a field rather than burying it in prose.

self report <work-id> "<summary>" --friction "<what differed>"

The flag is free text and can be given more than once. The sentences appear under the report they belong to in self work show and the folded work page, on the report block of self handoff, on the report's one-line summary in self log, and as one line in context when a project's recent reports carry none:

health: no friction on 1 of this project's 1 report in the last 30 days — self report … --friction "<what differed>"

It is optional. Reports already in a store are not retroactively incomplete, and self work done --report still records without it.

Group the recorded friction into proposals

self sweep reads the friction sentences recorded lately across the workspace's active projects, groups the ones that say the same thing, and proposes a change for each group that recurs.

self sweep [--since <window>] [--record]

Without --record it prints and records nothing. With --record each group becomes a proposal that reaches the person through the part of context that lists what is waiting for them. The window defaults to the last 30 days and is written as a whole number of days or weeks. There is no timer and no background process; an unattended run is an external scheduler calling self sweep --record.

A group needs at least three sentences before it is proposed. A proposal carries no count in its text and is built from the group's oldest sentence, so a group that keeps growing does not keep re-proposing itself under new wording; the events it was built from are recorded as its evidence. A sentence that says only that nothing differed is read and counted but never proposed, so an honest report is not punished.

Store a file with no report behind it, and point a rule at it

Guidance longer than one line had nowhere to live inside the store: a file was stored only as evidence attached to a work report.

self artifact add <path> [--entry <file>] [--why <text>]
self convention add "<text>" --artifact <id|path>
self state add "<text>" --artifact <id|path>

artifact add stores a file or a directory with no report behind it. It is not evidence: registering a file never satisfies self work done.

A record that points at one renders a pointer to self artifact open <id> after its text, in context, in the handoff packet, in the terminal render, in self search, and on self state show. The retention cap charges the pointer and never the document behind it, so the length of the guidance no longer competes with the rules for room in context. self status reports a live record whose file is missing or altered.

Given a path rather than an id, the file is stored first and pointed at second, so a failure between the two leaves a file nothing points at rather than a record naming bytes that were never stored.

Remove a stored file's bytes and keep the record that names them

self artif...
Read more

superself 0.9.0

Choose a tag to compare

@tonite31 tonite31 released this 25 Aug 04:39
53b7b6f

Superself 0.9.0: attach a directory as one artifact

Superself 0.9.0 lets a report attach a whole directory as one artifact — a bundle — instead of one --artifact flag per file. The record keeps the member list, and every read surface shows the bundle as one row with the one file a person is meant to open.

Attach a built site or a multi-file report in one flag

Run:

self report <work-id> "<summary>" --artifact dist [--entry <file>]

The directory's files are copied into the store under one artifact id, relative paths preserved. The event records a manifest of every member, so artifact list and artifact search answer from the record without opening stored files, and a search hit on any member resolves to the bundle's single row:

a-4k2pq  2026-08-25  my-project  w-xxxxx  dist/ (12 files)

The entry is the file a person opens

self artifact open <id> opens the bundle's entry: the member --entry names, else index.html, index.md, or README.md at the directory's root, else a minimal index the CLI generates and marks as its own. The HTML views link the same entry.

Bounds and refusals that keep the shared store sound

The store is a git repository cloned to every machine the workspace syncs to, so ingestion refuses what would break a clone or misname the record: symlinks and special files inside the tree, member names that collide once case and Unicode normalization are folded, paths that cannot round-trip through the event log, and more than 1,000 brought files or 100 MiB (package the directory into one file instead). Single-file attachment and every existing record are unchanged.

Upgrade after publication

After the v0.9.0 release workflow finishes, install the published package:

npm install -g superself@0.9.0

Evidence

  • PR #363 adds directory attachment as one artifact bundle with an entry and an event-carried manifest.
  • Case table is the reviewed design: 20 rules, 60 cells, one test per cell.
  • Release policy defines the version, tag, and publication path.

superself 0.8.0

Choose a tag to compare

@tonite31 tonite31 released this 24 Aug 13:46
d808ff9

Superself 0.8.0: carry complete work state between agents

Superself 0.8.0 gives a fresh agent the project rules, work state, reports, and recovery commands it needs to continue active work. It also lets a person revise an unstarted work proposal without replacing its id.

Hand active work to a fresh agent

Run:

self handoff <work-id> [--project <slug>]

The command emits one deterministic packet with the common Superself protocol, applicable conventions, bounded project context, complete work and report history, and recovery guidance for the current lifecycle and execution location.

The packet is read-only. The command accepts exact work ids, appends no event, creates no file, and does not start or switch an agent. Project-controlled text stays inside renderer-owned framing so report or convention content cannot become a packet section.

Revise a proposal before work starts

Run:

self work revise <id> "<revised plan>" --why "<what changed>"

The work id and every earlier plan version remain in the same unit. A revision invalidates the previous acceptance, so a person must accept the new plan before an agent can start it. The first self work start freezes the plan; later corrections use a successor work unit.

Upgrade after publication

After the v0.8.0 release workflow finishes, install the published package:

npm install -g superself@0.8.0

Release preparation checklist

  • Merge the accepted release PR with apps/cli/package.json at 0.8.0.
  • Confirm CI verify, contribution-policy, and dco are green on the release PR.
  • Stop before creating a tag, GitHub Release, or npm publication. Those actions require a separate publication instruction after this preparation is accepted.

Publication handoff after separate approval

These steps are not part of this release preparation. After a maintainer separately authorizes publication:

  1. Create annotated tag v0.8.0 on the release merge commit.
  2. Confirm the tag-triggered Publish workflow successfully publishes npm superself@0.8.0.
  3. Create the GitHub Release for v0.8.0 using the reviewed reader sections above.
  4. Confirm both npm superself@0.8.0 and the GitHub Release are visible before announcing availability.

Evidence

  • PR #357 adds one-id revision of standalone work proposals until first start.
  • PR #359 adds complete read-only work handoff packets.
  • Release policy defines the version, tag, and publication path.

superself 0.7.0

Choose a tag to compare

@tonite31 tonite31 released this 23 Aug 16:25
ce5f83b

Agent login and mini-app plugins

  • self login — device-flow login for agent credentials (scoped, refresh-rotated, 0600 file storage; self logout, self whoami).
  • self app install <key> / list / update / remove — signed mini-app plugins (wallet, email, landing) served by the rail and verified before a byte runs.
  • Plugin trust model C: the CLI pins two root public keys (fingerprints in the README); which release keys are trusted comes from a signed, expiring trust document fetched from the rail — a leaked signing key is revoked by publishing a new document, no CLI update needed. self app trust [--refresh] shows what this binary currently accepts.
  • --json machine output with one error envelope, exit codes 0/1/2/3 (3 = retryable), idempotent call keys on money-moving commands.

Behavior-preserving for every existing command: a 0.6.x workflow with no credential and no plugins behaves identically.

superself 0.6.0 — approvals, goals, and search reach installers

Choose a tag to compare

@tonite31 tonite31 released this 07 Aug 00:10

npm serves this release as superself@0.6.0:

npm install -g superself

New since 0.5.1

  • Destroying a record needs a person at a terminal, and undo takes it back (#223)
  • Goals join the record grammar — several stand, and replacing one is stated (#225)
  • Retention caps and the context budget are measured in context tokens (#226)
  • One placement verb moves any record — a record renders where its scope points (#236)
  • self search answers over the live records context does not show (#237)
  • Origin records — one record says where a project came from, and reads back both ways (#241)
  • Cross-project contribution links resolve and render across project logs (#254)
  • Preset adds gate behind the same retention-cap check as state add (#255)
  • One registration verb, run where the project is (#256), and project link completes a half-finished registration (#259)
  • Unshipped-by-branch stops claiming merged work when the fold cannot locate evidence (#260)
  • Every approval reaches the terminal render with its runnable command undimmed and untruncated (#265)
  • The landing site ships from this repo (#262)

New verbs and a grammar change make this a minor bump under pre-1.0 SemVer.

v0.5.1 — an agent reads every feature from the installed CLI

Choose a tag to compare

@tonite31 tonite31 released this 04 Aug 09:49

An agent working in a project can reach the managed block and the installed binary. This release makes that enough.

npm install -g superself

self help <topic>

Seven concept pages, shipped in the binary:

topic answers
agents how a session drives this CLI, start to finish
context what self context renders, and why something is missing from it
records one entity behind every record kind, and how a record is corrected
placement scope, priority and exposure — how a record earns its place in context
work the work graph: outcomes, evidence, criteria, and proposals
goals the long-term goal, objectives, milestones, and what reaching one takes
workspace the store, the projects in it, and moving it between machines

self lists them, so they are found without being announced. A subject a command already owns keeps one page: self help work prints the syntax and then the concept.

Until now --help said how to type a command and never when to reach for it — a reader of state place <id> [--exposure full|index|search] had the whole syntax and still did not know what exposure was.

A shorter managed block

The block written into AGENTS.md and CLAUDE.md is loaded every session, so it has to stay short. It keeps every rule that changes what a session does — read context, attach work, evidence-gated done, how a correction is spelled, decisions, blocked and retired units, register approved work, briefs on deferred work, merge control — and hands the rest to a topic. Its pointers render from the topic list, so it cannot name a page that does not exist.

The block says which CLI wrote it

<!-- superself:begin v0.5.1 -->

The block is rewritten by whatever binary a machine has. Without a stamp, an older CLI silently rewrites a newer block and two machines' commits disagree with nothing to point at. Blocks are found by the marker's prefix, so one written before this release is recognised and replaced in place — your existing projects need no migration, and the block comes forward on the next thing they record.

Full changelog: v0.5.0...v0.5.1