Skip to content

Releases: lightheaded/kari

kari v0.16.1

Choose a tag to compare

@github-actions github-actions released this 26 Sep 09:41
v0.16.1
b042b26

kari v0.16.1 — built from b042b26.

macOS no longer names kari in the dialog about access to the data of other
apps, when the request comes from a Claude session that kari did not ask for.

TL;DR

  • Stop macOS from blaming kari for what Claude sessions read (#55)

What changed

Stop macOS from blaming kari for what Claude sessions read (#55)

macOS asked whether "kari.app would like to access data from other apps",
also when kari was closed and the user only opened claude agents in a
terminal. The cause was the responsible process. macOS charges a privacy
request to the responsible process of the requester, and a child inherits it
from its parent. When a claude --bg call from kari started the Claude
daemon, kari became responsible for that daemon, and for every session that
the daemon started after that. Those sessions start the MCP servers of the
user. When one of them reads the data of another app, the dialog named kari.
The setting that turns off MCP servers in card runs did not help, because it
does not reach the sessions that the daemon starts in advance.

kari now starts every claude process with the responsibility disclaimed:
claude --bg, claude stop, claude agents and the summary call. The child
is then responsible for itself, the same as a daemon that a terminal starts,
and a dialog names claude and not kari. The call has no effect on Linux
and Windows.

Rust cannot give posix_spawn attributes to Command, so kari does not
replace Command. A pre_exec hook in the forked child calls posix_spawnp
with POSIX_SPAWN_SETEXEC and the disclaim flag, which turns the spawn into
an exec of that child. Pipes, the working directory, timeouts and kill
work as before. The flag comes from a private libSystem call that Chromium
and LLDB also use. kari looks it up at run time, so a macOS without it
starts the child as before.

Install

Download the .dmg for your Mac: aarch64 for Apple silicon, x64 for Intel.
The app is not signed. After you copy it to Applications, run:

xattr -dr com.apple.quarantine /Applications/kari.app

On Windows, run kari_0.16.1_x64-setup.exe. It installs for the current user,
because kari reads the Claude Code state of whoever is logged in. That installer is
not signed either, so SmartScreen asks once: More info, then Run anyway.

An installed kari updates itself to this release: it checks on start and every six hours,
writes the new version beside the running one and offers a restart. Settings, Updates has the switch.

Every host runs the headless node, so that its sessions stay on the board while no window is open.
A Mac takes kari-node-v0.16.1-aarch64-apple-darwin (or the x86_64 one),
a Linux host kari-node-v0.16.1-x86_64-unknown-linux-gnu.tar.gz,
a Windows host kari-node-v0.16.1-x86_64-pc-windows-msvc.zip.
Run kari-node service install to keep it running at login. One engine runs on a host at a time:
open the app and the node steps down, quit the app and it takes the host back.
A node updates itself only when asked: kari-node update, or serve --auto-update.

An Android phone installs kari-latest.apk. Add this repository to Obtainium: the asset name never
carries the version and the signing key never changes, so an update installs over the previous build.
The phone reaches the nodes over a private network, such as a VPN, and pairs with the code from
Settings, Nodes on the desktop. See TOUR.md.

See the README for requirements and setup.

kari v0.16.0

Choose a tag to compare

@github-actions github-actions released this 21 Sep 17:41
v0.16.0
9ca8ba4

kari v0.16.0 — built from 9ca8ba4.

One subscription can now be kept for its own work while another is spent. The
automation switch used to cover every machine or a single one; each quota row
carries one of its own, and it sets the machines signed in to that account.

TL;DR

  • Let the automatic behaviour be set for one account

What changed

Let the automatic behaviour be set for one account

Quota belongs to a Claude Code login, not to a machine, and a user with two
logins usually wants different things from them: keep one subscription for the
work it is paid for, and spend the other. The automation switch could not say
that. It sets every machine that answers, or the one machine a filter names,
and a subscription is usually neither of those.

Each quota row now carries the same Off / Ask / Auto switch, and it writes to
the machines signed in to that account. The row already names them, so the
control sits beside the meter it protects. mixed in place of a selected mode
means those machines do not agree, which one click puts right.

The account is the scope of the write, not a second place the mode is kept.
The hub resolves the account key to nodes and writes each node's own settings,
because the planner that reads the mode runs on that node and must still
answer with no hub and no network. Two things follow, and both are the honest
answer rather than a limitation to work around:

  • A machine that is asleep is out of scope rather than a failure. There is no
    queue for a setting, and it keeps the mode it was last given.
  • A machine that joins the account later starts at its own mode, and the row
    then reports mixed.

NodeStatus gains account_key, the key its quota row groups under, so the
hub can resolve an account to machines and the switch can read what they
agree on. It is the same key board() already groups the meters with, and the
demo board that takes the screenshots now carries it too, so those images show
the control taking the path it takes in the app rather than the fallback meant
for a hub that predates the field.

The write is one call rather than one call per machine, so a half-applied
switch names the machines that refused, as the every-node switch does. The
route is POST /kari/v1/hub/accounts/automation, with the key in the body:
a machine whose account kari cannot read is keyed node:<id>, and a path
segment with a colon in it is one more thing to encode correctly at both ends.
SplitHub asks both halves, because one login is usually signed in on this
machine and on hosts that only the server knows.

The switch on the phone is unchanged, and still sets a node or every node.

Install

Download the .dmg for your Mac: aarch64 for Apple silicon, x64 for Intel.
The app is not signed. After you copy it to Applications, run:

xattr -dr com.apple.quarantine /Applications/kari.app

On Windows, run kari_0.16.0_x64-setup.exe. It installs for the current user,
because kari reads the Claude Code state of whoever is logged in. That installer is
not signed either, so SmartScreen asks once: More info, then Run anyway.

An installed kari updates itself to this release: it checks on start and every six hours,
writes the new version beside the running one and offers a restart. Settings, Updates has the switch.

Every host runs the headless node, so that its sessions stay on the board while no window is open.
A Mac takes kari-node-v0.16.0-aarch64-apple-darwin (or the x86_64 one),
a Linux host kari-node-v0.16.0-x86_64-unknown-linux-gnu.tar.gz,
a Windows host kari-node-v0.16.0-x86_64-pc-windows-msvc.zip.
Run kari-node service install to keep it running at login. One engine runs on a host at a time:
open the app and the node steps down, quit the app and it takes the host back.
A node updates itself only when asked: kari-node update, or serve --auto-update.

An Android phone installs kari-latest.apk. Add this repository to Obtainium: the asset name never
carries the version and the signing key never changes, so an update installs over the previous build.
The phone reaches the nodes over a private network, such as a VPN, and pairs with the code from
Settings, Nodes on the desktop. See TOUR.md.

See the README for requirements and setup.

kari v0.15.0

Choose a tag to compare

@github-actions github-actions released this 15 Sep 22:19
v0.15.0
90b7588

kari v0.15.0 — built from 90b7588.

kari keeps its own hook entries current. You click "Install hooks" once. From
then on, every start compares the entries in settings.json with the events
this version registers, and writes them again when a release adds one. Away
mode and the autopilot gate no longer wait for a second click after an
upgrade.

TL;DR

  • Keep the hook entries current at every start (#49)

What changed

Keep the hook entries current at every start (#49)

A release can add an event to the set kari registers in settings.json. 0.11
added PermissionRequest for Away mode, and the autopilot gate added Bash to
the PreToolUse matcher. A user who installed the hooks before that kept the
old set, so the feature was off and only a line in the node log said so.

kari owns its entries now. At start the server compares the event, the
matcher, the timeout and the command line with what this version installs. If
one differs, it writes the entries again, with a backup first and every other
hook untouched. A user who never installed the hooks gets nothing, and the
write is idempotent, so the next start writes nothing.

entries_current replaces held_event_installed and gate_installed, which asked
the same question for one event each.

The hooks_install example refuses to run without CLAUDE_CONFIG_DIR: it writes
settings.json and removes the relay script, so it must never touch a real
install.

Install

Download the .dmg for your Mac: aarch64 for Apple silicon, x64 for Intel.
The app is not signed. After you copy it to Applications, run:

xattr -dr com.apple.quarantine /Applications/kari.app

On Windows, run kari_0.15.0_x64-setup.exe. It installs for the current user,
because kari reads the Claude Code state of whoever is logged in. That installer is
not signed either, so SmartScreen asks once: More info, then Run anyway.

An installed kari updates itself to this release: it checks on start and every six hours,
writes the new version beside the running one and offers a restart. Settings, Updates has the switch.

Every host runs the headless node, so that its sessions stay on the board while no window is open.
A Mac takes kari-node-v0.15.0-aarch64-apple-darwin (or the x86_64 one),
a Linux host kari-node-v0.15.0-x86_64-unknown-linux-gnu.tar.gz,
a Windows host kari-node-v0.15.0-x86_64-pc-windows-msvc.zip.
Run kari-node service install to keep it running at login. One engine runs on a host at a time:
open the app and the node steps down, quit the app and it takes the host back.
A node updates itself only when asked: kari-node update, or serve --auto-update.

An Android phone installs kari-latest.apk. Add this repository to Obtainium: the asset name never
carries the version and the signing key never changes, so an update installs over the previous build.
The phone reaches the nodes over a private network, such as a VPN, and pairs with the code from
Settings, Nodes on the desktop. See TOUR.md.

See the README for requirements and setup.

kari v0.14.0

Choose a tag to compare

@github-actions github-actions released this 11 Sep 11:17
v0.14.0
b8ae7a0

kari v0.14.0 — built from b8ae7a0.

This release keeps your unsaved text when something other than you stops kari.

kari already asked before Cmd+Q and the tray threw unsaved input away. A signal
did not go through that path. A rebuild that replaced the app killed the window
with no word, and took the open card with it.

kari now catches SIGTERM and SIGINT. With no unsaved input it quits at once, as
before. With unsaved input the window comes to the front and asks, and kari
quits anyway after 20 seconds, so an unattended build goes through. A second
signal quits at once, because Ctrl+C twice in a terminal must always work.

The new task dialog and the card drawer also write their text to the browser
store on every keystroke. The draft goes back into the form the next time it
opens, and the form says where the text came from. A drawer draft carries the
time its card was last changed, so a newer card drops a stale draft.

No check covers a quit path, because CI has no window, no tray and no bundle.
Two unit tests hold the one rule a test can hold: every wait ends.

TL;DR

  • Ask before a restart throws away unsaved input

What changed

Ask before a restart throws away unsaved input

A rebuild that replaced the app took the card that was open in the new task
dialog with it. kari already asks before Cmd+Q and the tray throw unsaved
input away. A signal was not on that path, so it killed the window with no
word.

Two layers, because no question survives every kill:

  • kari catches SIGTERM and SIGINT. With no unsaved input it quits at once, as
    before. With unsaved input the window comes to the front and asks, and kari
    quits anyway after 20 seconds, so an unattended build goes through. A second
    signal quits at once, because Ctrl+C twice in a terminal must always work.
  • The new task dialog and the card drawer write their input to the browser
    store on every keystroke. The draft goes back into the form the next time it
    opens, and the form says where it came from. The dialog drops its draft when
    the card is added. The drawer saves each field on blur, so its draft holds
    the text that still waits for a blur and the message that is not sent yet: a
    save, a send, and a confirmed discard each drop it. A drawer draft holds the
    updated_at of its card, and a newer card drops it, so a stale draft cannot
    go over what the node changed.

Every quit question now ends. Cmd+Q, the tray and an AppleScript quit share
one path, and a script cannot tell them apart from a keypress, so that path
waits quietly for ten minutes before it quits. The window shows a countdown
only for a rebuild, where nobody may be there to read it. Two unit tests hold
that rule: a future edit that sets either wait to "forever" fails the build.

Install

Download the .dmg for your Mac: aarch64 for Apple silicon, x64 for Intel.
The app is not signed. After you copy it to Applications, run:

xattr -dr com.apple.quarantine /Applications/kari.app

On Windows, run kari_0.14.0_x64-setup.exe. It installs for the current user,
because kari reads the Claude Code state of whoever is logged in. That installer is
not signed either, so SmartScreen asks once: More info, then Run anyway.

An installed kari updates itself to this release: it checks on start and every six hours,
writes the new version beside the running one and offers a restart. Settings, Updates has the switch.

Every host runs the headless node, so that its sessions stay on the board while no window is open.
A Mac takes kari-node-v0.14.0-aarch64-apple-darwin (or the x86_64 one),
a Linux host kari-node-v0.14.0-x86_64-unknown-linux-gnu.tar.gz,
a Windows host kari-node-v0.14.0-x86_64-pc-windows-msvc.zip.
Run kari-node service install to keep it running at login. One engine runs on a host at a time:
open the app and the node steps down, quit the app and it takes the host back.
A node updates itself only when asked: kari-node update, or serve --auto-update.

An Android phone installs kari-latest.apk. Add this repository to Obtainium: the asset name never
carries the version and the signing key never changes, so an update installs over the previous build.
The phone reaches the nodes over a private network, such as a VPN, and pairs with the code from
Settings, Nodes on the desktop. See TOUR.md.

See the README for requirements and setup.

kari v0.13.0

Choose a tag to compare

@github-actions github-actions released this 11 Sep 09:15
v0.13.0
9d46868

kari v0.13.0 — built from 9d46868.

This release makes the phone a client you can work from, and the board say
where every card runs.

The phone gets a card you can leave and read. The system Back button closes
the card instead of the app. The card is now a sheet with a Chat page and a
Details page. The board carries the switch for what the nodes do unasked. A
long conversation reads from its end, in pages, on every client.

The board says more about each card. Each machine holds one colour, and a tag
on every card names the machine and the account that pays for it. The quota
bars of two accounts line up in the Mac app, and every window says when it
resets.

Two changes start a card quicker. A #name word in the one-line draft picks
its project. The toast that reports a new card offers Open.

A background run now starts no MCP servers, unless a card asks for them. An
unattended run no longer raises the macOS dialog about the data of another
app. A jump into herdr also opens in the workspace of the project, and not
next to unrelated work.

TL;DR

  • Show the machine and the account on every card, in colour
  • Give the phone card a chat page, and read a long conversation from its end
  • Line up the quota bars, and let every window name its reset
  • Three fixes for the phone: the Back button, the status bar and the automation switch
  • Pick the project of a quick task with a #name word
  • Offer Open on the toast that reports a new card
  • Open a jump in the herdr workspace of the project
  • Let a run start no MCP servers, and let a card overrule the setting

What changed

Show the machine and the account on every card, in colour

The board named the machine in a grey chip, and it never named the account.
On a board with three machines that chip reads like every other chip, so the
reader must stop at each card to find where the work runs. The account lived
only in the quota strip, so a card gave no way to see whose window it spends.

Each machine now holds one colour. A node tag on every card carries that
colour, the machine name, and behind a hairline the account that pays for it.
The same colour marks the chip in the filter bar, the machine in the quota
strip, the head of a queue block and the head of a plan. A colour learned once
holds everywhere. The drawer, the phone card and the conversation window carry
the tag too. The account half appears only when the board spends more than one
account, because a name that never changes tells the reader nothing.

The colour comes from a hash of the node id over eight colours. Nothing stores
it and nobody picks it. The colour survives a restart, and two hubs that see
one node paint it the same.

The quota strip keeps the grid it has. The machine name there takes the colour
of the machine, and the name carries no box and no width of its own, so the
bars of two accounts still start at one place.

State keeps its own colours: the left edge of the card and the state chip. A
first attempt put the machine colour on the top edge of the card as well. Next
to the state edge that reads as an alarm, so the machine colour stays inside
the tag. In Settings and in the Nodes tab the question is whether a node
answers, so the status dot there stays green and grey.

Give the phone card a chat page, and read a long conversation from its end

The card sheet on the phone put the fields, the run log and the last exchange
in one scroll, and the small cross in the corner was the only way out of it. A
conversation of any length was out of reach. Every client also asked the node
for the whole transcript at once, which is thousands of markdown turns on a
session that ran for a day.

The sheet now carries a bar of its own: the way back on the left, and two
pages on the right. Chat holds the conversation. Details holds the facts, the
fields, the run log and the line to resume the session in a terminal. The
prompt box stays at the foot of both. A card that never ran opens on Details,
because a conversation it does not have yet is an empty page. A card that
holds a permission prompt opens at the top of Chat, because the Allow and Deny
buttons sit above the turns: on the newest turn they opened off the screen, on
the one card that is opened to answer something now.

The conversation reads from its end, on every client. It asks the node for the
newest 200 turns, then for 400 more each time the reader asks, and for the
whole transcript when the reader asks for that. A page of older turns lands
above the reader and moves the scroll by the height it added, so the turn on
screen stays where it was. A turn is keyed by its place in the transcript and
not by its place in the list, or a new page would build the list again and
drop the reading position with it. A view that holds the conversation and
nothing else, the phone chat page and the pop-out window, opens on the newest
turn and follows it while the reader sits at the end.

The prompt box is never dead. An offline node, and a job that holds the card,
used to disable it: there was nothing to type into and nothing to keep. The
box now takes text in every state and says what will happen to it. The reply
box on a card in the board and inbox lists opens the same way, and the Send
button is what waits: while a job holds the card and its session does not
answer yet, Send is off and a line says why, so no second run starts in the
same directory. Each card also keeps the text typed for it while the drawer is
open, so a tap on another card no longer throws an answer away.

src/back.ts is rewritten as a stack that takes its history as an argument,
so src/back.test.ts can drive it with a fake one. The behaviour is the
behaviour it had: every open layer holds one history entry, and one step back
closes the layer that pushed it. Two cases that had no test now have one. A
layer that refuses to close takes a new entry, so the next press asks again.
A layer that opens as another closes takes over the entry the closing one
owes, which is what made the sheet need two presses in the first build. The
hook also takes a flag that says whether the layer is open, so a component
that stays mounted can hold a layer. The phone tabs use that flag: back goes
to the board from any other tab, and leaves the app from the board.

The demo board now names its hub. The fixture carried no hub_id, no
hub_name and no primary flag, and the Nodes tab of the phone reads all
three, so the phone layout of bun run demo showed no Nodes tab at all. The
Settings image is retaken with the three fields, so it shows the pill that
says which device pushes the columns, as a real board does.

Line up the quota bars, and let every window name its reset

The 7-day bars of two accounts stood at different places in the line, and the
reset time of one window ran into the name of the next.

A row held its parts in flex boxes inside a grid cell, each part with a fixed
pixel width. The Mac app draws with WebKit, which gives such a cell less width
than its content asks for, so the boxes were squeezed and the text inside them
overflowed. How far each row slipped depended on the words in it: a row that
said "2h 24m" pushed the second bar 14 pixels further right than a row that
said "14m". The same page in Chromium looked correct, which is why the fault
survived the last change.

Every part of every row is now an item of one grid: the name, the machines,
and for each window the label, the bar, the percentage and the reset time. No
part carries a pixel width of its own. The widest text sets the width of its
column, every bar starts where the bar above it starts, and the rows of a line
share the grid. Measured in WebKit at four window widths, both bars of a column
now start at the same place.

The bar is the one part that gives way. A window too narrow for the row shrinks
both bars to 28 pixels before it touches a word, and both shrink by the same
amount, because one track holds them. Below that the strip scrolls, so two
words can never overlap.

A window with no reset time showed an empty box beside the bar, which reads as
a lost number. Each window now says when it resets, or why it cannot: a window
that Claude Code reports no reset time for says "not started", and a window
with no reading at all says "no data". The tooltip of a meter names the reset
in clock time, the percentage used, and the sample the row was read from.

The phone shows the same. A meter there keeps its box when the window has no
reading, so the bars of two accounts stand in one column.

Three fixes for the phone: the Back button, the status bar and the automation switch

Three faults on the phone, each small on its own, made a card hard to leave,
hard to read and impossible to stop.

The Back button closed kari instead of the card. On the phone the only way
out of a card was the small cross in its corner, and a press of the system
Back button left the app. Finding the card again started from the board. Two
things stood in the way. Tauri's activity refuses back navigation, so a press
never reached the page: scripts/android-back.sh writes the generated
MainActivity.kt again with handleBackNavigation on, and the activity then
hands the press to the web view while the web view has a step to take. The
script runs after every bun tauri android init, next to the icon script, in
both workflows, because the generated project is not tracked. The page also
had no step to take, because a card is not a page. The card and every dialog
now put one entry in the history while they are open, and take it off again
when they close another way. Back closes the innermost layer first and leaves
the app only when nothing is open. A card that holds a prompt you did not send
asks before it closes, exactly as Escape does on the desktop. The history is
matched to the open layers after the current work, not during it, because
React mounts an effect twice in development and answering each step at once
read as a press and closed the card...

Read more

kari v0.12.0

Choose a tag to compare

@github-actions github-actions released this 10 Sep 17:26
v0.12.0
4a49d37

kari v0.12.0 — built from 4a49d37.

This release lets a card hold work for later, and carry more of it.

A card now takes a screenshot or a file, and every run of that card reads it.
A card can be booked to start when the rate limit resets, so you do not come
back at the reset to press Start. A card can be added and edited while its
node sleeps, and the write lands when the node answers.

Two changes make an unattended run safer to leave alone. An autopilot run can
open a pull request, but it cannot merge, tag or publish. A prompt handed to a
running session now says who sent it, and the card reports whether that
session took the prompt or holds it.

On a Mac, a build that you install keeps its permission answers, so macOS asks
once instead of after every build.

TL;DR

  • Stop an autopilot run short of releasing
  • Let a card be added and edited while its node is offline
  • Let a card carry files, and hand them to every run
  • Let a card be booked to run when the rate limit resets
  • Stop macOS from asking for the same permission after every build
  • Say who kari is on a peer send, and read the receipt

What changed

Stop an autopilot run short of releasing

Autopilot can do the work and open a pull request. It cannot merge, tag,
publish a release, push an image, or commit where a commit deploys.

An autopilot run needed a gate that a permission prompt cannot give, because
the run uses bypassPermissions and sees no prompt. kari already relays the
Claude Code hooks, so the gate is a PreToolUse hook on Bash that answers with
permissionDecision deny and a reason the run can act on.

The marker had to reach the session first. Proposal.auto said that autopilot
accepted the plan, but accept_proposal called start_card without it, so
nothing downstream knew where the run came from. Cards now carry
started_by_autopilot, and autopilot starts a card through start_card_auto.
A person who starts the same card clears the flag.

The new gate module reads intent rather than text. It splits a command line
the way a shell splits it, respects quotes, follows sh -c and nix run, and
reduces each part to a program and its arguments. So /opt/homebrew/bin/gh pr merge 1 is a refusal, nix run nixpkgs#skopeo -- copy is a refusal, and a
commit message that holds the words gh release is not.

A refusal is visible. It goes on the card, into the drawer, onto the phone and
into a notice, with the command that was tried. A silent deny would waste the
run and teach the user nothing.

autopilot_protected_paths is empty by default. Those directories belong to
the user, so this repository names none of them. Until you name one, that
fourth rule matches nothing, and there is no field for it in Settings yet.

The gate raises the cost of an accidental release. It does not stop a
determined process, and it must not be read as a guarantee. A session that
kari cannot resolve to a card is not judged, because a gate that fails closed
would block the terminal the user works in. A shell alias, a shell function, a
script on disk that holds the command, and a curl against the GitHub API all
get past it.

Let a card be added and edited while its node is offline

A node keeps the cards of its own machine, so every card write goes to the
node that owns the card. A node that sleeps, that is off the network or that
is shut down cannot take the write, and kari refused it. A person then lost
the note they typed because a laptop was closed.

The hub now holds the write instead. A new task, an edit of a card and a
delete all work while the node is away. The card is on the board at once,
marked "waiting for node", and the node row counts what is waiting. When the
node answers, the hub sends the queue, oldest first, before it publishes the
board.

The queue is in the hub's store, so it survives a restart. One card is one
write: a card added and then edited five times is one card to send, not six
calls, and a card added and then deleted leaves nothing to send at all.

An action that needs the machine is still refused with the node's name. Run,
stop, jump in, summarize and a booked run are not held, because a held one
starts work at a time nobody asked for. A move between columns is not held
either, because the column rules belong to the node. A file cannot be attached
to a card that still waits, because the file belongs on the node and the card
is not there yet.

A write the node refuses stays at the head of the queue and stops the flush
there. The writes behind it keep their order, and the node row shows the
reason. An edit of the same card rewrites the held write, so a person can
repair a card the node refuses again.

The card carries the id the hub gave it, and the node adopts that id through
the route that already restores a deleted card. A queued edit therefore still
names a card that exists, and the queue needs no new route and no change on
the node.

cargo run -p kari-core --example held_writes proves it against a real node:
it kills the node, writes a task and a note, starts the node again and prints
the card the node itself answers with.

Let a card carry files, and hand them to every run

A prompt sometimes needs a picture: a screenshot of the bug, a mock-up of the
screen. Until now there was no way to give one to a card.

Paste a screenshot into the prompt box of the card drawer, or press Attach and
pick a file. The New task dialog takes files the same way. kari names the paths
under the prompt of every run of that card, and starts the run with --add-dir
on their directory, so claude reads the files as part of the task. A card
shows a paperclip with the count, and the drawer shows a thumbnail of each
picture. One file is at most 4 MiB.

The bytes live on the node that owns the card. That follows from how Claude
Code reads a file: it opens a path on the host that runs the session. A server
was the other candidate and it fails twice. The server is optional and off by
default, so attachments would exist only for the users who run one, which
breaks rule 2 in AGENTS.md. And the node needs the file on its own disk in any
case, so a server can only ever be a hop. With a server the bytes travel down
the existing link, into the same route the node serves on loopback, and the
server keeps no copy.

Locking the node once a card holds a file was the other option, and so was
warning that a move discards the file. Neither is needed.
move_card_to_node already writes the card on the target and deletes it from
the source, so it reads the files first and writes them beside the new card. A
file that cannot be read stops the move and names itself.

The directory is the store, with no table beside it. A row and a file can
disagree, and then a card offers a file that no run can read. A name from a
client is reduced to letters, digits, ., - and _. That is not only about
the file system: the name is the last path segment of a URL on the node API,
that path travels raw inside a link frame, and Request::builder().uri()
refuses a space. Screen shot.png is what macOS names every screenshot.

The files sit outside the project, and the default permission mode refuses a
read outside the working directory, so an unattended job would block on a
permission prompt that nobody answers. Every run therefore gets --add-dir on
the card's attachment directory, and so does Jump in. A session that already
runs cannot be given the flag, so it asks once, and the prompt box says so.

kari removes the files it no longer needs, on the poll loop. An archive clears
them at once. A card marked done keeps them for attachment_keep_days, 7 by
default, because a done card can come back and a file that is gone cannot. A
directory whose card was deleted waits a day, so that the undo on the toast
still finds the files.

One fix came with this. A task card moved between the local node and a server
node was written with an empty id, because the split hub cleared the id and
restore_card keeps the id it is given. A second such move then wrote over
the first. The card now gets a new id.

Let a card be booked to run when the rate limit resets

The 5-hour window fills up, and the work that is left has to wait. Until now
the only answer was the planner, and the planner waits for a trigger. A user
who knew exactly what to run next had to come back at the reset and press
Start.

A card can now hold one booked run: a time, and an optional one-off prompt.
Press "Schedule" in the card drawer and pick the next reset of the 5-hour
window, the cycle after that, the next reset of the weekly window, or a time of
your own. Each button carries the time it would book. Text in the box at the
foot of the drawer goes with the booking as the one-off prompt. The card shows
a clock chip, the queue strip lists the booking in front of the planner's
steps, and the drawer cancels it.

The caller names a cycle, not a time, and the node resolves it from its own
rate-limit sample. The windows belong to the Claude Code account that node is
signed in to, so a phone that books a run on three nodes must not send one time
to all three. A sample can be older than the reset it names, so the resolver
steps forward one window at a time until the reset is ahead. Two minutes are
added, because resets_at is a report and not a promise. A node with no sample
says so instead of guessing: a guessed time would start the run into a window
that is still full, which is the failure the booking exists to avoid.

A booked run is a manual start with a delay, so the automation mode, the budget
and the fill ceiling do not gate it. Only the parallel cap does, and only by
making the run wait. Engine::schedule_tick reads the card rows on every
15-second poll and builds the board only when a booking is due. A run that
starts, or that cannot start, clears its booking and sends a notice. A booking
whose time passed more than 24 hours ago is dropped: a host that slept through
the reset still runs the card w...

Read more

kari v0.11.2

Choose a tag to compare

@github-actions github-actions released this 09 Sep 16:23
v0.11.2
9d9aea6

kari v0.11.2 — built from 9d9aea6.

This release stops autopilot from repeating a run.

Autopilot took one card again every 30 minutes, for as long as the trigger
held. The card had already run and its result waited in Review. Each repeat
forked the session, added a card to the board and spent quota on work that
was finished. A card now leaves the plan after one run and waits for you.

TL;DR

  • Stop autopilot from running the same card again every half hour
  • Keep the text when a send fails, and name a version skew

What changed

Stop autopilot from running the same card again every half hour

Autopilot ran one card 11 times in one day. Every run forked the session, so
the board grew a card for each fork, and each run spent quota on a
conversation that was already finished.

Two rules made the loop. An accepted plan stops being live 30 minutes after
it starts, and the panel clears. The planner then looks again, the
weekly-reset trigger is still true for 36 hours, and the card whose run
finished is still the top candidate, because the candidate filter dropped
only the states that wait for an answer: needs decision, needs approval,
working and done. A finished run gives the state validate, which the board
shows in Review. The planner took it again, and again.

The planner now leaves every card whose run ended. The card remembers the
outcome of its last run in last_job_state, so done, failed and stopped all
count, and the state validate is on the list of states that belong to the
user. A card leaves the pool after one run and waits for a person.

A manual move to Ready now settles the outcome of the last run, beside the
moves to Backlog and to Done that settled it before. That move is the way
back: drag a card out of Review onto Ready and the planner can take it again.

Keep the text when a send fails, and name a version skew

Two failure paths cost the user the message they typed.

The phone emptied the reply box before it started the request, and nothing
put the text back when the request failed. The desktop composer did the same.
A phone sends over a link that drops, so the cost fell on the user.

Act now resolves true or false, so a caller that holds typed text can wait
for the outcome. Both composers empty only after the send goes through. One
tested predicate, clearsBox, holds the rule: the send went through, and the
box still holds the text that went. Anything typed while the send was in
flight is a new message, and it stays. The phone also disables Send while one
send is in flight, so a slow link cannot take the message twice.

The other path was a bare 404. An older server does not have the route the
button calls, and the user read "404" with no idea what to do. Every handler
answers with a JSON error message and the router answers an unknown route
with an empty body, so an empty 404 on a /kari/v1/ path means one thing:
the far end runs an older kari. The shared error path says so now, and every
call gains it, not the send alone. A 404 that carries a message keeps it,
because a handler answered and that is not a skew.

Install

Download the .dmg for your Mac: aarch64 for Apple silicon, x64 for Intel.
The app is not signed. After you copy it to Applications, run:

xattr -dr com.apple.quarantine /Applications/kari.app

On Windows, run kari_0.11.2_x64-setup.exe. It installs for the current user,
because kari reads the Claude Code state of whoever is logged in. That installer is
not signed either, so SmartScreen asks once: More info, then Run anyway.

An installed kari updates itself to this release: it checks on start and every six hours,
writes the new version beside the running one and offers a restart. Settings, Updates has the switch.

Every host runs the headless node, so that its sessions stay on the board while no window is open.
A Mac takes kari-node-v0.11.2-aarch64-apple-darwin (or the x86_64 one),
a Linux host kari-node-v0.11.2-x86_64-unknown-linux-gnu.tar.gz,
a Windows host kari-node-v0.11.2-x86_64-pc-windows-msvc.zip.
Run kari-node service install to keep it running at login. One engine runs on a host at a time:
open the app and the node steps down, quit the app and it takes the host back.
A node updates itself only when asked: kari-node update, or serve --auto-update.

An Android phone installs kari-latest.apk. Add this repository to Obtainium: the asset name never
carries the version and the signing key never changes, so an update installs over the previous build.
The phone reaches the nodes over a private network, such as a VPN, and pairs with the code from
Settings, Nodes on the desktop. See TOUR.md.

See the README for requirements and setup.

kari v0.11.0

Choose a tag to compare

@github-actions github-actions released this 09 Sep 12:22
v0.11.0
e633694

kari v0.11.0 — built from e633694.

A card can now take its herdr tab with it. Turn on "Close the herdr tab when a
card is done or archived" in Settings, and the tab behind a card closes as soon
as you move the card to Done or archive it. The option is off by default,
because a tab can hold an agent that still works, and kari cannot open the tab
again.

The release also keeps the Windows node lint clean.

TL;DR

  • Let a card close its herdr tab when you retire it
  • Keep the Windows node lint clean

What changed

Let a card close its herdr tab when you retire it

A finished session leaves its herdr tab open. The card moves to Done, or into
the archive, and the pane stays in the workspace until somebody closes it by
hand. After a busy day that is a row of dead tabs.

Settings holds a new option: "Close the herdr tab when a card is done or
archived". While it is on, a move to a Done column closes the tab of the pane
that the board matched to the card. An archive does the same. The option is off
by default, because the tab can hold an agent that still works, and kari cannot
open that tab again.

The engine reads the tab from the board before it writes the card, because an
archived card leaves the board, and then nothing can name its pane. It closes
the tab after the write, so a move that fails leaves the terminal alone. The
engine of the node that owns the card does the work, and that node is the host
that runs herdr. A card on another machine therefore closes the right tab.

The two herdr options now sit in a herdr section of Settings. The launch target
was under Autopilot, where a reader who looks for herdr does not find it.

Keep the Windows node lint clean

The peer module imports Duration at the top of the file and uses it only
in the Unix sender. On Windows that sender is not compiled, so the import
is unused and the lint step of the Windows node job fails the build of
main. The import moves into the Unix function. The release builds were
not affected: they compile without the lint.

Install

Download the .dmg for your Mac: aarch64 for Apple silicon, x64 for Intel.
The app is not signed. After you copy it to Applications, run:

xattr -dr com.apple.quarantine /Applications/kari.app

On Windows, run kari_0.11.0_x64-setup.exe. It installs for the current user,
because kari reads the Claude Code state of whoever is logged in. That installer is
not signed either, so SmartScreen asks once: More info, then Run anyway.

An installed kari updates itself to this release: it checks on start and every six hours,
writes the new version beside the running one and offers a restart. Settings, Updates has the switch.

Every host runs the headless node, so that its sessions stay on the board while no window is open.
A Mac takes kari-node-v0.11.0-aarch64-apple-darwin (or the x86_64 one),
a Linux host kari-node-v0.11.0-x86_64-unknown-linux-gnu.tar.gz,
a Windows host kari-node-v0.11.0-x86_64-pc-windows-msvc.zip.
Run kari-node service install to keep it running at login. One engine runs on a host at a time:
open the app and the node steps down, quit the app and it takes the host back.
A node updates itself only when asked: kari-node update, or serve --auto-update.

An Android phone installs kari-latest.apk. Add this repository to Obtainium: the asset name never
carries the version and the signing key never changes, so an update installs over the previous build.
The phone reaches the nodes over a private network, such as a VPN, and pairs with the code from
Settings, Nodes on the desktop. See TOUR.md.

See the README for requirements and setup.

kari v0.10.1

Choose a tag to compare

@github-actions github-actions released this 09 Sep 08:47
v0.10.1
eb854c4

kari v0.10.1 — built from eb854c4.

The conversation of a card can open in a window of its own, with a prompt
box, so it stays in view while you work on the board. The node row of a
session card is one line again.

TL;DR

  • Open a conversation in its own window

What changed

Open a conversation in its own window

The conversation lives in the drawer, and the drawer closes when the board
needs the room. A long session is easier to read in a window that stays
open while you work elsewhere. The button beside "Show all" opens one: the
same turns, the same search, and a prompt box of its own that sends into
the running session or resumes it as a background job. One window per
card; a second click brings the open one to the front. The window follows
the board, so a new turn appears in it without a reload. In a browser
preview the same view opens in a new tab.

The node row of a session card said in a line of its own that the card
cannot move. The node name now carries that line as its hover text, so the
row is one line like the rest.

Install

Download the .dmg for your Mac: aarch64 for Apple silicon, x64 for Intel.
The app is not signed. After you copy it to Applications, run:

xattr -dr com.apple.quarantine /Applications/kari.app

On Windows, run kari_0.10.1_x64-setup.exe. It installs for the current user,
because kari reads the Claude Code state of whoever is logged in. That installer is
not signed either, so SmartScreen asks once: More info, then Run anyway.

An installed kari updates itself to this release: it checks on start and every six hours,
writes the new version beside the running one and offers a restart. Settings, Updates has the switch.

Every host runs the headless node, so that its sessions stay on the board while no window is open.
A Mac takes kari-node-v0.10.1-aarch64-apple-darwin (or the x86_64 one),
a Linux host kari-node-v0.10.1-x86_64-unknown-linux-gnu.tar.gz,
a Windows host kari-node-v0.10.1-x86_64-pc-windows-msvc.zip.
Run kari-node service install to keep it running at login. One engine runs on a host at a time:
open the app and the node steps down, quit the app and it takes the host back.
A node updates itself only when asked: kari-node update, or serve --auto-update.

An Android phone installs kari-latest.apk. Add this repository to Obtainium: the asset name never
carries the version and the signing key never changes, so an update installs over the previous build.
The phone reaches the nodes over a private network, such as a VPN, and pairs with the code from
Settings, Nodes on the desktop. See TOUR.md.

See the README for requirements and setup.

kari v0.10.0

Choose a tag to compare

@github-actions github-actions released this 09 Sep 08:37
v0.10.0
975c218

kari v0.10.0 — built from 975c218.

The card drawer is now the place to talk to a session. A prompt typed there
goes into the running session, so the terminal you look at answers it, and
a session that is not running resumes as a background job. The summary says
how far the work travelled: committed, pushed, PR open, merged, released,
deployed, CI passed or failed. The whole conversation is one click away,
with a search. On the phone, a swipe moves between columns and the app
opens on the Working column.

TL;DR

  • Swipe between columns on the phone, and open on the Working column
  • Rebuild the card drawer around the conversation
  • Say how far the work travelled in every summary
  • Hand a prompt to the running session instead of starting a copy
  • Line up the quota bars, and keep the answer to "May run unattended"

What changed

Swipe between columns on the phone, and open on the Working column

The phone board shows one column at a time, and the code comment promised
that a swipe moves between them. Only the arrows and the dots did, so
every swipe went nowhere and the thumb had to find the small arrow at the
top. A horizontal swipe over the column now turns the page. A scroll of
the list still scrolls: a move that goes more up than sideways is not a
swipe.

The app also opened on the inbox. A phone is taken out to see what runs,
so it opens on the board, on the column that holds the Working cards. The
inbox badge still says when something needs an answer. The column on
screen is remembered by id, so a reordered board keeps the page.

Rebuild the card drawer around the conversation

The drawer had grown by accretion. The project and the node appeared twice,
once as a fact and once as a field. The title sat in an input at the foot
of a long form, under a Save button that was easy to forget. Two empty
text areas took a screen each. And the way to talk to the session, the
one-off prompt, was the last box on the page and started a background job.

The drawer is now one page with the conversation at its heart:

  • Click the title to rename the card. The edit happens in place.
  • "Where it stands" opens the drawer: the summary, its delivery line, the
    next step, and the state of the background job with the reply it
    suggests. One click puts that reply in the prompt box.
  • "Card" lists facts and fields in one grid. Project, node, priority,
    model, permission mode, the scheduler prompt, notes: each saves when you
    leave it. There is no Save button and nothing to lose on a close.
  • Empty text areas are one line high and grow with the text.
  • "Conversation" shows the last prompt and reply, and "Show all" lists
    every prompt and reply of the session, oldest first, with a search.
    A long transcript loads its last 300 turns first.
  • The prompt box sits at the foot. Its button says what a send does: into
    the running session, a background job that resumes the session, or a
    background job that starts the task. Cmd+Enter sends.

Every toast about a card carries an Open button, so a "Done" or "Started"
notice is one click from the card it names. The phone uses the same drawer
and the same prompt box.

Say how far the work travelled in every summary

A summary said what a session was about and where it stood, but not
whether the result had left the machine. "The change is on the branch" and
"merged and released" read the same on the card, and the question that
matters most after a background run is exactly that one: committed, pushed,
a PR open, merged, released, deployed, and did CI pass.

The summary now carries a delivery line built from those words, and the
narrative ends with the latest state of the work. The excerpt that Haiku
reads gains a TOOL line for every git and gh command the session ran, so
the judgment rests on what happened and not only on what the reply said.
Those lines ride along with the last 30 messages and do not use up their
budget.

A background job also writes its own account of where it stands into its
state file: a detail line, what it waits for, and the reply it suggests.
kari reads the three, shows them on the card, and offers the suggested
reply as one click into the prompt box.

Hand a prompt to the running session instead of starting a copy

"Continue in bg" on a session that was open in a terminal did nothing that
the terminal could see. Claude Code refuses to run one session twice, so
claude --bg --resume started a copy of it, and the prompt landed in a
second transcript that nobody was looking at. The card then showed a
blocked job, and the only way to give the session its next step was to
type it in the terminal by hand. A reply from the phone took the same
path and failed the same way.

Every running Claude Code process opens a message socket for its peers,
and kari now uses it. The card drawer has a prompt box at its foot, and
the phone card keeps its reply box. A prompt goes into the running
session's own queue: an idle session answers at once, a busy one takes it
after the current turn, and the terminal shows both. A session that is
not running resumes as a background job with the prompt, as before, and a
task starts one. start_card refuses a running session outright, so the
scheduler and an older client cannot start a copy either.

The run log gets one line per prompt sent from kari, and a prompt that a
peer handed in counts as a turn of the session, so the card moves to
Working when kari speaks to it.

The transcript reader can now return the whole conversation of a session,
oldest first, with a limit and the total, for the conversation view in the
drawer. The node API, the hub and the server carry both calls, so a phone
that talks to a server reaches a session on any node.

Line up the quota bars, and keep the answer to "May run unattended"

The stats strip put each account on a row of its own, and each row set its
own widths. An account on two machines pushed its meters further right than
an account on one, so no bar started where the bar above it started. A window
with no reset time dropped the reset text, and moved the meter beside it.

Each line of rows is now one grid, and the rows share its tracks. The widest
name sets the name column for every row, and each part of a meter keeps its
box while it has nothing to show. So the bars stand in a column whatever the
text beside them says.

The New task dialog also forgot the "May run unattended" box between tasks.
It now opens with the answer you gave last, held on this device. A column
that takes Ready cards and no Backlog card still decides for itself, because
a card added at the foot of a column has to land in that column.

Install

Download the .dmg for your Mac: aarch64 for Apple silicon, x64 for Intel.
The app is not signed. After you copy it to Applications, run:

xattr -dr com.apple.quarantine /Applications/kari.app

On Windows, run kari_0.10.0_x64-setup.exe. It installs for the current user,
because kari reads the Claude Code state of whoever is logged in. That installer is
not signed either, so SmartScreen asks once: More info, then Run anyway.

An installed kari updates itself to this release: it checks on start and every six hours,
writes the new version beside the running one and offers a restart. Settings, Updates has the switch.

Every host runs the headless node, so that its sessions stay on the board while no window is open.
A Mac takes kari-node-v0.10.0-aarch64-apple-darwin (or the x86_64 one),
a Linux host kari-node-v0.10.0-x86_64-unknown-linux-gnu.tar.gz,
a Windows host kari-node-v0.10.0-x86_64-pc-windows-msvc.zip.
Run kari-node service install to keep it running at login. One engine runs on a host at a time:
open the app and the node steps down, quit the app and it takes the host back.
A node updates itself only when asked: kari-node update, or serve --auto-update.

An Android phone installs kari-latest.apk. Add this repository to Obtainium: the asset name never
carries the version and the signing key never changes, so an update installs over the previous build.
The phone reaches the nodes over a private network, such as a VPN, and pairs with the code from
Settings, Nodes on the desktop. See TOUR.md.

See the README for requirements and setup.