Releases · enes-alatas/spool

GitHub

21 min read Original article ↗

v0.7.0

What's Changed

Loop health watchdog. The hub now opens a problem, and tells the owner, when a loop's turn runs past a time limit, when a wake starts no turn, and when a message to a person goes undelivered past a grace period; each limit is a setting, and the Fleet page and the loop page say which it is and since when. The turn limit is checked outside the loop's own process, so a loop wedged mid-turn is reported too. (#1142–#1147, #1260: PRs #1245, #1246, #1248, #1255, #1258, #1267, #1268)

Import from prompts.chat. New loop fills itself from a prompts.chat bot URL: name, mission, runtime hint and the egress to allow, all editable before Create. New loop also imports a prompts.chat team: a preview lists the loops with their roles and handoffs and the channel they share, and Create makes them, each mission ending with its handoffs to the others. Links beside the import open prompts.chat's bots and teams to browse. The hub fetches from prompts.chat only, on the operator's paste (ADR-0063). (#1262, #1325: PRs #1271, #1272, #1274, #1288, #1291, #1327)

import-demo-r5.mp4

A person hears when a loop dropped their message. A message the hub gives up on is answered with a drop notice in the chat it came from, and the control room says which loops dropped it. (#1205, #1238: PRs #1239, #1241)

Cloud loops keep their session. A cloud loop keeps one claude.ai session through a rotation and a stop, so neither leaves a stale session in the operator's account. (#1278, PR #1293)

Fixes.

  • A claude that closed its output but kept running is ended instead of holding its loop deaf to ticks, messages, Pause and Kill. (#1247, PR #1252)
  • One loop that stops reading its commands no longer holds every other loop's ticks. (#1259, PR #1261)
  • A loop name or bot username that a mention already means is refused at create and rename, so no loop takes another's mentions. (#1273, PR #1279)
  • A new bus kind reaches no member's stream until it is named as streamable. (#1240, PR #1253)
  • Hub listeners take plain TCP, not Go's default multipath TCP. (#1250, PR #1251)
  • A cloud loop's setup view never shows a check's result before the one that ended. (#1243, PR #1244)
  • Disabled controls in the control room mute instead of fading. (#1275, #1277: PRs #1276, #1292)
  • A login whose loops all run in the cloud no longer shows a plan cap as if it acted: the control room says its figures will not update, or that usage is unknown. (#1303, PR #1307)
  • Signing a Claude login in on a hub running natively on macOS now works: the CLI opens no browser, the sign-in link is read even when the terminal wraps it, and the login stays out of the Keychain. (#1304, PR #1306)
  • The setup walk of a cloud loop from before its environment was set saves the repo with the environment in one edit, instead of refusing the repo with a message about the environment. (#1286, PR #1309)
  • spool upgrade now upgrades a hub no systemd unit runs, such as one started by hand on macOS: it backs up, installs and rolls back as the hub's own user, stops the hub, and asks you to start it again with the exact command. Its backup now works with the BSD tar macOS ships. (#1311, PR #1312)
  • On macOS, restoring a backup's workstation volumes works: the restore reads the volumes file with the BSD tar macOS ships. (#1314, PR #1328)
  • The Channels page no longer says no Telegram or Slack room carries a channel: it says a loop can bind its room to a channel on its own page. (#1317, PR #1318)

Upgrading

Run sudo spool upgrade v0.7.0 from v0.6.x on a hub a systemd unit runs. From v0.5.1, follow docs/OPERATIONS.md → Upgrading by hand.

A hub you started by hand, such as ./bin/spool on a Mac, upgrades to v0.7.0 by hand as before, because the spool upgrade in a v0.6.x binary still needs a unit: stop the hub, back up its data directory, move the checkout to v0.7.0, run make build and make image, and start it again (Upgrade). From v0.7.0 on, run ./bin/spool upgrade --no-unit as the user who runs the hub, without sudo (A hub with no unit).

After the upgrade every cloud loop makes one new claude.ai session at its next resume or rotation, since its stored state carries no session to keep; from then on it keeps one.

Full Changelog: v0.6.2...v0.7.0

Images

spool images prints how to fetch them. By digest:

  • ghcr.io/enes-alatas/spool-workstation@sha256:8d26489404bdf012905bca837da135f82674d9f8ef51390547e5b8f73ed9f40c
  • ghcr.io/enes-alatas/spool-egress@sha256:75ff504bbcb91c148144b7809500be32e5825da1ab06ad28b792c23a1ddcc1ba

The workstation image runs Claude Code 2.1.310.

v0.6.2

What's Changed

Several Claude logins. A hub now holds named Claude logins, each refreshed on its own, and every loop runs on the one it names. Settings lists the hub's logins and adds each; New loop and the loop page choose and show a loop's login; the Fleet page shows each login's plan usage, and the plan cap sleeps only the loops on the login that is over. (#925, #926: PRs #1148–#1178, #1229, #1236)

Cloud loops reach their repo. A cloud loop starts from its own GitHub repo, in a claude.ai environment of its own, through Claude's GitHub App (ADR-0061). The hub's token for the loop is a network secret on that environment, so it never enters the VM; the loop's page walks the setup (script, hub address, token with Rotate) and checks that the hub hears from the loop. Only the loop's own session reaches the hub, and only during a turn. (#1168, #1179: PRs #1185, #1190, #1214, #1218, #1226, #1233)

Fixes.

  • A message that reached a loop just before its process was killed is said again to the next session, instead of being dropped; a message the hub gives up on after three crashed turns shows in the loop's timeline. (#1189, PRs #1206, #1208)
  • Two images sent together in Telegram reach the loop as one message. (#1199, PR #1211)
  • The first-run page's Loops card names the loop whose wake failed, and how. (#1197, #1198, PRs #1209, #1210)
  • Settings says which login a check ran on, so a bare hub no longer shows one login as both signed in and refused. (#1203, #1212, PRs #1216, #1221)
  • The workstation image checks the fingerprint of Anthropic's apt signing key and pins the Claude Code version a release ships. (#1200, #1223, PRs #1222, #1234)

Breaking for API users. The single-login routes and plan usage's top level are gone; the control room reads plan usage from logins alone (#1077, PR #1178).

Upgrading

Run sudo spool upgrade v0.6.2 from v0.6.0 or v0.6.1. From v0.5.1, follow docs/OPERATIONS.md → Upgrading by hand.

A cloud loop created before this release is held with a problem until its repo and environment are set on its page; the shared environment under Settings → Cloud is no longer the start's source.

Full Changelog: v0.6.1...v0.6.2

Images

spool images prints how to fetch them. By digest:

  • ghcr.io/enes-alatas/spool-workstation@sha256:a2ee18b2a7beb7ef0d90b3fd8b79043b211f58d6ff516a0be32af91514ad3f10
  • ghcr.io/enes-alatas/spool-egress@sha256:a3f8f60805c0fa60f300e3f94bd698d3eceb38d40a0c1c8609f7f11a62ac3476

The workstation image runs Claude Code 2.1.310.

v0.6.1

spool-demo.mp4

What's Changed

Upgrading

spool upgrade no longer refuses a release (#1149): from v0.6.0, run sudo spool upgrade v0.6.1; from v0.5.1, follow docs/OPERATIONS.md → Upgrading by hand.

Full Changelog: v0.6.0...v0.6.1

Images

spool images prints how to fetch them. By digest:

  • ghcr.io/enes-alatas/spool-workstation@sha256:eaaf37bdb550036c738b72e2fca18785f6877f57e2371852c7cc95f1712754c2
  • ghcr.io/enes-alatas/spool-egress@sha256:da65a331995e4f724c0b2b01ad039621b486877a47921a35b3d20fdc27225d6a

v0.6.0

Cloud loops, users, and one Claude login

325 commits since v0.5.1. Binaries for linux and darwin (amd64, arm64) with SHA256SUMS and build attestations are attached; the images are published by digest below.

spool-demo.mp4

Cloud loops (ADR-0056, ADR-0059)

A loop can now run in a Claude Code cloud session beside the contained loops.

cloud-demo.mp4
  • Create a cloud loop from New loop; it wakes in a cloud session driven through the claude CLI.
  • Settings → Cloud is a guided setup: the cloud listener switch, the public address check, the Claude account sign-in with the CLI's own login, and the shared environment picked by name with the egress allowlist to paste and confirm.
  • Each cloud session gets a hub token of its own, revoked at a stop. A stopped cloud loop's page says a turn may still be running and links the session.
  • A cloud session is handed its loop's env-var connections as it starts; the room says when a connection change reaches a cloud loop, and deleting a cloud loop names the secrets a session keeps.
  • A cloud turn's cost is the hub's estimate, shown as such or as unknown.
  • A held cloud loop reads "needs setup"; a cloud loop warns while its allowlist isn't confirmed.

One Claude login (ADR-0058)

  • Contained loops reach the model through the hub's Claude proxy on one claude.ai login, signed in from first run or under Settings → Claude login. It supersedes the setup-token, refreshes before it expires, and Sign out revokes it.
  • A hub with no claude of its own signs in with the workstation image's.

Users, roles and sign-in (ADR-0048, ADR-0050, ADR-0051)

  • The control room has users with passwords and sessions. The first start creates admin; spool user manages users; roles bound what a user may call and see.
  • Users link their Telegram or Slack account under Settings → Account. A link code is the one way a person becomes an allowed sender. An owner sees links, blocks users, and manages users from Settings.
  • A loop's owner is chosen as a user, and the room shows who created and changed a loop.
  • The first-run page can be finished without a chat surface.

Private conversations (ADR-0049)

  • Every allowed sender holds a private conversation with a loop (dm:<name>). A loop's page shows the conversations the reader may read; a linked user reads the ones that are theirs.

Corrections (ADR-0055)

  • An edited or deleted Telegram or Slack message reaches its loops as a correction; a message still waiting is corrected in place; the room shows the edit beside the message.

Loop health (ADR-0053)

  • The control room shows a loop's open problems, and the owner is told once when a loop keeps failing or its workstation stays down.
  • A loop says what it is working on with a [doing: …] trailer; the Fleet row and the loop page show it.

Connections

  • A github-app connection: the hub holds the app's key and mints hour-long installation tokens. A loop's git and gh fetch them through the spool-github helper and never see the key.

Loop controls

  • A loop can be set to work alone (the Agent tool is denied).
  • A per-loop daily USD cap sleeps the loop until the day turns.
  • Plan usage only rises within a window, with its reset fixed.

Install, upgrade, backup (ADR-0052, ADR-0057)

  • A tag builds and attaches the binaries with checksums and attestations and publishes the images by digest. The hub never pulls; spool images says how to fetch a release's images.
  • spool upgrade upgrades the hub a systemd unit runs, with backup and rollback; spool migrations lists a build's migrations.
  • Backup and restore scripts cover the data directory and the workstation volumes.

Security hardening

  • Each workstation gets its own network and no NET_RAW; the hub warns at boot on an engine that lets DNS past the wall.
  • The egress allowlist refuses an IPv4 literal and a one-label domain, and reads *.example.com as .example.com.
  • spool-hook refuses a gh or git call that carries a credential, and kills that would reach other loops' processes.
  • The operator listener bounds what a caller can make it hold; the loop listener closes idle connections; members read no connection config and no pairing code.
  • Toolchain go1.26.10 (GO-2026-6809).

Control room polish

  • Settings → Egress is a dialog with a filtered list; copy chips for the literals an operator types; round loop controls; the mission is edited in a dialog; web addresses are links; a Fleet row shows its loop's mission.
  • The README demo video shows the room as it ships, and "Where loops run" shows a cloud loop running.

Upgrading from v0.5.1

  • By hand, this once: v0.5.1 has no spool upgrade, so follow docs/OPERATIONS.md → Upgrading.
  • The new admin's one-time password goes to stdout, which the unit discards: run spool user reset admin.
  • The setup-token keeps working until an admin signs in under Settings → Claude login; that sign-in replaces it. Cloud loops need the sign-in.
  • No re-linking: senders allowed before the upgrade keep working and keep their loops. A person links under Settings → Account only before they are chosen as a loop's owner.
  • A stored egress host the new allowlist refuses is removed under Settings → Egress.

Known issue

  • spool upgrade on v0.6.0 refuses every release (#1149): its parser rejects the header spool images prints. It fails safe and nothing is changed. Fixed in v0.6.1.

Full Changelog: v0.5.1...v0.6.0

Images

spool images prints how to fetch them. By digest:

  • ghcr.io/enes-alatas/spool-workstation@sha256:0fe04e3509aa00be432f8570ea3b734aad761f9ea295f6815014495ea7255f80
  • ghcr.io/enes-alatas/spool-egress@sha256:27fb2826902f13eae99847a2ae23bf36ca2cd503574c3325bb438b52ca2abb2f

v0.5.1

What's Changed

  • feat: the hub reads the Claude plan's usage from its loops' streams by @enes-alatas in #654
  • feat(web): each Fleet row shows its share of today's fleet spend by @enes-alatas in #656
  • fix(docker): every docker wake catches the proxy up with the fleet's hosts by @enes-alatas in #658
  • feat(web): the Fleet page shows the plan's 5-hour and 7-day usage by @enes-alatas in #659
  • test(itest): the live egress row counts only turns begun after each reach by @enes-alatas in #660
  • feat(web): a first-run step the hub is working on lists its phases by @enes-alatas in #663
  • fix: the toolchain moves to go1.26.9 for its standard-library fixes by @enes-alatas in #666
  • chore(web): bump source-map-js to 1.2.2 past its advisory by @enes-alatas in #669
  • ci: a feat PR that changes one half of the product names the other by @enes-alatas in #670
  • feat(httpapi): the onboarding read says what each waiting pillar is doing by @enes-alatas in #664
  • feat(loop): above the plan cap every loop sleeps until the window resets by @enes-alatas in #672
  • feat(web): the room sets the plan cap and shows a capped fleet by @enes-alatas in #671
  • fix(itest): the docker MCP-broker row skips on a host without docker by @enes-alatas in #673

Full Changelog: v0.5.0...v0.5.1

v0.5.0

What's Changed

  • fix(loop): a rehomed loop is not_provisioned until its workstation is built by @enes-alatas in #641
  • feat(store): secrets sealed at rest under a hub key by @enes-alatas in #642
  • feat(loop): an env-var change reaches an awake loop from its next turn by @enes-alatas in #644
  • feat(redact): the operator token and the hub key are redacted by @enes-alatas in #645

Full Changelog: v0.4.3...v0.5.0

v0.4.3

What's Changed

  • refactor(egress): remove a loop's own egress entries by @enes-alatas in #633
  • feat(loop): the loop view says when a rehome is pending by @enes-alatas in #635
  • feat(web): a bare loop's Workstation panel moves it into a docker container by @enes-alatas in #637
  • fix(itest): the empty-context check no longer races a new loop's first tick by @enes-alatas in #638

Full Changelog: v0.4.2...v0.4.3

v0.4.2

What's Changed

  • feat: every change to a connection is on an append-only record by @enes-alatas in #608
  • feat: rotating a connection's value retires the old one and ends the sessions that held it by @enes-alatas in #612
  • feat: revoking a connection takes it from every loop, refuses it from then on, and keeps it redacted by @enes-alatas in #613
  • fix(telegram): a loop's reaction to a teammate's message reaches a supergroup by @enes-alatas in #615
  • feat(web): a connection's record, rotate and revoke in the control room by @enes-alatas in #616
  • feat(web): a loop's Telegram notes say where a reaction to a teammate shows by @enes-alatas in #619
  • chore: remove the per-loop secrets routes; private env-var connections replace them by @enes-alatas in #618
  • feat: the fleet's extra egress hosts are stored and apply without a restart by @enes-alatas in #620
  • feat(web): Settings has an Egress section, the fleet's allowlist by @enes-alatas in #621
  • feat: secrets a loop holds don't reach teammates or hide in encodings; ADR-0045 states the posture by @enes-alatas in #625
  • feat(httpapi): the hub brokers a loop's http MCP servers by @enes-alatas in #627
  • feat: a bare loop can be rehomed into a docker workstation by @enes-alatas in #631

Full Changelog: v0.4.1...v0.4.2

v0.4.1

What's Changed

  • fix(httpapi): a docker loop that cannot reach the loop listener is refused by @enes-alatas in #477
  • docs(release): a release is titled with its tag alone by @enes-alatas in #486
  • feat: channels exist in the hub, with the fleet channel named group by @enes-alatas in #488
  • feat(web): a Channels page creates channels and chooses their loops by @enes-alatas in #490
  • docs: a loop reaches only Spool's MCP server by @enes-alatas in #494
  • fix(runtime): Spool builds on macOS, keeping Pdeathsig on Linux by @enes-alatas in #498
  • fix(loop): a loop whose claude cannot reach the hub is down, and its work waits by @enes-alatas in #496
  • fix(httpapi): a Docker Desktop workstation reaches the loop listener by @enes-alatas in #509
  • feat(cmd): a docker hub puts its loop listener where workstations reach it by @enes-alatas in #511
  • feat(web): name a claude that can't reach the hub, and offer no Power on for it by @enes-alatas in #510
  • feat(loop): a loop posts to its channels by name by @enes-alatas in #512
  • test(uifixture): the fixture seeds channels besides the fleet channel by @enes-alatas in #513
  • feat(telegram): a group is bound to a channel per loop, and carries it both ways by @enes-alatas in #516
  • docs: a Report a problem form, triage rule, and CONTRIBUTING for outside reports by @enes-alatas in #522
  • feat(loop): the loop view says what the session reaches through MCP by @enes-alatas in #525
  • chore(ci): an issue from outside the team is labelled and welcomed by @enes-alatas in #528
  • feat(web): the loop page lists its rooms and binds one to a channel by @enes-alatas in #530
  • feat: reactions exist in the hub — ADR-0040, store and bus kind by @enes-alatas in #536
  • feat(web): the loop page shows what the session reaches through MCP by @enes-alatas in #537
  • feat(telegram): reactions travel both ways between a loop's bot and the hub by @enes-alatas in #538
  • fix(web): no next wake is shown while a loop is in a turn by @enes-alatas in #539
  • feat(loop): a loop reacts with send_message and hears of reactions on its next turn by @enes-alatas in #540
  • feat(web): one slider sets a loop's min wake, tick and max wake by @enes-alatas in #545
  • feat(slack): reactions travel both ways between a loop's app and the hub by @enes-alatas in #544
  • feat(web): the control room shows a message's reactions by @enes-alatas in #547
  • chore(egress): a workstation can pip install from PyPI by @enes-alatas in #546
  • feat: polls exist in the hub — ADR-0041, store, bus kind and the close by @enes-alatas in #555
  • feat(telegram): a loop's poll is a native Telegram poll, and its votes reach the hub by @enes-alatas in #557
  • feat(web): a loop's channels are tabs of its page, and Activity filters by channel by @enes-alatas in #556
  • feat(slack): a loop's poll is its app's post with buttons, and clicks are votes by @enes-alatas in #558
  • feat: a loop polls, votes and closes with send_message, and votes ride with its next turn by @enes-alatas in #560
  • fix(telegram): an unanswered send is retried on a fresh connection within seconds by @enes-alatas in #562
  • feat: the control room renders polls with their tallies by @enes-alatas in #563
  • feat: the hub pins a PreToolUse hook that refuses the host's shared-state commands by @enes-alatas in #565
  • feat: a loop dismisses a lost send, and is reminded of it until it does by @enes-alatas in #566
  • feat: connections can be created, listed, described and deleted, secret write-only by @enes-alatas in #568
  • feat(web): show when a loop dismissed or left a lost send by @enes-alatas in #569
  • fix(telegram): the HTTP/1 transport offers only http/1.1 in its TLS handshake by @enes-alatas in #571
  • feat: a connection is attached to and detached from loops, and an attached one can't be deleted by @enes-alatas in #573
  • feat: a connection's env-var kind replaces env-credential by @enes-alatas in #575
  • feat(web): Connections page and loop-page connections panel by @enes-alatas in #577
  • feat: per-loop secrets are env-var connections, and an attached env-var is in the wake env by @enes-alatas in #578
  • fix(web): connections say env variables reach their loop, and long names fit the panel by @enes-alatas in #583
  • feat: GET /api/onboarding reports readiness per pillar by @enes-alatas in #584
  • feat(web): first-run page for the three onboarding pillars by @enes-alatas in #586
  • feat: the harness pillar checks the Claude login with one haiku answer by @enes-alatas in #587
  • feat(web): the first-run Harness card sets up and checks the login in place by @enes-alatas in #590
  • feat(slack): a Slack channel is a room bound to a channel per loop by @enes-alatas in #591
  • feat(web): the first-run Loops and Chat surface cards open their steps in place by @enes-alatas in #592
  • test: two docker itests stop racing other hubs on the shared daemon by @enes-alatas in #594
  • feat(loop): an attached mcp-server is a server in the loop's mcp-config by @enes-alatas in #598
  • feat(web): a Slack loop's channels are rooms on its page by @enes-alatas in #596
  • feat(docker): an attached MCP server opens its host to its loop alone by @enes-alatas in #602
  • feat: a connection can be private to one loop, and is shared one way by @enes-alatas in #603
  • feat(web): the loop page's secrets are its private connections by @enes-alatas in #605

Full Changelog: v0.4.0...v0.4.1

v0.4.0

What's Changed

  • fix(web): the Undelivered pane restacks by its own width, not the viewport's by @enes-alatas in #417
  • fix(loop): a refused Claude login is a named down reason, not silence by @enes-alatas in #416
  • feat(surface): the owner hears once that a refused login stopped their loops by @enes-alatas in #421
  • fix(web): a refused Claude login shows its cause and its fix by @enes-alatas in #422
  • feat(surface/slack): each loop's Slack app holds a Socket Mode connection by @enes-alatas in #423
  • fix(telegram): a native reply reaches its author whichever bot ingests it by @enes-alatas in #425
  • fix(web): the New loop runtime select says why it is locked by @enes-alatas in #428
  • feat(surface/slack): what a human says to a loop's app reaches the loop by @enes-alatas in #430
  • fix(web): New loop locks the workspace field for a docker loop by @enes-alatas in #432
  • fix(web): a Slack post in the fleet channel says it came via Slack by @enes-alatas in #433
  • feat(surface/slack): a loop's words reach Slack: group mirrors, owner_dm, threads by @enes-alatas in #434
  • feat(surface/slack): turned-away senders, the login notice and channel_name on Slack by @enes-alatas in #435
  • fix(web): Slack attach and channel copy say what actually happened by @enes-alatas in #439
  • fix(loop): a refused login keeps one tick, not a backlog of them by @enes-alatas in #436
  • fix: a running hub settles every send it accepted by @enes-alatas in #440
  • fix(cmd): a hub that stops serving exits 1 by @enes-alatas in #443
  • fix(store): commits wait on the WAL, not on an fsync; a perf smoke asserts the envelope by @enes-alatas in #444
  • docs(readme): Quick start is a numbered path that works end to end by @enes-alatas in #448
  • feat(surface): a stopping hub fails its surfaces' held sends before the store closes by @enes-alatas in #449
  • feat(attach): a photo or file sent to a loop reaches it as a path it can read by @enes-alatas in #454
  • feat(attach): a loop sends a file with its message by @enes-alatas in #455
  • fix(web): a context trend bar keeps its width at any turn count by @enes-alatas in #459
  • feat(web): glyph and wordmark match the README banner by @enes-alatas in #461
  • feat(slack): files cross Slack both ways by @enes-alatas in #458
  • chore: the README opens with a demo video, rendered by make demo by @enes-alatas in #463
  • feat(httpapi): the control room reads, serves and sends attachments by @enes-alatas in #467
  • feat(web): messages carry files in the control room by @enes-alatas in #469
  • docs(conventions): every slice of a multi-PR issue is its own sub-issue by @enes-alatas in #465
  • docs(conventions): rare procedures move to skills by @enes-alatas in #473
  • chore(deps-dev): Bump brace-expansion from 1.1.18 to 1.1.21 in /web in the npm_and_yarn group across 1 directory by @dependabot[bot] in #468
  • docs(conventions): a bare #n may sit anywhere in a code comment by @enes-alatas in #478
  • feat(web): allowing a pending sender takes the code they were sent by @enes-alatas in #482

New Contributors

Full Changelog: v0.3.0...v0.4.0