Skip to content

Changelog

The format follows Keep a Changelog, and the numbers follow semantic versioning.

While the major number is 0, a minor bump may break something. From 1.0.0 the usual promise applies: a patch fixes, a minor adds, a major breaks.

  • The example agent says it is running again after Resume. examples/agent.mjs used to work on with its card still reading paused and the resume word left on the run. It now reports running as soon as it sees Resume, as board.mjs pause does.
  • A chart on Home can count several projects. Add project in the chart picker adds another project, each with its own select and column. The chart draws one bar a day, the sum of them all, and hovering a day shows each project’s count by its key. Charts made before read as they did.
  • Home follows every project live. A task that moves, or an agent that starts, reports, falls silent or finishes, shows on Home’s cards and lists within seconds, without a reload. Home holds one connection for all your projects, reads once for a burst of changes, and reads nothing while its tab is hidden.
  • A self-hosted board answers at /api/health. It answers 200 while the app can reach the database and 503 when it cannot, with no sign-in and nothing about the board in the answer. The app service in docker-compose.prod.yml has a healthcheck that uses it.
  • The README opens with a clip of an agent at work. In 25 seconds an agent takes a card, walks its plan, pauses when a person asks, and gives the card back on Take over. The board screenshot is the seeded board of today, and scripts/record-demo.mjs records both again.
  • The README and the site lead with agents. The first lines say that agents work as members, a short section shows the commands that connect Claude Code, and “What it does” lists what ships today. Hosting names the version to pin, what to back up and what the image needs.
  • The site opens on a new landing page. It is dark, leads with the 41-second promo film, and says in its first line that the board is for humans and agents. The film plays muted and looping, and a still stands in for it when the system asks for less motion.
  • Home’s project cards and list rows agree about an agent nobody has heard from. A card used to drop a silent agent while a list row still showed it at work. Now the card says “Reis working · Crackerjack silent”, and the row names the agent without the green dot.

  • The API and configuration docs match the code. The configuration page lists the S3_*, ATTACHMENT_* and development variables. The counts of property types and avatar colours, the view kinds and the activity kinds are right. Files, ship, search, settings, the changelog and older runs have pages, and the API overview says why the rest have none.

  • An invite lets in only the person who received it. An invite is now a link that works once, for 7 days; the email carries it, and People shows it to copy. Signing up with an invited email but without the link joins no project, and on a closed board is refused. An invite made before this release opens nothing: People marks it “link expired”, and adding the email again sends a new link.

  • The docs say only what Ushabti does today. The backup recipe now copies the bucket the attachments live in, since a dump alone loses them. No page says a delete cannot be undone, that there are no sprints or no rate limit, or that webhooks are the owner’s alone. The roadmap has a real Next, and a conduct problem is reported by email.

  • A new project knows what done means. Its Status ends in Done, and Done when names it from the start, so Ship archives, a blocker frees and a parent counts its parts without a visit to Settings. Existing projects keep their columns and their Done when.
  • An agent may ship a release. Ship is a member’s now, on the column, on the roadmap row and through POST /api/options/{id}/ship with a token, so the agent that deployed ships the release and the changelog has its day. board.mjs ship "<release>" --rest next|leave|clear [--strays] ships one by name, and board.mjs release "<name>" --target <day> makes or dates one. Unship stays an admin’s.
  • Ship takes the finished tasks that are in no release. The ship question counts the tasks that are over and carry no release, and offers to take them with a tick that starts on. Taken, they get the release, are archived with the rest, and the changelog lists them. The route takes strays: true. A sprint close offers nothing, since a sprint archives nothing.
  • A release is made on the roadmap. A roadmap with no release says so and asks for a name and a target date; Enter makes it. A roadmap with releases ends with an Add a release row. The note under Use releases links to the roadmap. A sprint roadmap offers none, since its sprints come from the cadence. A first release with no start and no task now starts today, so it draws the moment it is made.
  • Ship sits on the roadmap row too. An open release with a target offers Ship on its row, to the same people and with the same question as the column header. When the project has no Done when, both say that nothing counts as over yet and point to Settings → Project, instead of offering to archive nothing.
  • The board read no longer carries the descriptions of archived tasks. A project with hundreds of them sent every one on every load and every change. Search still finds an archived task by a word in its description: the server looks, and the hit joins the bottom of the list a moment later, with the line it was found on.
  • A board reads itself once when its live connection opens, not twice. Every connect and reconnect downloaded the whole board two times and threw one copy away, which on a phone was slow at the moment the connection was busiest.
  • The API’s answers are compressed. The board read went over the wire as raw JSON, over half a megabyte on a working project, on every load and every change. It now goes gzip or Brotli when the browser or the agent accepts it, about a tenth of the size.
  • Uploads on the Docker dev stack survive a crash of the dev server. A crash stopped the app container, and RustFS stayed on the old container’s network, so every upload failed until somebody recreated it. The dev server now starts again inside the same container.
  • Home shows three projects to a row, as lists and charts do. The three sections share one column, so their cards line up, and a project card has more room.
  • A new list is picked, previewed and made on one page. Its projects are tiles; each picked one shows its rules and how many tasks it gives, and nothing is written until Create list. A list left unnamed is named after its projects.
  • A new chart shows labelled fields and a live preview. Its columns are pills, it starts on one you do not chart yet, and it draws the chart it will add before you add it. The same chart cannot be added twice.
  • A chart’s bars fill the width of its panel again. Since the project cards gained their own strip of days, a chart’s thirty bars stopped at 140px and left the right of the panel empty.
  • A task key on Home takes its project’s colour on hover. A key on a row of a list is grey at rest, and while the row is hovered or focused it takes the colour of the task’s project, so you see where a task lives as you reach for it.
  • A project card shows how busy it has been. A card on Home draws a small bar for each of the last fourteen days, cut in the project’s zone, with today in the accent and the number of changes beside them. A project with nothing in those days says how long it has been quiet.
  • A project card says where it is heading. Under the name, a card on Home shows the current release with its ship day and how many of its tasks are done, or the current sprint and the days it has left. A project with neither shows its newest change instead: who did what to which task, and the column it moved to.
  • A project has a colour. Each project gets one of ten colours, picked from its key, and every existing project gets one too. The owner or an admin changes it in Settings → Project, where each swatch shows the key on the colour. Home will draw projects by it next.
  • An empty Home says what to make first. With no projects, Home shows one panel that explains a project and makes one, while lists and charts say they wait for it. With a project and no lists or charts, each shows a wide panel that makes the first. Once a section holds one thing, its panel goes and the small button in the heading takes over.
  • Drag your projects into your own order. On Home, take a project card by the grip in its corner and drop it where you want it, or lift it with Space and move it with the arrow keys. The switcher and every list of projects follow. The order is yours: nobody else’s list moves. A project you make or join goes to the top. This release carries a migration, which keeps the order everybody sees today.
  • Read one day of a chart exactly. Point at a bar of a chart on Home and that day lights up, with a tooltip that names the day and its count. Tab to the chart and walk the days with the arrow keys, Home and End; a screen reader says each one.
  • A project card reads in rows that line up. Each card on Home opens with the project’s colour square and its key, then the name and your role. Its board is a bar of greys with every column named and counted, and its foot shows who is on it and which agent is working, or when it last changed. Cards side by side keep their rows level, the gear fades in on hover, and a project quiet for three weeks reads dimmed.
  • Home leads with your projects. Projects come first, then your lists and charts, in quiet outlined panels. Each heading has a small “+ New” button in place of the dashed card. Colour is kept for what needs you or is happening now: a chart’s bars are grey and only today is lit.
  • A list on Home shows its first tasks. Each list card names its first five tasks, in the list page’s order, and a click opens one on its board. “+N more” opens the whole list. The rules and the counts per project left the card; they are on the list page. Home is wider, so three lists sit across, and every list card is the same height.
  • A list always has a project. New list opens an editor that saves nothing until you pick a project; the list is made with it. Leave before that and no empty list is left on Home. The last project of a list cannot be removed: delete the list instead. POST /api/lists now needs a projectId.
  • A chart’s bars stay inside their panel. On Home the thirty bars ran out of the chart and over the next card, and an empty day drew as a full block. They now fill the panel’s width, and an empty day is a hairline.
  • Home fits a phone again. The fourteen day bars on a project card were each 32 pixels wide, so on a phone the card ran past the screen and the page scrolled sideways. They now share the width of the card.
  • A chart on Home counts the tasks that entered a column each day. Under Charts, pick a project, a select and one of its options in place. The chart draws the last 30 days in the project’s zone, with today’s number and the average a day. A task made in the column counts too. The feed now names a select’s option by id, and older lines get theirs once, by today’s name.

  • A search says how many tasks it found. When the list under the top-bar box shows fewer than it found, a line under it reads 12 of 60.

  • board.mjs update brings a copy up to date from its board. It fetches board.mjs and SKILL.md from /skill/ and says which version it had and which it has now; a failed download changes nothing. The skill also takes rename, check-add and check-set, and describe --replace writes over a description a person approved a rewrite of, after posting the old one as a comment.

  • The time zone is picked from a list. The box in Settings → Project offers the zone names as you type, so “Berlin” offers Europe/Berlin. A name off the list is still sent, and the server still decides.

  • Home holds your own lists of tasks from several projects. The project list is now called Home, and My lists sits above the projects. A list takes one or more projects, each with the board’s own filter chips, and shows the tasks that pass any of them, grouped by project. A row opens the task on its own board. Only you see your lists, and the switcher lists them under the projects.

  • An agent can remove or reword a checklist item, and the feed says so. board.mjs check --remove and --rename, or check-rm and check-edit by the number task now prints. A removed item leaves its words in the activity log, and a reworded one its old and new words, whoever did it.

  • The project list shows how every project is going. Each card draws its main board as a thin bar with a line of counts under it, as you would see it, filters and your own filters included. A column you folded on that board steps aside to the end of the line. The card says how many tasks wait for you, how many agents work and how many of them are silent, and what changed last. The waiting number there and in the switcher counts hand-overs too.
  • The changelog is there only while releases are on. With Use releases off, the top bar and Settings → Project offer no Changelog link, and the member and public pages are not found. Turn releases on again and the same entries are back.
  • The arrow keys keep the highlighted row in sight in a long list. The lists of the filter panel, the sort panel, Set…, the link picker and the top-bar search scroll with the highlight, so Enter never picks a row you cannot see.
  • The 0.21.0 image did not build. A test read a file the image leaves out. 0.21.1 is 0.21.0 with that mended; nothing else changed.
  • A view has its own link. The open view’s pill carries a small Copy link button, shown on hover and focus, and the address bar names the open view with ?view=. A link opens on that view, over the one this browser remembers, and does not change what it remembers.

  • A run’s whole log and every past run can be read. The Agent tab shows Show earlier lines on a run’s log until its first line, and the lines you opened stay while the agent writes new ones. Earlier runs says how many there are and shows twenty more at a press. Agents read the same through GET /api/runs/{id}?before= and GET /api/tasks/{id}/runs?before=.

  • An admin can delete any comment. The owner and admins see ✕ on every comment, not only their own, so a comment by the project or by somebody who left can be taken down too. Every delete now asks first, and names whose comment it is. The Activity tab says who deleted whose comment, never what it said.

  • The person picker has a Find box. Type a few letters of a name to narrow the list, and press Enter to pick. Your own name comes first, and the menu opens on it when the field is empty. The task panel, Set on picked cards and a type’s Starts as all work this way.

  • An admin can rename an agent. Open Face on the agent in Settings → People and change its name. Its comments, runs and activity show the new name; text already written, such as an old @mention or a log line, keeps the old one. Two agents of a project cannot share a name, in any case, because a mention finds an agent by it. A running board.mjs watch answers to the new name without a restart.

  • A view can show, filter and sort by when a task was made and last changed. Turn on Created or Updated in the card settings to put the day on every card and a column in a list; both stay off until somebody does. A filter asks is on, before, after or within today, this week, the last 7 or the last 30 days, read in the project’s time zone, so “Updated is before” a day a month ago finds the work nobody touched. A list or a board sorts by either, newest or oldest first.

  • A long title is cut to three lines on a card. One card no longer towers over its column. The whole title shows when you hover a card or a list row.
  • A watcher knows from the feed whether a change is for it. A value line now carries the property’s type, and a person line the personId it names; a created line carries assigneeIds. board.mjs watch decides from those, so a card dragged across a column no longer makes every agent read the whole board. A line from an older board still reads it. The panel’s activity names the person who was assigned, not their id.
  • The Iteration type reads Sprint. The type picker, the property row, the card settings, the errors and the docs say Sprint. The sprint length note names Close, the button a sprint has. The API still names the type iteration.
  • A task shows when it was made and when somebody last changed it. The panel reads “Made 3 Oct by Ana · changed 2 hours ago” under the title, with the full time in the project’s zone on hover. A comment, a checklist item, a link, a file, an archive and a put back now count as a change. A drag inside one column, a run report, a beat and a value a deleted option took away do not, so the top-bar search ranks by real work.
  • Done when can name more than one option. In Settings → Project, tick every option that means over, such as Done and Won’t do. A blocker in any of them frees what waits on it, and Ship, progress and parts count it as done. The API now answers doneWhen as { propertyId, optionIds }; a write may still send optionId.
  • An agent’s tokens can be told apart. Connect asks for a name for the token, filled in with today’s date. Each token row shows its name, its prefix and the day it was made. Revoking a token names it, and says the agent keeps working when it has other tokens.
  • A member adds a type as they add a property, and an agent adds no property. The Types page shows Add type to every person on the project, not only the owner and the admins. Deleting a property, an option or a type stays an admin’s. An agent token that tries to create a property gets 403 Only a person can do this.
  • Tasks of a person who left keep their name, marked as gone. A card, a list row and the panel show “Ada (left)” with a faded face, where they used to show nobody. A board grouped by a person gives them a column of their own until their tasks are handed on, so Unassigned on the board and in the filter mean the same thing. The filter lists them under Left the project; the picker shows them as the value but never offers them. Somebody who rejoins is a member again.
  • Releases and sprints are switches in Settings. Settings → Project has Use releases and Use sprints in place of Set up sprints. Use releases adds a dated property Release and a Roadmap on it; Use sprints asks for the length and the first day and makes what Set up sprints made. Off asks first, says what stays in numbers, and deletes nothing; on again makes nothing twice. The Options carry dates switch is gone from Properties, and a select dated before keeps its dates. A project that dated a select or had an iteration has its switches on after the upgrade.
  • Closing a sprint archives nothing; only shipping a release does. A sprint’s button is now Close: it ends the sprint and moves its unfinished tasks to the next one or leaves them, and the finished tasks stay on the board. A task in a closed sprint and an open release stays until the release ships. The changelog lists releases only, and a closed sprint’s line in the feed says closed where a release’s says shipped.
  • Shift-click picks from the open task. Click a card, then Shift-click another in the same column, and both are picked with every card between them. The same works in a list.
  • A sprint made by a late Ship starts today. It used to start the day after the last sprint’s end date, so a Ship three weeks late made a sprint that had already ended.
  • A roadmap bar with no start date begins after the previous one ends. It used to start at the oldest task under the option, so one old task moved into a release dragged its bar years back. Only the first dated option still starts at its oldest task.
  • A new property with more than 40 options is refused, never cut short. It used to keep the first 40 and drop the rest without a word. The box now takes one option per line, so a name may hold a comma, and says the limit before it sends.
  • The link box offers every task it can. Tasks already linked used to fill its twelve rows and hide one that could still be picked, so the box said “No task by that name” when there was one. An empty box no longer says that either.
  • Set and Archive work on any number of picked tasks. Past 200 they used to refuse the whole pick. They now go in batches of 200, one after another. If one is refused, the toast says how many went and how many did not, and after an archive only the tasks that did not go stay picked.
  • Settings opens without reading every task. It reads the project, its properties, views and people, and only the few tasks the card view preview draws. A change somebody else makes reloads only that, so Settings stays quick on a project of thousands of tasks.
  • A project tells its agents how to work on its board. An admin writes the agent rules in Settings → Project, in Markdown: which option means review, when to ask a person, what done means. An agent gets them when it claims a task, board.mjs claim prints them, and the watcher puts them into the prompt. Each change writes a line in the activity feed, and a webhook can ring for it with the new kind Agent rules (rules). The project carries a migration.
  • A sprint ends when somebody presses Ship. The board no longer closes a sprint by itself on its end date, and no longer archives or moves work on a timer. A sprint past its end stays open, and its column says “Ended 2 days ago” on a line below its name. Ship makes the next sprint when none is open after it. The cadence is only the sprint length now; “Sprints kept ahead” is gone. The project carries a migration that drops the two columns the roll kept, so 0.19.0 cannot run on a database 0.20.0 has opened.
  • Set… on a multi-select adds or takes off one option and keeps the rest. Setting Labels to bug on 30 selected tasks used to replace every task’s labels with bug alone, and said nothing. The bar now asks Add or Take off, shows how many of the selection carry each option, and says what a press will do before it does it. The bulk route takes "change": "add" or "remove".
  • A watcher keeps hearing new work after a write of more than 200 lines. Archiving a column of 300 cards, or an import of 500, left every listening agent deaf: the watcher read the same first page again and stopped. The activity feed now goes on from a page’s last line with afterId beside after, and the watcher pages through a burst of any length, even from an old --state file.
  • Removing an agent keeps what it did. Its past runs, with their plans and logs, stay on the Agent tab of every task it worked on, and its comments and activity keep its name. Only its tokens and its place in the project go, and the question in Settings now says so.
  • Settings on a phone shows that there is more. The menu fades at its right edge while more pages are off screen, and the page you open scrolls its own item into view. A webhook’s URL takes a line of its own, so the address is not cut. Field labels, menu items and member emails are easier to read.
  • The Project settings page has headings. Its cards sit under “Name and key”, “Dates and progress”, “Sprints”, “Sharing and export” and, last, “Danger zone”. A label such as “Count progress by” stays on one line, and “Make it public” is as wide as its words.
  • A property row reads cleanly in Settings. The “Shown when” line and a sprint’s length start under the property’s name and no longer touch the edge of the card. “Options carry dates” and “Shown when…” sit beside the delete ✕, and “Shown when…” is a real button. An option’s ✕ is bigger and easier to see.
  • A button in Views and Types looks like a button. “Make main”, “Card view…” and the actions of a type are quiet text buttons in sentence case, with a ring for the keyboard. They no longer read as the small grey capitals of a label. The groups “Every type” and “This type” read as the head of the card.
  • Settings draws its controls dark. The page now tells the browser it is dark, so a date box, a dropdown list and the file button no longer draw white. Settings has one checkbox, in the page’s own colours, with a ring for the keyboard and the words as a part of it. An empty date reads muted, so it no longer looks like a date.
  • A dropdown in Settings is the board’s own menu. It replaces the browser’s native list. It opens with Enter, Space or Down, walks with the arrows, jumps by the first letter, and closes with Esc. A card’s edge or the bottom of the window no longer cuts it, and two dropdowns in one card line up.
  • A type can say what a new task starts with. On the Types page, Starts as under a property sets a value for that type: a Bug starts with Severity Minor. The server writes it on every create, from the board, a list or an agent, and a value the create sends always wins. The composer says it first: starts with Severity Minor. A person never has a default.
  • A parent shows how many of its children are done. The card and the list row draw 3/5, as a bar or as a count, and the panel heading reads Children · 3 of 5 done. Done is the project’s Done when, and an archived child counts as done. Children is a row of the card view, so it can be moved or taken off.
  • Settings has a Types page. Name the select whose options are your task types, then press a type to see the properties it shows: those on every type, those on this type, and those on other types. Add to, Only on and Take off change a property’s Shown when rule, and a change that drops values asks first with the count. Agents read project.typeBy.
  • A task can have a parent, one level deep. Pick Part of… or Add a part… in the task menu. The panel shows a Parent row and a Children list, and each row opens its task. A part is never a blocker. Agents use PUT and DELETE /api/tasks/{id}/parent, or board.mjs parent and unparent, and the export carries each task’s parent.
  • A property can say when it shows. In Settings → Properties, Shown when… picks a select and its options: “Severity, shown when Type is Bug”. A task then draws that property — in the panel, on the card and in the list — only while its value of the select is one of them. The ✕ clears the rule.
  • A task that changes type drops the values its new type does not show. A hidden value no longer closes a blocker, passes a filter or leaves in the export. The panel, Set… and a rule written in Settings ask first and name what goes; a drag, an agent, an option delete and a ship drop at once. The task’s history says what went. The value, bulk and move routes answer with dropped, and board.mjs set prints it.
  • A heading in the description is bigger than its text. A # heading was the same size as body text and read as bold words. Now #, ## and ### step up in size, and the box and the page draw the same sizes.
  • The description box reads as the description does. Inline code wears the same box, a blank line takes the page’s gap between paragraphs, and a heading has the same space above and below. Put the cursor on a blank line and it is a full line again.
  • Running the dev server leaves the checkout clean. next-env.d.ts is no longer tracked. Next.js writes it on every dev start, build and typecheck, each a little differently, so a restart of the dev server for a new route left a change that nobody made.
  • Two headings with blank lines between them sit as far apart in the box as on the page. With two or more blank lines between them, they used to sit closer together in the box.
  • The description opens at once. A click on it used to wait about 300 ms for the editor, however short the text. Now the editor shows in the same frame once the panel has loaded it.
  • The task panel shows the activity of its own changes at once. A value set in the panel, and the values it dropped, used to reach Activity only after the panel was opened again.
  • Two “Shown when” rules written at once can no longer close a circle. The server now checks for a circle under the project lock, so one of the two writes is refused. After a rule is written, Settings reads the board again, so a rule that the write frees from a circle shows at once, and so do the values it drops.
  • The Docker dev server serves a route folder added while it runs. It answered 404 until a restart, because Docker Desktop does not pass the event for a new file into the container. A small watcher of src/app now restarts the dev server when a route file comes or goes.
  • A Blocked by or Blocks row opens its task. Press the key or the title, or reach it with Tab and press Enter, and the panel shows that task — an archived one too. The ✕ still only unlinks.
  • A task can hold files. Images and video go to an S3-compatible bucket — DigitalOcean Spaces, AWS S3 or RustFS — set with S3_BUCKET and its keys. A file is read only through the board, which checks that you are on the project. SVG and anything that is not an image or a video always downloads. An agent uploads with board.mjs attach USH-14 shot.png, which prints the Markdown line to paste. With no bucket set, Settings → Project says what to set.
  • Drop or paste a file into the description or a comment. The upload starts at once, a line says how far it got, and the line becomes the file’s Markdown when it is ready. An image shows in place, a video plays, and any other file is a link with its size. A comment waits to send until its files are in. The task panel lists the files under the description, newest first, and the uploader or an admin can remove one.
  • The description reads as it will look while you write it. Every line shows rendered Markdown but the line under the cursor, which shows its marks: headings, bold and italic, strikethrough with one tilde or two, code, links, task keys, images and video of the task, and task items whose box ticks. Tables stay as written. What is saved is still plain Markdown, so an agent reads exactly what you typed. Comments keep their plain box for now.
  • A search shows the most recent task first when two hits match equally well. A key match still comes first, then the title, then the description. Inside each, a live task comes before an archived one, and the task changed last comes first, so an old task no longer pushes this week’s work out of the twelve hits.
  • A search puts open work before done work that matches as well. Inside one match, an open task comes first, then a task that is done by Settings → Project → Done when, then an archived one. A key typed in full still opens its task, done or not.
  • docker compose up works again. The local object store is now RustFS, because the MinIO images can no longer be pulled. The server makes its bucket itself as it starts, so no helper container runs. Production is unchanged: any S3-compatible store still works.
  • A card of a shipped sprint keeps its sprint when you drag it inside “No open sprint”. The drag used to clear the sprint. Under a filter that a task with no sprint fails, that column now shows its cards but takes no card from another column and offers no “Add a task”, because the new card would vanish where it landed.
  • An edited comment is saved with an Update button. Under the edit box of your comment, Update saves the new words and Cancel puts the old ones back. A click away from the box no longer saves the comment, and leaves the box open with your words in it. Ctrl + Enter (⌘ + Enter on a Mac) still saves, and Esc still cancels.

  • The sprint picker opens on the current sprint. In the task panel and in the bulk edit, the menu of an iteration marks the sprint that holds today with the word “current” and opens with it highlighted. A board you group by an iteration starts with the filter “Sprint is current” when a sprint holds today, and you can remove that chip as any other. With no sprint on today, no filter is added, so the board keeps its columns.

  • Iteration, a property type for sprints. Its options always carry dates, so it has no switch, and it works everywhere a dated select does: board columns with the date, Ship, the filter “is current”, the roadmap and the changelog. Set up sprints now makes an iteration, and an existing Sprint select that carries dates becomes one.

  • A shipped sprint steps out of the way. A board grouped by an iteration draws no column for a shipped sprint, and the panel and the bulk edit offer only the open ones, plus the one a task already holds. The filter keeps the shipped ones behind one Shipped row, and Settings folds them behind one row that says how many, where Unship still works. A shipped version of a dated select stays where it is.

  • An iteration has a cadence that makes the next sprint. Set up sprints asks for the length in days and the first day, and makes Sprint 1 and Sprint 2. When a sprint ships, the next one is made, named and dated after the last, so Move to the next option always has somewhere to go. Settings shows the length and how many sprints wait ahead. You can still make, rename, date and delete a sprint by hand.

  • A sprint whose end has passed rolls by itself. Nobody has to press Ship on Monday. The next time somebody opens the board after a sprint’s target day, the tasks that are over are archived and the rest move to the next sprint. Each moved task gets a comment from the project: “Moved from Sprint 14 to Sprint 15 when Sprint 14 ended.” The changelog says Ended for that sprint and Shipped for one somebody shipped. A version or any other dated select still waits for a press.

  • A roadmap bar opens its tasks. Press a bar, or focus it and press Enter or Space, and the panel shows the option’s name, its dates, its note and its tasks in board order, through the view’s filters. A shipped option lists its archived tasks too, marked Archived. Each row opens its task; Escape or ✕ closes the panel and puts the focus back on the bar.

  • A select says whether its options carry dates. Settings → Properties has a checkbox Options carry dates beside the name of each select, off by default. Only then do its options show Start, Target, Note and Unship, and only then does a filter on it offer current. Turning it off hides the boxes and keeps the dates. Set up sprints turns it on for Sprint. Only the owner or an admin can turn it.

  • Sprints are set up in one step. Settings → Project has a row, Set up sprints. One press adds a select property Sprint, a board Sprint on Sprint is current, grouped as the main board is, and a list Backlog on Sprint is nothing yet. Each one is an ordinary property or view afterwards. The row says Sprints are set up once a property named Sprint exists. Only the owner or an admin can press it.

  • A roadmap view draws one bar per dated option. Make it from + in the view strip, pick Roadmap and a select property. Each option with a target date gets a row: its name, its colour, and a bar on a line of weeks from its start date, or its oldest task, to its target. The bar fills by the same progress the column header shows, under the view’s filters. A shipped option’s bar is full, ends on its shipped day and sits below the open ones. A line marks today, and the roadmap opens with today in view. The view’s kind is roadmap.

  • A changelog page lists the shipped options and their tasks. Changelog in the top bar, beside Archived, opens /p/{id}/changelog; on a phone the link is in Settings → Project. It lists every shipped option of every select property, newest first, with its shipped date, its note as markdown and the tasks that carry it in board order, archived ones included. Public changelog in Settings → Project is off by default; when an owner or an admin turns it on, /changelog/{key in lower case} answers without a session, with no keys, no people and no links into the board. Only one project per key can be public; a second one gets 409. GET /api/projects/{id}/changelog answers the same data to a token.

  • A dated column can ship. On a board grouped by a select property, an owner or an admin sees ✓ Ship in the header of a column whose option has a target date. The header asks the question in numbers: how many tasks are over and how many are not. It offers Move to the next option, Leave them or Clear the value for the ones that are not over. One press archives the tasks that are over, does what was picked with the rest, writes today as the option’s shipped date and folds the column. POST /api/options/{id}/ship takes { "rest": "next" | "leave" | "clear" }; a token gets 403. Unship in Settings clears the date and leaves the archived tasks archived.

  • A column header shows the target date and the progress of its option. On a board grouped by a select property, a column whose option has a target date shows it beside the count, and a thin bar under the header shows how much of the column is done by the project’s Done when rule. The view’s filters narrow the bar as they narrow the cards. A shipped date takes the target’s place. In Settings → Project, Count progress by picks one number property, such as Points; then the bar sums it and the header reads 21 of 34.

  • A filter can say that a sprint is current. A rule on a select property whose options carry dates offers Current, beside Nothing yet. It means the option whose start and target hold today, so a sprint board rolls over by itself on the day. A task added under the rule goes into the current sprint.

  • A view can carry a card view of its own. In Settings → Views, Card view… opens the card view editor for that one view. The first change gives the view a copy; until then it draws the project’s card view and follows it. Use the default throws the copy away. A list’s columns are its card view, so a list can now show the property a board groups by without putting it on every card.

  • A task can hold links, such as its pull requests. A new property type, Link, holds up to 20 web addresses. A pull request, an issue or a merge request reads as owner/repo#N on any host. In the panel each link opens in a new tab; paste one into the box to add it. A filter finds a link by its words, a sort orders by the first link, and a new Link property starts off the card. An agent writes them with board.mjs set and adds one with --add.

  • An option of a select can carry a start, a target, a shipped date and a note. A Version, a Sprint or a Quarter is an option with dates. Settings → Properties shows the start, the target and the note on each option of a single select, and the shipped date with an Unship when it is set. The option routes take startAt, targetAt, shippedAt and note, the board answer and the export carry them, and the skill’s props prints them. This carries a migration (0019_option_dates: four nullable columns on property_options).

  • A pull request puts its link on its task, from GitHub Actions. Copy examples/github/ushabti-links.yml into .github/workflows/. It finds the task keys in the title and the branch name of a pull request, never in the body, and adds the link to the task’s Pull requests property with an agent token. A key with no task writes a line in the job log and does not fail the job. The docs page Pull requests from GitHub says how to set it up.

  • A comment has no “Use as description” press. A comment is a comment, and the description is edited in its own box. The author of a comment still has Edit and Delete on it.
  • Only an admin, and only a person, can ship or unship an option. A write of an option’s shipped date from a member or from an agent token is now refused with 403. A member and an agent still write the name, the colour, the start, the target and the note.
  • The top bar shows how many tasks wait on a person. When an agent asks a question, N waiting appears beside the search box on every view, whatever filter is on. Press it for the list, the oldest ask first, with the agent, the question and how long it has waited. Pick a row and the task opens with the cursor in the answer box. The browser tab title starts with (N) too.
  • The project switcher says where an agent waits. Beside each project with a question open, the switcher and the All projects page show “N waiting”, by the rule of the top bar. The number also comes with GET /api/projects and with the project of GET /api/agent/me.
  • A question nobody answered is emailed once. When an agent has waited 15 minutes on a question and nobody has answered, the board sends one email with the project, the task, the agent, the question and a link that opens the task. It goes to the person the task is assigned to — the first person property in the Settings order — else the last person who changed the task, else the owners and admins. It never sends a second one. It needs mail and USHABTI_URL, as Forgot password? does, and anybody can turn it off on /account. The task carries a migration.
  • Any emoji can be a face. After the grid on /account and on an agent in Settings → People, a small box takes any one emoji, from the keyboard or the system picker. It saves as soon as it holds exactly one emoji. Other text saves nothing and says “One emoji only.”
  • A face and a chip read on every colour. Initials, the ◆ and the words on a filled chip now take the ink that APCA says reads better, which is light ink on most of the palette. Dark initials on pink are gone. An emoji face sits on a dark tint of its colour with a thin ring of the full colour, so a frog on green or fire on red reads. The swatches on /account and in Settings → People show the face as the board will draw it.
  • A person wears an emoji instead of their initials. /account has a Face row below Colour: Initials and a grid of about thirty emoji, saved on click. The emoji is drawn on the person’s colour everywhere their face shows. PATCH /api/auth/me takes emoji, which is exactly one emoji or null, and every member, comment author, activity actor and run agent carries it. The release adds one nullable column, users.avatar_emoji.
  • An admin changes the colour and the emoji of an agent. In Settings → People, Face on an agent’s row opens the same colour swatches and emoji grid as /account, saved on click. An agent with an emoji keeps a small ◆ badge, and a card or the assignee picker now draws an agent with ◆ rather than initials. PATCH /api/projects/{id}/agents/{agentId} takes color and emoji, for the owner and admins only.
  • A person who forgot their password gets a reset link by email. With mail on and USHABTI_URL set to the board’s public address, the sign-in page shows Forgot password?. /forgot asks for an email and always answers the same sentence, whether or not an account uses it; the link works once, for 24 hours. The link is built from USHABTI_URL, never from the request. POST /api/auth/forgot is limited per address and per typed email. With mail on and no USHABTI_URL, the server logs at start that the forgot page is off.

  • An admin downloads the whole project as one JSON file. Settings → Project has an Export row for the owner and the admins. Download saves ushabti-{KEY}-{YYYY-MM-DD}.json with the properties, views, members, invites and every live and archived task with its values, checklist, comments and blockers. No password, token, session, reset link or webhook is in it, and the activity feed is not. GET /api/projects/{id}/export answers it; a token gets 403.

  • The server emails the invite and the reset link. Set SMTP_URL and MAIL_FROM, and an invite to an email with no account, or a reset link made for a member, goes to that email as plain text. The page says “Emailed to …” or “Could not email …”, and the link stays on it either way. Both routes answer emailed: true | false. With neither variable set, nothing changes.

  • The author of a comment can edit it. Edit opens the comment as a box in place, and it saves as the description does. The comment keeps its place in the thread and reads edited beside its time. Only the author may: an owner or an admin can still delete another person’s comment, but not put words in it. PATCH /api/comments/{id} takes body and an optional baseBody, and the feed line is comment with action: "edited".

  • A task key in a description or a comment opens that task. USH-12, in any letter case, is a link when it names a task on the board or in the archive. A click opens the task in the panel, and the link is the task’s own address, so ⌘-click and a copied link work too. A key in code, in a link, of another project or of no task stays plain text. The saved text does not change.

  • An agent that refines a task gives it a new title. board.mjs retitle <key> "<title>" writes the whole title and never puts the agent’s own @Name in it. The skill makes a new title the first step of refining, with no exception for a title that looks clear enough, and the watcher’s default goal and its mention prompt say so too. Until now the skill said nothing about the title, so an agent kept a hurried title whenever its own name was not in it.

  • Options can be reordered in Settings. Drag an option by the grip on its chip, or lift it with Space and move it with the arrows. The order is the order of the columns and of a sort, so Priority can be put in order although no board is grouped by it.

  • The project name switches between projects. Press it in the top bar of the board, Settings or the Archive for a menu of your projects, the current one marked ✓, then New project and All projects. The arrow keys, Enter and Escape work, and past eight projects a box narrows the list. All projects left the user menu, and the name in the Settings and Archive bar is no longer a link to the board: Back to board is. On a phone the letter beside the name is the button.

  • The task panel and the view strip work with a keyboard and a screen reader. A task opens with the focus on its title, and closing the panel puts the focus back on its card. Each field is read with its property’s name, the menus say when they are open, the tabs are real tabs that the arrows walk, and an error toast is an alert. The view you are on is marked as the current one, and its pill no longer offers a keyboard drag that the strip cannot do.

  • A filter finds the tasks where an agent waits. Agent waiting sits beside Blocked in the filter list and keeps only the tasks whose open run asked a person something. A hand-over does not count. The card leaves when the agent takes the answer and goes back to work. A rule stores it as _agent_waiting.

  • A deleted task can be undone from its toast. The line after Delete task has an Undo button that puts the task back in its column and its place. It stays while the pointer or the focus is on it, and Tab reaches it without the toast taking the focus. The line still names the Archive, which keeps the task for thirty days.

  • A person filter can say Me. Me sits at the top of a person rule’s values and means whoever is reading, so one shared “Assignee is Me” view shows each person their own tasks. A task added under it is assigned to the person who added it. The rule stores "__me__", and an agent reads it as itself.

  • Deleting an option asks first, and names the cost. The ✕ on an option turns the row into a question with the number of tasks that lose it: “Delete Doing? 14 tasks lose it.” Reset to default in Card view asks first too, and says how many rows go back. GET /api/options/{id}/count gives the number.

  • An owner can make a person an admin. An admin adds and removes people and agents, issues and revokes tokens, sets up webhooks and imports, deletes properties, options and views, and changes the project’s settings. Three things stay the owner’s: deleting the project, handing it to somebody else, and changing an admin’s role. Pick a role from the select on a person’s row in Settings → People; “owner” asks first, because you become an admin. An agent is always a member.

  • A field says who is editing it. When somebody has the title, the description or a checklist item open, the others see one line under it: “Anna is editing the title”. It goes within a second or two of them leaving the field. It blocks nothing; you can still type and save.

  • The task panel shows who else has the task open. Their faces sit at the top of the panel, one for each person however many tabs they use, with the name on hover and on focus. A face comes within a second or two and goes when they close the task, or within a minute if their tab dies. Nothing is stored, and the other boards do not reload for it.

  • A listening agent says its name. Every agent in the top bar draws the same ◆, so pointing at one, or reaching it with Tab, now shows its name at once, under the face and inside the window, on a phone as well. A screen reader reads “Refiner is listening”. The slow browser title is gone from it.

  • You pick your colour from the whole palette. The Account page offers the eleven colours that options use, not eight, so each person on a team of six or more can have a colour of their own. Only the grey is left out, because it reads as “nobody”. The initials on an avatar, and an agent’s mark, now take a dark or a light ink, whichever reads on the colour. A colour you saved before still works.
  • A shared filter chip says that a change is for everyone. Open one of the view’s chips and its panel reads Changes this for everyone above the question, as its ✕ already asks Remove for everyone?. A chip of your own says nothing extra.
  • A sort names its two ways by what the column holds. A→Z and Z→A for a title, Earliest first and Latest first for a date, Option order and Reverse order for a select. Smallest first is left to numbers, and a checkbox runs Ticked first or Not ticked first. The Sort button, the list headings and the chip read one table, so they agree.
  • One word for each idea. An empty field in the task panel says what its column and a filter chip say — No due, Unassigned — and not Empty. Selecting cards says “select” throughout: the check reads Select and Deselect, and the ✕ reads Clear selection. The top bar link to the archive, the page it opens and the toast after a delete all say Archived, so the link no longer sits beside an Archive button that does something else, and an archived task comes back with Put back everywhere. The hint under the description and the comment box says ⌘ on a Mac and Ctrl everywhere else. The card view no longer tags the task rows “built in”; the Type column already says Task.
  • Settings has one place that says what a card shows. The On card ◉ / ○ switch left each row of Settings → Properties. Use Settings → Card view: it shows and hides every property, and it keeps the place a property had. showOnCard on PATCH /api/properties/{id} still works.
  • A view is made only from the + in the view strip. The “Add view” form at the foot of Settings → Views is gone; that page renames, regroups, reorders and deletes, and its note says where a new view comes from.
  • Properties have one order, set in Settings. Drag a property in Settings → Properties and it moves in the task panel, among the chips that share its place on a card, and among the columns of a list. Card view keeps only where a row sits and how it reads, so its ↑ and ↓ buttons are gone. The key, the title and the description stay before the properties, and the checklist and the comments after. A chip that is only a colour or a face still opens its place, so the priority square stays in front of the key. A card view saved with an order of its own now shows the Properties order, and the API ignores order in a card view.
  • A sort belongs to the person who picked it. Sort and a list heading now change only your screen, the way a filter you add does, and the strip says Only you see this. A sorted board holds still, so one person’s order no longer stops everybody else dragging inside a column. Save for everyone puts your order on the view with your filters. The lens PUT takes sort beside filters, and a view carries lensSort.
  • A task opens on the Agent tab while an agent works on it. A running or paused run shows first. A run that waits for an answer opens on Comments, because the question is a comment. A tab you pick stays while the agent reports.
  • The button that shares your filters now reads “Save for everyone”. It used to read “Put on the view”. On a phone it reads “Save for all”. It does the same thing: your filters become the view’s, and everybody sees them.
  • The box you type a new task into grows with the title. A long title shows in full, in a board column and at the end of a list, where it used to scroll inside a box two lines high.
  • The account page shows a password with the same eye as the sign-in page. Each password box has its own eye, where one worded button used to show both.
  • An invited person never gets an account without their projects. Sign-up writes the account, the memberships and the spent invites in one transaction. A failure used to leave an account with no project, and a second try then said the email already had an account. An invite sent during the sign-up is no longer lost.

  • USHABTI_SIGNUP, USHABTI_WEBHOOK_PRIVATE and DATABASE_POOL_MAX in .env now reach the server with docker-compose.prod.yml. Before, the file did not name them, so the server never saw them: sign-up stayed open with USHABTI_SIGNUP=closed, a private webhook stayed refused and the pool stayed at 12. If you closed sign-up this way, check it now: pull, run docker compose -f docker-compose.prod.yml up -d, and look for accounts you did not expect.

  • A value picked while a task is being archived stays on the panel. A read of the task that crossed the value’s save used to put the old value back until the panel was opened again. Now a read that crosses any of your own saves is thrown away and made again once the save is done.

  • A board read no longer fails with “terminated”. board.mjs sends a read once more when the connection drops halfway through the answer. It never sends a write twice.

  • A property never gets two options with the same name. The server refuses a second option, or a rename, whose name is already there in any letter case, and says which one: “Priority already has an option named High.” Two people, a quick double Enter or an agent can no longer make two “Blocked” columns.

  • Enter in a select picks an option and never makes one by mistake. When two options matched what you typed, Enter made a new option of the typed words, and on a board grouped by it a new column. Now the arrow keys move a highlight, Enter picks the highlighted row, and a new option comes only from its own Add row, which a name that is already there never shows.

  • A long line in a description or a comment wraps. Inline code, a long link and a code block used to make the text scroll sideways, which hid the end of the line. They now wrap as they do in the editor, on a phone too.

  • A text save no longer overwrites a change it did not see. When two people edit the same title, description or checklist item, the second save is refused instead of silently winning. The field asks in place: it shows the saved text above your words, with Keep mine and Take theirs. An agent may send the text it started from and gets a 409 the same way.

  • A password a password manager fills is no longer lost. A manager that writes the box without telling the page left it looking full, then emptied it on the next keystroke anywhere, and sign-in sent no password. Sign in, sign up, a reset link and the account page now read the box itself. The Change password button no longer greys out while a box looks empty; it says what is missing instead.

  • Typing @ shows who you can mention. A short list opens beside the box, under it or above it when the box is near the foot of the screen — in a comment, in the title and the description of the panel, and in the box that adds a task to a column or a list. The agents come first, with a word beside any that is listening at that moment, then the people. The letters after the @ narrow the list, the arrow keys move, Enter or Tab picks and Esc closes it. Picking writes the whole name, spaces and all, which is what an agent watching for its name reads.
  • The dev server can be reached by another name. It used to block its own live-reload resources for any host but localhost, so the page never came alive and the login form posted as plain HTML with no error. Set ALLOWED_DEV_ORIGINS to the names, comma separated, and those hosts work.

  • The Show button beside a password is an eye. It sits on sign-in, sign-up and the page a reset link opens, it is square and the height of the box, and it still answers to “Show the password” for a screen reader. The button itself was sound; a test now presses it on both forms and reads the field back, so a reveal that stops working is caught here and not in a browser.

  • A note you have not sent survives a closed tab. The comment box writes nothing until you send it, so there was nothing for the hook that saves a field on leave to send, and a long note typed into a tab that then closed was gone. The words now wait in your browser, under one key per task of one project, and the box has them again when you open the task in the same browser. Sending the note throws the draft away, and so does emptying the box; so does the board, once the task the note sits on is gone, which means a task you delete and put back comes back without it. Nothing about it reaches the server. The comment box is the only composer that keeps a draft: the ones that add a task or an option write what they hold the moment you click away, so words put back in one of them would make a task or an option by themselves.

  • A long board keeps its order. Adding tasks to the end of one board over and over used to lengthen the rank each task carries — one character every sixth task — until at about the 1,537th the search behind it gave up and stopped answering above the task before. From there the board read back in no order at all. The search now runs as far as the ranks it is given, and the end of a list is rewritten when a rank would reach 32 characters: one statement, about 256 rows, nothing that moves on screen. An old board with ranks already past the cap mends itself on the next task added to the end.

  • An edit is no longer lost when the tab is closed on it. A field saves on blur, and a tab closed while the field still has the focus sends no blur, so the words went nowhere. One hook, useSaveOnLeave, sends what the blur would have sent on pagehide, with keepalive so the browser finishes it after the page is gone. It is not an autosave: it sends only what you typed in that tab, so a tab left open on a page somebody else changed puts nothing back. The task title, the description, a checklist item, the project name, key and time zone, a property name, an option name, a view name, a webhook address, the answer you type into the filter panel and your own name all use it. The value boxes of the task panel are the exception: one of them still loses what you typed if the tab goes while it has the focus.

  • A dying heartbeat no longer closes a hand-over. board.mjs beat reports the run lost when it is killed with the session, so a card comes back the moment an agent dies. A worker that ended with finish --to "review", or with ask, left its run waiting on purpose — and the beat, killed half a second later, wrote “the agent was stopped” over it. Both doors are shut now: the beat and the watcher read the run once and say nothing when it is no longer running, and PATCH /api/runs/{id} refuses lost on a run that waits with 409 That run waits on purpose, so a lost report cannot end it. The feed line also names the author of lost at last — “shut down, and the run ended with it” where an agent said goodbye, and “stopped answering, so the board closed the run” where the lease acted.

  • Write an agent’s name where a task is born and it wakes. The watcher’s mention used to hear only a comment. It now also hears @Name in the title of a task a person creates, and in a title or a description a person changes, so a task can be handed to an agent as it is written. The prompt says where the name is, and an agent may take its own name out again with board.mjs unmention <key> once it has done what was asked. Only a person’s words wake it: an agent’s own task, edit or tidy-up wakes nobody.

  • A board comes in from Trello. Settings → Import, owner only, takes a Trello JSON export: lists become the columns, cards become tasks in the order they had, with their labels, due dates, first member as assignee, checklists and comments. The page is the flow — pick a file, read the preview with its counts and its mapping, change where any list or label lands, then press Import. A name that matches an option the board already has is proposed against it; a property the file needs and the project has not got is made. Archived cards stay behind unless you say otherwise, and what will not come — attachments, custom fields, start dates, an archived list, everything in the history that is not a comment — is counted in a sentence before you press anything. Up to 5 MB and 2000 cards, written under one project lock in batches of 500. The same file twice makes nothing twice, and a webhook rings once for the whole import. Trello only; Jira is its own job.

  • A date rule in a filter can name a window of days, and stays true tomorrow. is within sits among the date operators and swaps the date box for a list of words: Today, Tomorrow, This week, Next week, The last 7 days, The last 30 days, The next 7 days, The next 30 days and Overdue. A week runs Monday to Sunday. The chip reads the way a person would — Due this week, Due in the next 7 days, and Overdue on its own. Overdue is before today and nothing else: nothing on a task is a fixed field, so the board cannot know what done means; say “late and still open” with a second rule. A task added under a relative rule is written with no date, because which day inside the window you meant is a guess. Which “today” is the project’s, not the reader’s: a new Time zone row in Settings → Project (owner only, saving on blur, refusing a name the server does not know) says where the day comes from, and it is UTC until somebody changes it. The server works the day out when it reads the board and sends it with the answer, so no browser and no agent ever reads its own clock and everybody on one view sees one week. This carries a migration (time_zone on projects): run npm run db:migrate, which the image does on every start.

  • A task can say what it waits on. It used to live in a comment, where nothing could read it. A task now names the tasks that block it, and the card wears one small grey chain beside its key — nothing more, because a board where half the cards are waiting still has to be readable. The panel holds the two short lists, Blocked by and Blocks, adding by the same box the filter uses and removing with a ✕ that asks nothing; on a task with no links yet the way in is ⋯ → Blocked by…, so a task without them looks exactly as it did. Blocked joins the filter as a fixed word rather than a property. A circle is refused with one sentence — USH-12 already waits on USH-71, so this would be a circle. — worked out under the project lock. POST /api/tasks/{id}/blockers and DELETE /api/tasks/{id}/blockers/{id} both take an agent token, and board.mjs link USH-71 --blocked-by USH-12, unlink and task are the short way. A blocker stops blocking when it is over: archived, and also the one option the owner names under Done when in Settings → Project, which falls back to archived by itself when that property or that option is deleted. This carries a migration (the task_links table and done_when on projects): run npm run db:migrate, which the image does on every start.

  • A board reads on a phone. Below 560 px the board draws one column the width of the screen and stops scrolling sideways, with a strip of column pills above it: a dot, the name and the count, the one on screen filled. A pill, a 60 px sideways swipe and the left and right arrow keys all reach another column, and all three move the same one thing. A board opens on the first column every time. There is no drag down here — a card moves by the grouping property in its panel, or by Set… on several at once — no column drag and no folding, and a fold made on a wider window is ignored rather than cleared. The check that picks a card is drawn without waiting for a hover wherever there is no hover to wait for. Above 560 px nothing changes.

  • A deleted task can come back for thirty days. A delete now marks the task instead of taking it away: it leaves every board, list, search and count at once, and every route about it answers 404. The delete says so in one line — USH-14 deleted. Put it back from the Archive within 30 days. — with no button on it, because the way back must not live in something that goes in five seconds. The archive page gained a second list, Deleted, gone in 30 days, newest first, with the days left and one Put back that returns the task whole, with the key it had. A task deleted while it was archived comes back archived. After thirty days a sweep takes the row for good, and it runs on a delete and on a read of that list, because there is no timer. DELETE /api/tasks/{id} answers {"ok": true, "goesAt": "…"}, and there are two new routes, POST /api/tasks/{id}/restore and GET /api/projects/{id}/deleted, both of which take an agent token. This carries a migration (deleted_at on tasks and its index): run npm run db:migrate, which the image does on every start.

  • A big board mends its ranks in a moment. The rewrite used to try one reach after another and write out every one of them in full, only to measure it and throw it away — 3.4 s of that on a board of 40,000 tasks, all of it while the project was locked and nobody else could add a task. It now works out how long a reach would be without building it, and builds once: 9 ms for the same board.

  • The end to end suite refuses a BASE_URL on a bare IP. A production build sets the session cookie Secure, and Playwright’s request context will not send a Secure cookie to an IP address, so the specs that read the API that way used to fail one by one with nothing that named the cause. playwright.config.ts now stops at once and says to use localhost.

  • The top bar stays inside itself while cards are picked. With a long project name and a long person’s name the bar ran up to 174 px off its own side between 560 px and 900 px, and the box that finds a task was squeezed to a border and its padding. Below 900 px the box and the two links to other pages now give their room to what is picked, as the project name already did; above it the person’s name shortens like every other name on the bar. The box keeps a floor of 88 px at every width it is drawn at, instead of losing 23 px of it on one pixel of window at 561 px.

  • A small tablet keeps the names in the top bar. The project name and your own name used to go at 560 px, where the bar still had 118 px of room. They go at 520 px now, which is where the room runs out, and between 520 px and 560 px the box that finds a task shortens first rather than the names.

  • The deleted activity line says which way round it went. kind stays deleted for a delete and for a put back, and data now carries action — deleted or restored — beside key, title and goesAt, which is null on a put back. A webhook receiver watching deleted now hears a task coming back as well and has to read the action.

  • Pick several cards and set one property on all of them. Hover a card and a check appears in its corner; x picks the one the cursor is on, and Shift-click picks a run inside one column. A bar in the top bar says how many are picked and offers Set…, which asks which property and then what about it, and writes every picked card in one call — POST /api/projects/{projectId}/tasks/values, which an agent may make too. Escape, the ✕ or a change of view ends it.

  • Archive the tasks you picked, and pick on a list. The bar gained Archive, which asks in the bar itself — Archive 3 tasks? — and then takes them off the board in one call and ends the pick. A list picks the same way: the check sits in the gutter before the key rather than as a column of its own, x picks the row the cursor is on, and Shift-click picks a real range down the whole list, because a list is one column of rows. POST /api/projects/{projectId}/archive now takes taskIds beside the property and value it already swept a column by. It stays a person’s route, and the count it answers is how many tasks really went, so archiving a task that is already archived costs nothing.

  • A page lists the archived tasks. Archive, in the top bar beside Settings, opens every archived task of the project, newest first, with the key, the title and when it went. Put back on a row returns one to the rank it never lost. The key and the title open the task, and a box narrows the list by key and title. A search and the task’s own link were the two ways back to one before this, which is enough to find a task you can name and nothing for one you cannot. To make room for the link, a screen 520 px wide or narrower now leaves out the project name in the board’s top bar — the mark beside it says the same thing — and your own name beside your avatar, on every page that draws the user menu.

  • A run can end by handing over. A worker whose pull request is open has finished its session, but not the task, and the card used to go quiet while a reviewer worked. board.mjs finish USH-14 --to "review" ends the session and leaves the run waiting instead: the card reads Waiting for review and says how long it has waited, the board leaves it alone exactly as it leaves a run that asked a person a question, and the Agent tab says who has it. The next agent that claims the task closes the hand-over as done and opens its own run, so one task still holds one open run. Take over ends it as it ends any other. Nobody is told and nothing is reserved: <who> is a word on the card, not a member of the project. No migration.

  • A long step no longer reads as a dead agent. An agent can say how long its next word will take — step USH-14 --say "Running the suite" --for 45 — and the board waits that long, up to an hour, before it closes the run as lost. Its next report puts the ordinary thirty minutes back. A heartbeat still cannot hold a card open. This release adds a column to agent_runs, so run npm run db:migrate before you start it.

  • A webhook rings when something changes. An agent waits on the stream, but a serverless function, a CI job or a chat bot has nowhere to wait from, so it had to poll. Settings → Webhooks is a new page, the owner’s alone: give a URL, copy the secret it shows you once, and press Send a test to watch the row say delivered 2s ago. Pick which kinds ring it — the words the activity feed already uses — or leave it at Everything. What arrives is the doorbell the stream rings, written down: the kind, the task key and the moment, and never the change itself, so the receiver reads the board for the rest. Each body carries X-Ushabti-Signature: sha256=…, an HMAC-SHA256 over the timestamp and the body, and docs/webhooks.md has a receiver’s check in ten lines of Node with a known answer to test it against. A failed try is made again after a minute, five minutes and half an hour, and then dropped — the feed is the way to catch up. Nothing waits for the network: a change writes one row per webhook and returns, so a receiver that is down costs one INSERT and slows nobody’s board. Roll the secret makes another one, once. The last twenty deliveries of each webhook are kept. A webhook cannot be pointed at a loopback, private or link-local address, or at a URL carrying a name and password: a board that called those would tell its owner what else is on the network it runs in. The address is checked when it is saved and again before every try, resolving the name each time, and the refusal is a plain sentence in the row. Set USHABTI_WEBHOOK_PRIVATE=1 on the server when the receiver really is on that network.

  • Archiving a whole column now writes its lines through the one funnel. It wrote them straight to the table, which was invisible until there was something hanging on the funnel: a swept column rang no webhook. The rows are the same; the road is the one every other change takes.

  • A run that is over can still be read. The Agent tab of a task used to exist only while an agent was working, and a run took its plan and its log away with it when it closed. The tab now stays for as long as the task has ever had a run. Under the open one — or alone, with nothing pulsing — it lists the runs that are over, newest first: who, when it started, how long it ran, how it ended and what it set out to do. Press a row and that run’s plan and log open in place, as they were left. Nothing changed on the card. An agent reads the same list in pastRuns on GET /api/tasks/{taskId}, and one of those runs in full at GET /api/runs/{runId}.

  • A board can be sorted, as a list can. A Sort button beside Filter opens a list of everything a card carries: press one and every column draws its cards in that order, press it again to turn the order around, and a third time for the order the board itself keeps. The chip the list already drew is now drawn on a board too, and its ✕ is the other way back. A sort writes nothing, so while one is on a card cannot be dragged to another place inside its column — it can still be carried to another column, which writes that column’s value and leaves every rank alone.

  • An agent can tick a checklist item. board.mjs check USH-14 "the item" added one and nothing ticked it, so an agent that met an acceptance criterion had to call the API by hand. --done now ticks the item those words name — the whole text, or one part of it that fits only that item — and --undone puts it back. Words that fit nothing, or that fit two items, tick nothing and print the checklist instead, because a tick on the wrong criterion says work is finished that nobody has done.

  • A filter you add is yours; one press puts it on the view. A rule you add through the Filter pill narrows your screen and nobody else’s. It is saved against you and that view, so it survives a reload and follows you to another browser. A view may still carry rules of its own, which everybody sees; the board shows those plus yours, and yours only ever narrow further. The chip row says which is which: the view’s chips, a thin divider, then yours, Clear, and a muted tail — Only you see this — Put on the view. Put on the view copies your rules onto the view for the whole team in one write, and any member may press it. Removing one of the view’s chips asks in the chip itself first: Remove for everyone? On a phone the row scrolls sideways and the tail shortens. An agent has no rules of its own and reads only the view’s, which is what the team shares. This answers the fault where a member who filtered to their own name re-filtered the board for everybody, live.

  • There is a way back into an account with a forgotten password. There is no email in Ushabti, so the way back is a person who can vouch for you: the owner of a project you are in. Settings → People now carries Reset password on a member’s row. It asks first — “Make a reset link for Ada? It signs them out everywhere once used.” — and then shows one link, with a copy button, readable there and never again. The owner sends it by whatever channel the team already has. The person opens it, types one new password, and is signed in; every other session of that account ends in the same breath. A link works once, lasts 24 hours, and is put out of use by a newer one for the same person. Used, old, replaced or invented, it says one sentence — “This link does not work any more. Ask the owner of your project for a new one.” — and never whether an account exists. The token is stored as a digest, exactly as an agent token is, and carries its own prefix, ushr_, so a leaked link and an agent token are told apart at a glance. Ten dead links from one address in ten minutes are answered 429, counted on the page as well as on the route. Only a person, and only the owner, can make one: an agent can neither make a link nor use one.

  • A task can be archived, and put back. Archive task in the task panel takes a card off every board and every list and keeps everything on it: the values, the checklist, the comments, the history and the link. The panel then carries one row — “Archived 3 days ago · Put it back” — and putting it back returns it to the rank it never lost. A search still finds an archived task and says archived on the row, which with its link is the way back to one. ↓ on a column header archives everything in that column at once, after a question in the header that names the count, and the board says how many went. Archive is now the everyday way to make a task go away; delete stays for a mistake. An agent may archive the task it finished — board.mjs archive USH-14 — but only a person can sweep a column.

  • A guessing attack is slowed. Ten failures inside ten minutes are answered 429 with a Retry-After header and one sentence the sign-in and sign-up forms show inline: “Too many tries. Wait 8 minutes and try again.” Sign-in is counted per address and per email, sign-up per address, and a bad agent token per address, which covers every JSON route at once. Only failures count: a sign-in that works counts nothing and clears the count for that email, and a token that works is never counted and never slowed. The count lives in the memory of one process, which is enough for the one server the image is built for; the self-host page says so.

  • A column can be folded to a strip. The « in a column header turns it into a narrow strip that still shows its name and its count; pressing the strip opens it again. A card dragged onto a strip lands at the end of that column, and the arrow keys step over it as they step over an empty one. The fold is kept in your browser, per view, so it survives a reload and changes nothing for anybody else.

  • The owner can invite a person who has no account yet. Adding an unknown email in Settings → People remembers it as an invite; the person joins the project the moment they sign up with it, through a closed board too. The row shows invited until then, and the owner can withdraw it. Before, a closed board told the owner “ask them to register” and the person “ask the owner”.

  • n makes a new task. On a board it opens the composer at the top of the column the cursor is in, or the first column; on a list, at the end. It stays out of a field you are typing in and out of the task panel.

  • An agent can wait for work. board.mjs watch holds the board’s stream open and starts a harness session — Claude Code, Codex, pi, OpenCode, any command that takes a prompt and exits — when a task is assigned to the agent, when a person mentions it in a comment, or, if you ask for it, when a person creates a task. It claims the task first, so the card shows life within a second, and it stops the session when somebody takes the card over.

  • The board says which agents are listening. An agent holding the stream open wears a ringed avatar in the top bar, and its token reads “listening now” in Settings → People. An agent that is away draws nothing.

  • An agent can ask a question and wait. Its run turns to waiting: the card shows the question and how long it has waited, the Agent tab says to answer in a comment, and the comment box says whom you answer. The board never closes a waiting run for silence. The answer wakes the agent again.

  • A comment can become the description. An agent that refines a task a person already described posts its draft as a comment; “Use as description” makes it the description, and asks first when that replaces words.

  • An activity feed for agents. GET /api/projects/{id}/activity?after=… returns what happened after a moment, so an agent that was away catches up.

  • board.mjs can check, describe and ask. describe refuses to write over a description a person wrote.

  • npm run start now serves the build the image serves. It was next start, which printed “next start does not work with output: standalone” and then served a slightly different server. It now runs node .next/standalone/server.js, the one the image runs, after copying the static files where that server looks for them. PORT picks the port and HOST the address, which is every interface unless you say otherwise. One thing to know: that server carries the copy of .env the build made, not the file on disk, so edit .env and you must build again or pass the setting on the command line.
  • A row of a task’s run history is lighter, and the list is ordered by when each run started. pastRuns on GET /api/tasks/{taskId} no longer carries stepsTotal, stepsDone or lastLog: nothing drew them, and filling them read the plan and a log line of twenty runs on every task read. A whole run, counts and plan and log, is still GET /api/runs/{runId}.
  • A property is dragged into place in Settings → Properties. Each row has a grip on its left, as a view’s row does. Space lifts the row, the arrows move it, Space puts it down. The ↑ and ↓ buttons are gone: the grip is the same route by keyboard, and two ways to do one thing is one too many.
  • A run history row no longer says the board closed a run it did not. lost is written by two different things: the lease, when a run has said nothing for thirty minutes, and an agent itself, as its last message when it is being shut down. The row read both as the first, so a run whose agent was killed six minutes in read as one that had stopped answering for half an hour. A row whose run ended inside its lease now reads shut down; one the lease really closed still reads lost. It is worked out from the two moments the row already carries, so old runs read right too and nothing has to be written again.

  • / reaches the search box while cards are picked. Below 900 px the pick bar takes the box off the top bar, and the key went on asking for a box nobody could see, so it did nothing at all. It now leaves every picked card out and puts the cursor in the box, on one press: a search hides nothing and ends by opening one task. The check in a list gutter also holds the 24 px square a finger needs where there is no hover, as the check on a card does; the check itself is the 14 px it was.

  • An id that is not a UUID answers 400, not 500. Every id column is a UUID, so Postgres refused to cast a word like not-a-uuid and the refusal reached the caller as 500 Something went wrong on the server. — our fault, for a request that was merely wrong. One helper now reads the shape before the database sees it, and every route that takes an id goes through it, in the path and in a body alike: 400 That is not a task id., with run, view, project, property and the rest in place of task. Upper case passes, because Postgres reads a UUID either way. 404 still means a real id that names nothing.

  • Escape in the task title throws the edit away. Escape was meant to drop what you had typed, but it blurred the box, and a blur is what saves a field. The old title came back on screen while the new one went to the server, so the words returned on the next read. Escape now writes nothing and the title stays as it was. The description editor already behaved, and a test now holds both to it.

  • Settings → Views fits a phone. On a narrow screen a view row put everything on one line, so a name box fell to 18 px and the word in it was cut. The row now uses two lines, as a property row does: the grip, the name and the row’s buttons on the first, Shows as and Columns by on the second. The colour swatch and the ✕ of an option, 11 and 15 px, now hold a 24 px square for a finger; the colour and the mark stay about the size they were.

  • The grips in Settings are easy to catch on a phone. The grip on a property row and on a view row was 13x20 px, under the 24 px a finger needs, so a miss landed in the name box. The button is now a 24x24 px square. The six dots did not change.

  • board.mjs check reads the flag in either place, and refuses an empty item. check USH-14 --done "the item" gave the item to the flag and then said there was no item at all, so an agent had to know that flags come last. A switch — --done, --undone, --held, --free, --once — no longer takes the next word. And a term of only spaces matched every item, so on a one-item checklist it ticked that item without being asked; it is now refused like a missing one.

  • A reset link now leaves a record. The row that holds a link is swept away as soon as the link is spent or replaced, so afterwards nothing said that the owner had ever handed out a way into somebody’s account. Making a link now writes a line on the project’s activity — who made it, and who it was for. No screen draws it yet; an agent, or anything that reads the API, gets it from GET /api/projects/{id}/activity. The link itself is never in it.

  • A property you change on an archived task stays changed. An archived task has no card, so its panel draws its values from its own read of the task. The change was saved, and the screen then went back to the old value, which read as a click that did nothing. The panel now keeps that answer up to date, as the board does for a card.

  • A value the server refuses goes off the screen again. The panel draws a value the moment you pick it, and the board puts itself right when the write is refused. The panel did not, so on an archived task — where the panel is the only place a value is drawn — the message said the change had not saved while the new value sat there. The panel now reads the task again when a write is refused, so the value you see is the value that is kept.

  • An open task panel follows what somebody else does to it. A task another person archived, or put back, while your panel was open left the panel as it was: no archived row, and Archive task still in its menu until you loaded the page again. The panel now reads the archived word from the board, which the stream keeps fresh, so it arrives with every other change. A link to an archived task also drew nothing until its own read landed; the key, the title and the archived row are now there at once, with Loading under them.

  • Archiving a task twice keeps the moment it first went. A second Archive on an archived task used to write the moment again and add a second line to its history, so a retry lost the day it really left the board. Putting a live task back wrote a line for nothing in the same way. Both now say what the task should be rather than what to do to it: a call that changes nothing answers the same and writes nothing at all.

  • The board no longer counts every value on every read. The number the question before a property delete names — “5 options and 42 values go with it.” — rode on every board load, for every property, for a number that is read once a month. The question now asks for it when you press the row, and says Counting what goes with it… for the moment it takes.

  • A handful of smaller ones. A board read makes one round trip fewer; a used or expired password reset link is swept away when the next one is made, instead of sitting in the table for ever; and two end-to-end tests that could fail a correct build no longer can.

  • A rule of yours can no longer contradict the view’s. Picking a property the view already filters used to start a second, blank rule beside it: answering it emptied the board with two chips that fought each other and nothing that said why, and Put on the view then handed both to the team. The panel now says it where you pick the property — The view already filters Priority. Remove it for everyone first — and writes nothing. Put on the view says the same and keeps your rules where they are. The way out is the ✕ on the view’s own chip, which asks for everybody first.

  • The filters you add hold still when one is refused. Writing a rule through the API about a property the view already filters is now refused there too, with the same sentence, so no screen can be narrowed past the way back. A refused Put on the view no longer shows the contradicting board for a moment before it snaps back: the board waits for the answer, and says what the answer was. Escape out of Remove for everyone? puts the focus back on the chip you pressed, so the next Tab carries on from the row instead of the top of the page.

  • A pause completes. board.mjs pause reports that the agent stopped, waits, and reports that it runs again when a person presses Resume. The shipped client could not send the status that clears a pause, so the panel said “It answers on its next report” for ever.

  • The stream says ready only once it is listening. It used to send ready before it subscribed, so a change in that gap was never announced to a client that read the board on ready.

  • A comment reads like a description. It is markdown too, drawn by the same renderer: a list is a list, code is code, and a long note has paragraphs. Nothing changes about what is stored, so every comment already written reads better the moment the panel opens. The composer says so under the box.
  • No env file reaches the image. Next copies .env and .env.production into the standalone output, and only .env was ignored. A .env.production sitting beside the Dockerfile shipped in the public image. Every .env* is now out of the build context and out of the repository, except the example.
  • A missing DATABASE_URL says so at the start. In production the server fell back to the development URL and died later on a refused connection, which named the port and never the cause. It now stops with DATABASE_URL is not set. The build still passes with no database at all.
  • Find a task from the top bar. A box beside the project name answers as you type: by the key a person reads on a card — DP-4, or just 4 — by words in the title, and by words in the description. Press / from anywhere on the board to reach it, the arrows to move down the hits, Enter to open one. A hit found only in a description carries the line it was found on, so a row always says why it is a hit.
  • A search reaches past the filter. It looks at every task in the project, because it hides nothing and ends by opening one task. A hit the view is not drawing says not in this view on its own row, so a key somebody sent you still opens on a filtered board instead of quietly finding nothing.
  • The views can be put in order. Drag a pill along the strip at the top of the board, or a row by its grip on the settings page: it is one order, so moving a view in either place moves it in both. The settings page answers the keyboard as well — Space lifts a row, the arrows move it, Space puts it down.
  • Any view can be made the main one. The settings page gives the word to another view in one click. The main view is the one a board opens on when nobody has picked one, and the one view that cannot be deleted, so the word is given rather than taken away: naming a new main view takes it off the old one. Only the owner can, as with deleting a view.
  • A view can be a list. A view was always a board grouped by one property. It now has a kind: a board, which is what every view was, or a list, which draws one row for each task in the one order every view shares. Choose it when you make a view — the + in the view strip asks “Shows as” before it asks what the columns are — or change it later in Settings → Views. A list pill carries three lines where a board pill carries a dot.
  • The list is where the card order lives. Every view in a project shares one order, and until now no screen showed it: a board only ever draws part of it, so nobody could see the order they were all sharing. Drag a row, or lift it with Space and move it with the arrows, and the boards move with it.
  • The columns of a list are what a card carries. They come from Settings → Card view — the same rows, in the same order, drawn by the same chips. Take a property off the card and it leaves the list in the same breath. The key and the title open every row and stay put while a wide list scrolls sideways; a row that has no value for a column draws nothing, exactly as a card does.
  • A list can be made on a project with nothing to group by. A board is its columns and cannot be, which used to make the + a dead end on such a project.
  • Press a heading to order a list by it. Once for smallest first, again for largest first, a third time for the order the board itself keeps. A select orders by its own option order — Urgent above Low, not alphabetically between High and Medium — a multi-select by its highest option, a checklist by how far along it is, and a number by its size rather than its digits. A task holding nothing for that column goes last whichever way the order runs, and two that compare the same keep the rank they had.
  • The order is the view’s, so everybody looking at that view sees it, and it survives a reload. It writes nothing: the rank each task carries is untouched, so every board goes on showing what it showed before.
  • A sorted list cannot be dragged, and says so. A chip above the list names the column, and its ✕ gives back the order — and the drag — in one click.
  • Changing a view between a board and a list asks nothing and loses nothing. A board keeps the property it grouped by, unread, so turning it back restores the same columns.
  • A property can be deleted while a list remembers it. Only a board is counted now, because only a board reads one.
  • A colour-only chip carries its name in a list. On a card the title above it says what it belongs to and room is scarce; a column’s heading names the property, never the value, so a row of bare squares says only that the task has one.
  • Adding a task to a filtered list fills in what the filter asks for, including the property a board would have left to its columns.

This release adds a column to views, so run npm run db:migrate before you start the new image — the published image runs its migrations itself. Every view you already have reads as a board, so nothing on your screen changes until you make a list or turn one of them into a list.

  • A card view page in settings. …/settings/card says what a card on the board carries. Every row is one thing a card could hold — your properties, plus the five parts a task has of its own: its ID, its title, its description, its checklist and its comment count. Open a row and say where it sits — the edge stripe, either end of the header, the body, either end of the footer, or nowhere — and how it reads: a colour, a name, both, a face, a boxed word, one line or two. It saves as you click, and the card beside the rows is the board’s own card drawing your own tasks, so the preview cannot drift from the board.
  • An edge stripe. A band of colour down the left of a card, one property at a time, and only a property that has colours of its own.
  • The description can sit on a card, one line or two. It never could before.
  • PATCH /api/projects/{projectId}/card-view writes the whole card view, or null to go back to the default. People only: an agent that lost its token would otherwise rearrange every card on the board.
  • The task panel is as wide as you drag it. Its left edge is the handle — the arrow keys move it too — and the width it is left at is the width every task opens at, on this browser. A strip of the board always stays, whatever the window.
  • A task’s link carries the key on its card. ?task=DP-4, in place of a uuid nobody can read or say out loud. A link written before this still opens its task.
  • The task panel wears the colour of the card it opens: the edge stripe, or the first colour the card leads with, asked of the card view rather than chosen by the panel. Move the edge and the panel moves with it. The priority is no longer said again in a pill at the top of the panel — it is a property row there, like every other field.
  • “On card” on the Properties page now writes the card view. It is the short answer to the same question: off the card, or back where its type belongs. showOnCard on PATCH /api/properties/{propertyId} still works and means exactly that. Nothing writes config.showOnCard any more; an older project’s value seeds the card it starts with.
  • A card no longer decides anything for itself. It used to pick a lead select, draw multi-select values as dots and drop the grouping property. Those are now rows in the card view, and a new project starts with exactly the card the board drew before. One change is visible: the grouping property of a view no longer appears on the card when you switch to another view. It is off the card until somebody puts it on.
  • A click made in the first moment after a board loaded could be undone. The event stream asks for the board the instant it connects, and that answer was still in flight when the click wrote. It landed afterwards and put the board back as it was, with nothing on screen to say so. A read that started before a write of this tab’s own is now thrown away.
  • Every count on a card read zero until the card was opened. The board counts comments and checklist items in a subquery, and the task it counted for was written without naming its table. Inside the subquery that bare name belongs to the other table, so the count asked whether a comment is its own task and answered zero, every time. Opening a task then handed the real numbers to the card behind the panel, which is why the number looked like it only arrived on open. The project list counted its tasks and its people the same way and showed zero for both. The end to end test that covered this passed all along: it read the card the panel had already corrected. It now reads the board the server draws.
  • The comment count on a card is drawn, not typed. The emoji is gone. The count carries a hairline speech bubble, eleven pixels of inline SVG in the same muted mono as the checklist ratio beside it.
  • The development server listens on 3050. 3000 is a busy port on a working machine. npm run dev, the compose file, the dev image and the Playwright default moved together, so development holds one number. Nothing changed for a running instance: the shipped image, npm run start and the production compose still listen on 3000, and so does CI.
  • The README points at the documentation site from the top, next to what Ushabti is, instead of only from a badge and the last section.
  • A column full of tasks scrolls instead of squashing its cards. Past about a dozen cards every card in that column was pressed flat until the titles were unreadable. The column body always could scroll; a card clips its own content, which let the layout shrink it to nothing first and never reach the scroll.
  • A documentation site, at jaklimoff.github.io/ushabti. The guides, the whole JSON API and the agent protocol, in one place and searchable. It covers what the README has never had room for: every property type and what it holds, the whole keyboard model, what a filter does to the columns, every route with its fields and its real error messages, and the run protocol an agent has to speak — claim, report, beat, close.

    It lives in website/ and is built by GitHub Actions on every push to main. This file, the roadmap, the contributing guide and the security policy are read from the repository at build time, so a release note is written once and never drifts.

  • Filters inside a view. A view can now show only some of its tasks. The Filter button in the view strip asks which property, and the rule it makes appears as a chip on a line of its own under the strip — a line that exists only while the view is filtered, so an unfiltered board looks exactly as it did. Click the chip to change the rule, press its ✕ to remove it, or Clear to remove them all. Every property type can be asked about, because no field on a task is hardcoded: an option, a member, a word in some text, a number over or under, a date before or after, and “empty” for anything that can hold nothing. Every rule has to pass; a filter narrows, it never widens.

    The rules belong to the view and save the moment you make them, the same way the grouping property already does. The strip counts what you are looking at — 12 of 40 tasks — so a board can never quietly be a part of itself.

    Adding one is two answers in a panel that does not move: which property, then what about it. Picking the property writes nothing — the board cannot know which priority you meant, so it does not guess one and hide half the cards while you decide. The panel stays open after the first value, because a rule usually names more than one; ‹ goes back, and Escape puts the panel away and keeps what you typed, because nothing in this product has a Cancel.

    Five things a filter is usually allowed to get wrong, and does not here:

    • “is not” keeps the empties. “Priority is not High” shows a task with no priority, because a task with no priority is not High. Jira drops those and hands people an incomplete board they believe is complete.
    • A card added under a filter is not hidden by it. The composer fills in what the filter asks for and says so before you press Enter — Enter to add · sets Priority Urgent.
    • A column you cannot drop into is not drawn. A rule about the grouping property takes its columns with it, so a card can never vanish where it landed. A new column made under such a rule joins the rule instead of disappearing the moment it is named.
    • A rule whose property or option was deleted goes with it. Nothing tidies a view up when a property goes, so the rules are read afresh every time. A filter nobody can see never keeps hiding cards.
    • A question with no answer is not a rule. It lives in the panel until it means something, so nobody else on the board sees a half-made filter, and taking the answer back takes the rule with it.
  • A column cannot be dragged while a rule hides its neighbours. The order of the columns belongs to the property and everybody shares it, and a drop can only name the column it landed after. Through a filter that would rank the option after a column somebody else cannot see, and move it on their board too. You cannot reorder a list you are only shown part of.
  • The board, with the keyboard alone. One card carries the cursor and is the board’s only tab stop, so Tab reaches the board in one press instead of one press for every card. The arrow keys move the cursor from card to card and across columns, holding its place in the column and stepping over a column with no cards; Home and End reach the ends of a column; Enter opens the card. Space still lifts it, and while a card is lifted the arrow keys belong to the drag, as before. The card you are on now says so: it wore no ring at all, and on a board of forty cards the keyboard drag was a promise nobody could find.
  • A link to a task, in one click. The key at the top of the panel — USH-14 — copies the link to that task, and the ⋯ menu says Copy link for anyone who does not think to click a label. The link opens the board with the task open, which the address bar already did; nothing told a person that, and nobody copies an address bar with a drag on a task board.
  • A lifted card no longer jumps over an empty column. 0.2.1 fixed this for the pointer and said the keyboard was fixed with it. Only half of it was. The drop target is one decision and where the arrow key puts the card is another, and the second one still asked dnd-kit, which scores by the distance between corners: an empty column is as tall as the board, so its two bottom corners sit far below and a small card one column further over won the sum. Sideways the board now works the columns out for itself — the nearest one that clears the card, cards or no cards — and keeps the height the card was at.
  • A run that stops answering closes itself. A killed agent used to leave a card reading “active” for ever, because the server cannot see another machine and nothing ever wrote that run again. Now the board counts the agent’s reports: after six minutes of silence the card says how long ago the agent last spoke instead of how long it has worked, and after thirty it closes the run as lost and gives the task back. Only Take over could clear a dead run before.
  • A heartbeat, so that a long build is not a dead agent. Run board.mjs beat USH-14 & beside the work and it sends { "beat": true } every two minutes. A beat says the process is alive and writes nothing else — no step, no log, and not the clock the thirty minutes counts, so a heartbeat left running by a killed session can never hold a card open. An agent that beats without reporting reads as quiet; one that does neither reads as silent. Killed with its session, the heartbeat closes the run on the way out.
  • An account page, at /account, reached from the menu behind your avatar. Change your name, pick your colour from the palette, and change your password. It also counts your other live sessions and can end them all, which — with no password reset in the product — is the only lever a person has after a password they no longer trust. Nothing about a person could be changed before: a name typed once at registration was permanent.
  • Settings is four pages behind a rail — Properties, Views, People, Project — each with its own address. docs/agents.md and the shipped skill both told people to open “Settings → Agents”, which was not a place: it was a section two and a half screens down a single scroll.
  • Settings → People → Connect replaces “Issue token”. It hands over the token with a copy button and the three commands that put it to work, each already carrying this board’s own address, and then says Waiting for the first call… until the token is used. The skill is served from the board itself at /skill/SKILL.md and /skill/board.mjs, so the instruction is true for anybody running the image rather than the repository.
  • Settings → Views can create a view. The section named Views could rename, regroup and delete one, but not make one.
  • A new board says where its columns come from, and stops saying it as soon as there is a task.
  • Loading states for the board, the project list, settings and the account page. There were none; a slow query looked like a click that had not landed.
  • USHABTI_SIGNUP=closed stops an instance taking new accounts.
  • components/ui/ — Button, Input, Field, Card, Row, Tag, Toasts, EmptyState, ConfirmRow, CopyField, Skeleton, StatusPage — behind a token scale for control height, radius, type and space. The same button used to be declared three times at three geometries, and the same input at 28, 30 and 34 px.
  • Agents. A project can now have machine members. An agent is a member like any other: it holds a person property, writes comments and appears in the activity log. Two things are its own — it signs in with a token instead of a password, and while it works it opens a run on a task.
    • Settings → Agents. The owner creates an agent, issues a token and revokes it. The plain text of a token is shown once and stored as a digest.
    • The whole JSON API accepts a token. Authorization: Bearer ush_… works on every route the browser uses, so an agent reads and writes exactly what a person can, in the project the token belongs to and no other.
    • A run makes the work visible. While a run is open the card carries a strip along its bottom: the agent’s name, the line it last reported, the time it has been going, and a bar that scans while it lives. The task panel grows an Agent tab beside Comments and Activity — its dot pulses while the run is live — holding the plan with its steps, the run log and the buttons.
    • Pause, Stop and Take over. Pause and Stop are requests: the agent reads them in the answer to its next report and obeys. Take over is not a request — it ends the run at once and gives the card back. Dragging a card an agent holds takes it over as well.
    • docs/agents.md has every call, and examples/agent.mjs is a working agent in about a hundred lines.
    • A skill, in examples/skill/ushabti/. Copy it into .claude/skills/ and Claude Code can work on the board: list, claim, step, set, comment, finish. It takes property and option names, not ids, and refuses an unknown one with the real choices, so an agent cannot bake a board’s ids into itself.
  • A cleanup workflow. It drops the caches of a pull request when the pull request closes, drops caches that nothing has read for a fortnight, and drops container versions that no tag can reach. Nothing that carries a tag is touched.
  • The image is 308 MB, down from 775 MB, and the two architectures build side by side on runners of their own architecture instead of one runner emulating the other. The tag build took 9m42s, of which 8m58s was the emulator.
  • The end to end tests now run against the production build, not next dev. CI already made that build and then threw it away, so the tests never touched what the image ships, and every route paid for its first compile inside a test. The step is 24s where it was 86s, and e2e/global-setup.ts, whose only job was warming those compiles, is gone.
  • Playwright’s browser is cached between runs, keyed on the Playwright version.
  • CodeQL runs on main and weekly, not on every pull request. It takes about seventy seconds and has never held a change back.
  • The image build writes mode=min build cache, scoped per architecture, where it wrote mode=max for both. max stored every layer of every build stage and filled the whole 10 GB repository cache in a day, which evicted the npm cache and then evicted itself.
  • The settings page showed no errors at all. It called notify() on eight failure paths and never rendered the toasts, so adding a member by an email nobody had registered — which is how it always goes the first time, because there are no invites — did nothing visible whatsoever. The error now appears, and for that case the sign-up link to send them appears with it.
  • An agent token could take a board apart. DELETE on a property, an option or a view asked only for membership, so any token could delete a field and every value in it. Those three, and the routes that add and remove members, are now the owner’s and a person’s alone.
  • An agent could clear a pause a person had asked for. POST /api/runs/{id}/control took a token, so an agent could write resume on its own run and the log would read as though a person had. Only a person may write a control word now.
  • Six controls destroyed data on one click. Deleting a property, an option, a view, a member, an agent or a token now asks in its own row and says what goes with it: “Delete Labels? 5 options and 14 values go with it.” Deleting a project asks you to type its key.
  • Changing the project key silently renamed every task. Task keys are built from the prefix, so it broke every pasted link and every key an agent had been given. It now says how many tasks it is about to rename.
  • A live board lost changes made while it was still hydrating. Server-sent events have no replay, so anything broadcast between the server render and the stream opening was gone for good. The board now re-syncs whenever the stream connects, which also covers a reconnect after a network blip or a laptop waking up.
  • The first-run copy on the projects page had never been seen by anybody. It rendered only when the new-project form was closed, and the form opens itself when you have no projects.
  • A mistyped password at registration was unrecoverable. The field has a reveal, and says plainly that there is no reset.
  • Reordering a property refetched the whole board on every press, six times to move a row six places.
  • The new-view panel opened at the far left of the strip however far right the + had moved, and views scrolled out of sight with no edge fade.
  • The delete control for a view appeared on the pill you had just clicked to select it, with no confirmation. It lives in Settings → Views now.
  • The user menu had no route to your account, and Sign out sat directly beside a link that only navigates.
  • not-found.tsx told everybody who mistyped a URL to go and ask a colleague for access. error.tsx offered one button and no way out, and threw away the digest that finds the error in the log. Both, plus a new global-error.tsx, now share one look with the rest of the app.
  • Settings wrapped rather than laid out below 620 px, dropping the arrows to a second line and stranding the card toggle on the first.
  • Option colours opened the operating system’s colour wheel, ignoring the palette the rest of the product picks from.
  • Every browser tab said “Ushabti”, and there was no favicon.
  • The show-on-card control was an unlabelled ◉, and the only route to project settings was a 26 px ⚙.

This release adds a column to agent_runs, so run npm run db:migrate before you start the new image. Any run already open whose agent last reported more than thirty minutes ago closes as lost the first time somebody loads that board, which is the point of the change.

  • A card can now be dropped into an empty column. The drop target was chosen by the distance between corners, and a column drop zone is as tall as the board, so two of its corners sit far below the card and any small card in the column next door won the sum. A column with cards of its own still took the drop through one of them; an empty one had nothing to win with. The target under the pointer now wins, and rectangle overlap comes next for the keyboard, which has no pointer and had the same fault. Dropping on the free space below the cards sends the card to the end of that column.
  • A note being written in a task no longer disappears. The panel rebuilt its loader whenever the board re-rendered, and the effect that reads the task cleared the panel each time the loader changed. That unmounted the comment list and took the half-written note with it. Anything that re-renders the board did it: a change made by another person, or the live connection reporting that it is up. The panel now clears only when it moves to a different task, and it also stops re-reading the task on every render.
  • The board no longer disappears when a card carries a Due date. The server wrote the date with the locale of the Node process and the browser wrote it with its own, so a browser set to British English read “28 Aug” where the server had sent “Aug 28”. React saw the two texts disagree and threw the server’s board away. The month names are now written out in the code, so both sides produce the same text.
  • DATABASE_POOL_MAX. One process held up to twelve connections and there was no way to say otherwise. That is fine for a database of Ushabti’s own, and wrong on a managed cluster shared with other applications, where the whole server may allow only 25. The default has not changed.

node-postgres verifies the certificate when the connection string says sslmode=require, which is stricter than libpq and stricter than most other drivers. On DigitalOcean and similar, pass the provider’s CA and ask for sslmode=verify-full&sslrootcert=/path/to/ca.crt.

The first release.

  • Email and password sign-in. Projects with members, added by email.
  • Custom properties: select, multi-select, person, text, number, date and checkbox. No field on a task is hardcoded, not even Status.
  • Views. Each one is a board grouped by a select, person or checkbox property.
  • Drag and drop for cards and for columns, with the pointer or the keyboard.
  • Task detail: markdown description, checklist, comments and an activity log.
  • Live updates over server-sent events.
  • Docker Compose for development, a production image on Docker Hub and on the GitHub registry, and migrations that run when the container starts.
  • 14 unit tests and 18 end-to-end tests, run on every pull request together with ESLint, Prettier, the types, the production build and a CodeQL scan.

Next.js 16 with Turbopack, React 19, PostgreSQL 18 with Drizzle, Node 24.

  • Eight warnings from react-hooks/set-state-in-effect. The pattern works but is worth removing.
  • One card order for all views, no rate limit on sign-in, no password reset. The end of ROADMAP.md has the full list.