Elyra
Elyra The coding agent eTerm The terminal that knows where each command ends Starf An activity monitor for Apple silicon that never invents a number e The native code editor Elyra Grove Native local development environment Askr The real server for Laravel & PHP Elyra Framework Rust + Svelte 5 framework for desktop apps Elyra Conductor Local project conductor Elyra SQL Server MySQL-compatible SQL server in Rust Elyra Félagi Agents as teammates on one board Elyra SQL Client Native desktop SQL workbench Elyra SQL Anywhere Replication-ready SQL engine Elyra Sjá SEO & GEO workspace for macOS Elyra DataGrid Server-driven data grid for Laravel
Start here
Concepts
Release notes
What's new
Elyra
What's new in Félagi

What's new in Félagi

An overview of new features and improvements, newest first.


Version 0.91.8

Date: 16 September 2026

Fixed: an issue key in a URL now opens the issue

/app/issues/POS-336 used to answer 404. Only the row id worked, so a link built from the identifier — the thing every other system knows an issue by — went nowhere.

Tip: This came in from the helpdesk, whose notification emails link to the issue they pushed. It is worth more than that one email: the identifier is what the API returns, what a commit message quotes and what somebody reads out in a standup. Anywhere you can copy POS-336, you can now paste it after /app/issues/.

Tip: It redirects to the canonical address rather than rendering a second copy of the page. One address per issue, so a link somebody copies out of the bar afterwards is the same link every other part of Félagi writes.

Tip: The obvious repair was to link by the issue number instead, and it would have been a silent bug. The URL takes a row id; number is the workspace's own counter. On the installation that reported this they agree for 343 issues out of 351 — so it would have tested clean, worked for months, and opened the wrong issue eight times. On a second workspace they agree almost never.

Tip: An unknown prefix is refused rather than ignored. XYZ-231 is a 404, not issue 231 — otherwise whoever followed the link would be reading an issue they did not ask for and would have no way to tell.

Tip: A key from another workspace is a 404 too, exactly as a row id from another workspace has always been. Nothing about what you can reach has widened; only how you can spell it.

Tip: The API reference now says this outright, and says not to build a link out of number.


Version 0.91.7

Date: 16 September 2026

Fixed: a card now stays where you drop it

Dropping a card into the middle of a board column did nothing. It slid back to where it started the moment you let go, and the order it settled into survived a reload — so it looked like the board had decided for you.

Tip: Top and bottom always worked, which is why this lasted. Positions are fractional: dropping at the head writes one less than the first card, at the foot one more than the last, and in between the midpoint of the two neighbours. The first two produce a number nothing else has. The midpoint does not — every issue is created at position zero, so in a column nobody had ever dragged in, the midpoint of two neighbours was zero as well. The card was written with the position it already had, the tiebreak decided, and nothing moved.

Tip: So the two places anybody tries first are the two that were never broken. A test asking only "does the card move" would have passed on both.

Tip: Dropping a card now renumbers the column rather than squeezing a value in between two others. A column is tens of cards and the page has just loaded every one of them to draw it, so a few writes per drag is a fair price for an order that cannot collapse into a tie — which the fractional scheme would have reached on its own eventually, halving the gap until two doubles stopped being different numbers.

Tip: On a filtered board the index you drop at counts the cards you can see. The visible neighbour decides the spot and the whole column decides the numbering, so the cards the filter is hiding keep their relative order around the one you moved instead of being shuffled through each other.

Tip: The board and the drop arithmetic were already sorting by the same rule — position, then newest first. What looked like two rules was one rule with nothing to sort by: in an untouched column every position is zero and the tiebreak does all the work. After a drag that column has real positions and the tiebreak never fires again.

Tip: Verified in a browser as well as in tests. A card at the top of a column, dropped at the third slot, comes to rest at the third slot and is still there after a reload.


Version 0.91.6

Date: 15 September 2026

Two things that were counting the wrong thing: the untouched badge counted weekends, and a meeting had two text fields where everybody used one.

Untouched counts working days now

The badge on a started issue said untouched 3d after three calendar days. Now it waits for three working days.

Tip: The failure it fixes is the ordinary one: somebody works on Friday, is asked about it on Monday, and the board says they have been silent for three days. One working day of silence, reported as three — and the person being asked knows it is wrong, which is how a badge stops being read.

Tip: Weekends, the twelve Norwegian public holidays, and 24 and 31 December are all free days. The last two are not public holidays in law, which is exactly why they had to be added by hand: chasing somebody on Christmas Eve for not having touched an issue is wrong whatever the statute says about the date.

Tip: Over Christmas and Easter the old count accused most of the board of having stopped. Ten of the twelve holidays move with Easter, so they are computed rather than listed — a table would be a list somebody has to extend every few years, and the year it runs out is the year every Good Friday becomes a day of silence again.

Tip: What a day of leave costs is deliberately unchanged. The eves are free for the badge and still ordinary working days for absence, because a day wrongly called a holiday takes a day of leave back from somebody who did take it off — the harder error to notice. Two questions, two answers, and a test that fails the day somebody merges them.

Tip: The Flow report's stalled days figure still counts calendar days, and that is not an oversight. The badge decides whether to ask somebody a question, and nobody should be asked about a Sunday; the report measures how long a thing sat, and elapsed time contains weekends. Sharing one number would have subtracted a working-day allowance from a calendar-day measurement.

A meeting has one text field

Notes and Minutes are now just Notes. Of twenty-one meetings, four had notes and two had minutes — while every meeting held sat under Awaiting minutes for ever, waiting for a field nobody filled in.

Tip: Tidy up with AI, Circulate and the action-item drafting all moved onto the notes. The cleanup happens when the notes go out, which is the moment somebody actually wants it, rather than in a second box that had to be filled in first.

Tip: The Awaiting minutes flag and the banner counting meetings "without minutes" are gone. A list of outstanding work that is never empty is not a list, it is wallpaper. A meeting that has happened now reads Held, and one whose notes have gone out reads Circulated.

Tip: Accepting a tidy or a draft rewrites what you typed. There is no second copy any more. Both show you the result in full first and neither saves until you press the button — that review is the whole safeguard, so read it.

Tip: Tidying speaks HTML and the notes are a plain textarea, so the text is converted on the way out and back. What lands in the box is Markdown — headings and bullets you can keep typing after, not tags you have to work around.

Tip: The two meetings that did have minutes keep them: the migration appends the text to their notes under a heading rather than dropping a column with a record in it.

Tip: Over the API the notes are the organiser's until they are circulated and everybody's afterwards. Before that they are half sentences typed in a room; after it they are the record that landed in people's inboxes, and hiding that from the people who were sent it would be theatre.

Also

An issue that came out of a meeting now says so in those words when something refuses to delete it, rather than calling itself "a line in a meeting's minutes".


Version 0.91.5

Date: 11 September 2026

The whiteboard is drawn on a canvas now. Everything on it — notes, boxes, arrows, ink, frames, issue cards — is painted by a rendering engine rather than built as HTML, on the GPU where there is one and on the CPU where there is not.

Why it feels different

It used to be one HTML element per thing on the board, moved by transforming the layer they all sat in. That works, and it never feels right: every pan is a layout of the whole page, every note is its own reactive scope, and the cost of the board is the number of things on it.

Tip: A canvas moves the camera instead. Panning is a handful of numbers rather than a re-layout, so forty notes cost what four hundred do — and only what is on screen is drawn, because a spatial index answers "what is in view" without walking the board.

Tip: WebGPU where the machine has it, Canvas 2D where it does not, and the fallback is the same scene drawn a slower way rather than a stub. It is reachable from a failure mid-session too — a lost GPU device is a thing that happens. Which one is live is printed in the bottom-left corner, because "is this the fast path" is the first question anybody asks of a canvas that feels slow.

Tip: Shapes are drawn from their maths rather than from pictures, so they stay sharp at any zoom. Text is rasterised once per change and reused: moving the camera does not redraw a single letter.

The engine is ours

It comes from our own collaborative-canvas project rather than from a dependency — the camera, the spatial index, the scene builder and the two renderers.

Tip: Only the drawing half came across. Its document model, its server, its identity and its storage stayed where they are: Félagi already has all four, and the seam between them turned out to be four methods wide. The board still writes one element at a time through Livewire, still broadcasts over Reverb, still obeys the same permissions, and the boards, elements, comments, votes and API are untouched.

Tip: The vendored files are kept close to upstream on purpose rather than reformatted into house style. It is an engine with its own tests in its own repository, and the cheapest thing about it is being able to pull a fix across. Each file says where it came from and what was deliberately changed.

What the hands get

  • Trackpad panning and pinch to zoom. Treating every wheel event as zoom is what made a board on a laptop feel like it was fighting you.
  • Eight resize handles, not one corner.
  • Alignment guides against every edge and centre, with the 24-pixel grid as the fallback — and hold to put something exactly where you meant.
  • Shift-drag pans with any tool; shift on an element still adds it to the selection.
  • writes on the one thing selected, arrow keys nudge a pixel (ten with shift), and double-click on empty surface makes a note there.

Tip: Typing is still a real textarea placed over the element. A caret drawn by hand is a month of work and a worse caret — no spellcheck, no dictation, no IME, and no selection anybody recognises. The canvas redraws the words the moment you click away.

Five things this found on the way

Tip: The PNG export disagreed with the screen. It walked the elements in row order while the board sorted them, so the file people send to other people had things covering each other the wrong way round. There is one order now, and the export is drawn by the same engine as the screen rather than by a second renderer written by hand.

Tip: Fit to content stopped avoiding the floating panels. That is the exact bug that once made notes unreachable underneath the toolbar — not selectable, not movable, not deletable, with nothing on screen to say why. It measures the panels again.

Tip: ⌘] and ⌘[ were still printed on the shortcut card and no longer did anything. A shortcut list with a dead entry in it is read once and then not trusted, which costs the entries that do work. Both are back, and a test now holds the card and the handler together.

Tip: The vote badge and the comment badge sit in two corners that the resize handles also occupy. Asked in the wrong order, the corner of a selected note voted instead of resizing. Handles are asked first, and a badge can only be pressed where one is drawn — the first vote is cast from the selection toolbar, where there is a word for it.

Tip: A palette read once at start-up means every sticky keeps its light colours on a dark page until somebody reloads. A canvas redraws when it is told to and never on its own, so the theme is watched.

What is deliberately not done

Connectors are still straight lines. The engine routes elbows and curves too, and turning that on changes the shape of every arrow already drawn — a decision rather than a side effect.

Nothing rotates. There is no column to keep an angle in, so the handle is suppressed: a note that turned and snapped back on the next reload is worse than one that cannot turn.

No browser test lane. This was driven in Chromium by hand; what the suite can hold is the page serving both canvases, the bundle carrying the engine, and every agreement between the PHP and the JavaScript that fails silently — a type with no shape, a default size that disagrees with the server's, a swatch with no colour behind it.


Version 0.91.4

Date: 11 September 2026

The Timeline is an overview again. One row per epic, dates at epic level, and a status somebody set rather than one the page worked out.

The colour is a judgement now

Four statuses, set by hand: On track, Tight, Off track, Not planned. A press on the status in the row changes it — four items, one click, no dialogue.

Tip: The old colour was arithmetic: estimated work left against working days left. It was honest and it answered the wrong question. An estimate nobody revises makes an epic turn red because it was underestimated in January, not because it started slipping in March — two different sentences, and one derived number cannot tell them apart. It said the wrong one at exactly the moment somebody was deciding something.

Tip: That calculation is untouched on the Gantt, which is where it belongs: the person reading it owns the plan and knows what the estimates are worth. It is not duplicated on the Timeline, so there is one place to change it and one answer.

Tip: An epic nobody has judged reads Not planned rather than getting a fifth, empty state. Four colours in the legend, four of which are statuses — a legend with an excuse in it is a legend nobody finishes reading.

Tip: Anybody who may write issues can set it. Management reads it. The role exists to read the work rather than to do it, and saying how an epic is going is part of the work. The gate is in the component, not only on the button: a hidden control is not a closed door.

How old the status is, on every row

Each row says when its status was last set, and says never set when nobody has made one. After a fortnight the line turns amber, and the top of the page counts how many have gone that way.

Tip: This is the failure a manual status brings with it, and it is the whole reason the row has a fourth column. Green set six weeks ago and green set this morning are the same pixel, and the first one is usually wrong.

Tip: Nothing about the status itself changes when it ages. A colour that quietly rewrote itself after two weeks would be a derived status again, with a worse rule — so the page says the age out loud and leaves the judgement to the person who made it.

Why it is red, written down while it is still known

The panel takes an optional note with the status, and shows it with who set it and when. It is in the CSV too.

Tip: "Why is it red" has a different answer every time, and a board meeting is the worst place to reconstruct it. Ten seconds when the status is set; ten minutes of the meeting otherwise.

Tip: Setting a status from the row keeps the note that is already there. The row is for keeping the colour current, which has to cost nothing; the panel is for saying why.

The issue dates are gone from the page

No issue rows under an epic, and no issue dates on the bars. An epic without milestones used to draw its dated issues as faint diamonds, and a chevron opened its issues as thinner bars on the same axis. Both are gone.

Tip: Dates at issue level are noise in an overview and they are already on the Gantt, which is the page for planning against them. What replaces them in the panel is the question a portfolio reader actually has: what is closed and what is not — closed first with a tick, open below, no dates on either.

Tip: Progress now reads 9 of 15 issues done · 60%. A director reads the percentage and asks about the fraction, and printing one without the other makes somebody do arithmetic in a meeting.

Tip: Milestone names are no longer drawn under the bars — they are on the diamond and in the panel. A label per checkpoint needed a row half again as tall, on a page whose whole argument is that it can be read in ten seconds.

Tip: The rows have a label column now: the epic on the left with its identifier and period, time in the middle, the status on the right. The titles used to float above their own bars, which gave the whole width to time and scattered the names diagonally down the page — reading the list of epics meant reading a diagonal.

What else went with it

The derived fields left the panel. Health, Work left and Days left were the three halves of the old calculation, and there is nothing for them to explain now. The board column went too: two things called status on one screen is one of them unread.

The faces left the rows. Who is carrying an epic is a question the By person grouping answers, and it still does — unchanged, with its capacity figures intact. A row answers four things.

Tip: That grouping's two queries now run only when somebody asks for it. The page used to fetch who was carrying what on every view, because every row wore faces.

The dashed red overrun tail is gone. Red belongs to Off track, said by a person, and two reds on one chart is one reading lost.


Version 0.91.3

Date: 11 September 2026

Search issues now searches the comments. Most of what is known about an issue is said underneath it rather than in the description, and a search that read only the description found the issues that were written well and missed the ones that were worked on.

Comments are part of the haystack

Search issues on the board matches the title, the description, the issue number — and now every comment body. Type the name of a customer, an endpoint or an error and you find the issue where somebody mentioned it in passing three weeks ago, not just the handful where it made it into the description.

Tip: When the thread is the only reason an issue matched, the card says in a comment. A result with the word nowhere on it reads as a broken search rather than a wider one, and the badge is the difference between the two. There is no badge when the title or the description already holds the word — a label on every row is not a label.

Tip: Withdrawn comments are not searched. Taking a comment back removes its words and keeps its row, so the replies under it still point at something. Searching the tombstone would match nothing and cost a join.

Tip: The ⌘K palette is deliberately unchanged and still leaves comments out. The two boxes are for different things: the palette is a way to get somewhere, and it stops being one the moment it can return the same issue eleven times. Search issues is a way to narrow a list you are already looking at, where eleven reasons to match is exactly what you want.

Tip: One exists subquery, not a query per issue. Drawing a board of twelve matching issues costs exactly as many queries as drawing a board of one, and there is a test that holds it there — the version of this that reads the thread per card works on a board of ten and times out on a board of four hundred.

The API answers the same question now

GET /api/v1/issues?q= searches comments too. It had its own copy of title like … or description like …, which meant an agent searching a workspace and a person searching the same workspace for the same word got different answers.

Tip: There is now one definition of a match in the model rather than one per caller. When the haystack widens again it widens everywhere at once, which is the only version of this that stays true.

Fixed: a word compared against the issue number

Tip: Every search for a word used to ask the database whether the number column equalled that word. MySQL answers by quietly coercing the text to zero, matching nothing and reporting nothing — so it cost a clause on every search and was invisible in the result. The number is only compared when the term is actually a number.


Version 0.91.2

Date: 11 September 2026

Documentation only. A guide for the half of working with an agent that had none — using one, rather than hiring one.

New: Giving an agent work

Manual → Working with agents → Giving an agent work. Hiring an agent covered the form an administrator fills in once. This is the page somebody opens on the day they want to hand something over: the two ways to do it, what the agent actually receives, how to write a task it can finish, watching a run, stopping one, and work that starts itself.

Tip: The fact worth reading first, because nothing on screen implies it: the agent is sent the issue, not the comment thread. Its brief is where it is and what is checked out, the workspace's Agent context, the agent's own instructions, the identifier and title with the priority, the description, and the acceptance criteria — plus its skills, as files. The comment that mentioned it is not in there, and nor is any comment before it.

Tip: So @Freya — also check the webhook path starts a run that knows nothing about the webhook path. A mention is a start button. If something matters, put it in the description and then mention the agent, or edit the description and press Queue another.

Tip: Autopilots are documented for the first time — manual, on a schedule, or on a webhook, with the full set of template placeholders. An autopilot opens an issue and assigns it, rather than starting a run directly, so automated work lands on the same board and under the same permissions as work a person assigned. Nobody has to learn a second place to look.

Tip: A placeholder with nothing behind it is left as it is rather than blanked. A board full of {{ payload.title }} tells you the payload changed shape; a board with gaps tells you nothing.

Tip: The page says plainly that a run in flight cannot be stopped. Withdrawing the comment that queued one cancels it while it is still queued; after that it runs to its end, or ends if the issue is deleted or the machine stops. That is a gap rather than a design, and writing it down is the first half of closing it.

Tip: It also says not to hand an agent an epic. Nothing refuses, and nothing good happens: an epic is a container, so the agent receives a title with no work in it.

Tip: Four places to teach an agent, narrowest to widest — the issue description, the agent's Instructions, the workspace's Agent context, and skills. The two middle ones are the ones people forget exist.

Fixed: two things the docs had wrong

Tip: A claim about a digest. An agent's summary reaches everybody following the issue the way any comment does; there is no digest and no per-kind setting, which two other pages already said. The new page said otherwise until it was checked against them.

Tip: The poll interval and the placeholder list were both verified against the daemon and the renderer rather than written from memory. Three seconds, and seven date placeholders plus payload.

What this release does not do

No code changed. The version moved so the manual in the application matches the repository; nothing in the product behaves differently.

The two gaps the new page names are still gaps. Stopping a run in flight, and an epic being assignable to an agent with no warning.


Version 0.91.1

Date: 6 September 2026

Nine things reported from use. A Download button that did nothing, video attachments, files an agent can actually read, Markdown, and a card you can drag from anywhere.

New: write in Markdown

Markdown beside a description or a comment box swaps the visual editor for the Markdown behind it. Visual swaps back. Per field, so a description can be in Markdown while the comment box below it is not.

Two reasons. Editing by hand is faster in Markdown once you know it. And an agent reads and writes Markdown reliably where it does not reliably produce clean HTML.

Tip: What is stored is HTML, always. Markdown is a view of the same text, converted on the way in and out through the same sanitiser as before — so the API, the search index and the digest mail are untouched, and which box you typed in leaves no trace.

Tip: The switch is offered only where it is lossless, and says so where it is not. Markdown cannot express a pasted table with merged cells. Where the words would change, the button is replaced by a short reason rather than being silently absent.

Tip: Underline, subscript, superscript and highlight have no Markdown syntax, so they appear as the tags they are. Markdown has always passed inline HTML through, and dropping somebody's formatting on a round trip would be the worse answer.

Tip: In Markdown mode the @ picker is not offered on a comment, because @ is ordinary text there. A name written as @someone still becomes a mention on save: mentions are read from the words, not from the picker.

Tip: Two things the sanitiser had been quietly deleting are now kept, and both were reachable from the editor's own shortcuts. Typing --- makes a horizontal rule, and saving removed it. And strikethrough written in Markdown produces a different tag from the one the visual editor writes, so the same formatting survived one way and not the other.

New: video attachments

MP4, WebM and MOV, up to 50 MB. A screen recording is often the fastest way to show a bug, and without this it went through another channel — which meant it was not attached to the issue, and six months later nobody could find it.

Tip: Video gets its own size limit, and the reason is that the general one would have made the feature ornamental: 25 MB is about twenty seconds of a screen recording, so accepting the format at that cap would accept video and refuse the thing people record. Fifty leaves headroom under the request body the application server allows.

Tip: It shows as a row with Preview, not as a thumbnail, and Preview plays it in the page with a scrub bar and no autoplay. A recording that starts talking the moment somebody opens an issue is a recording they close.

Tip: Seeking works because a video is served as a file response rather than a stream. The streamed response answers no Range request, and a browser cannot scrub such a video — Safari will not begin playing one at all. The format would have been accepted and then not played on half the machines here.

Tip: MOV is video/quicktime, which is what a Mac records by default. Refusing it would have refused the commonest case on the machines this is used from.

New: an agent can read an attachment

GET /api/v1/attachments/{id} returns the file. Uploading and deleting worked from the start and downloading had no route at all, so an agent could see that a screenshot existed and what it was called, and never open it — whatever the picture explained had to be retyped into the description by hand.

Tip: The url field on an attachment now points at that route. It named the web route, which takes a session and answers a bearer token with the login page: a 200, an HTML body, and nothing to say what went wrong. Complete metadata beside an unreachable URL reads as working, which is the expensive kind of broken.

Tip: ?download=1 on any attachment URL hands over the original bytes as a download, whatever the type. It wins over ?size= and ?preview=1, because a download of size=large would be WebP bytes under a name ending in .png.

Tip: A read token is enough. write includes read, so there is no token that can upload a file and not fetch one.

New: a link to one comment

Copy link on any comment copies an absolute URL ending in #comment-123. Opening it scrolls to that comment and rings it briefly. Comments get shared in Slack constantly, and until now the only thing that could be sent was the issue plus a sentence saying which remark was meant.

Tip: Offered to anybody who can read the comment, not only its author. The menu used to be drawn only for the writer, because until now it held nothing anybody else could use.

Tip: A withdrawn comment offers no link. The card stays, because the replies under it point at it, and there are no words to send somebody to.

Fixed: the Download button in the attachment preview did nothing

Clicking it did exactly nothing — no file, no error, nothing on screen. Three separate faults, and two of them would still have been wrong after making it clickable.

Tip: It was a <button>. The component decides whether to be a link or a button from the address given when the page is rendered, and the address was being filled in afterwards by the browser — so it rendered a button and then set an href on it, where it means nothing. A sweep now checks every template for the same shape.

Tip: The preview of an image is a resized copy, so even a working button would have downloaded WebP bytes under a name ending in .png. The download now asks for the original explicitly rather than stripping a flag off the preview's URL.

Tip: An image is served inline so that thumbnails work, so a plain link would have navigated to the picture rather than saving it. ?download=1 is what settles it, on the server, where a client that is not a browser gets the same answer.

Fixed: a card could not be dragged from anywhere on it

Dragging an issue card between columns worked from some parts of the card and not others, and the failure did not look like one: the card follows the cursor exactly as a real drag does, then snaps back on release.

Tip: That was the browser's own drag on a link, not ours. A card carries three — the title, the epic badge and the project badge — and an anchor is draggable whether we like it or not. The sorting library declines to start a drag on the title without cancelling the event, so something took over that looked identical and did nothing. Every link on a card now refuses to be dragged itself, which leaves the card free to be.

Fixed: pasting a screenshot only worked on New issue

⌘V now attaches a screenshot in the comment box and while editing a description as well. It worked on New issue and nowhere else, which is the half that matters least: an issue is filed once and commented on for weeks.

Tip: A pasted image lands where the form it was pasted into puts its files — a comment's on the comment, the description's on the issue. On the description it attaches immediately rather than waiting for Save, because that is what every other file on that page does.

Fixed: New issue was a narrow sidebar

It opens as a wide modal now, with what you write on the left and what you choose on the right. The sidebar left the description editor 444 pixels across and 112 tall with no way to make it bigger — fine for a sentence, and the only field on the form that holds prose.

Tip: It was a sidebar for a reason, and the reason is gone rather than forgotten. As a centred modal the form used to outgrow the window: the heading scrolled off the top and the buttons sat below the fold. This one bounds its own height, scrolls the fields in their own box and keeps the buttons outside it, so both of those stay true at any window size.

Fixed: the description was 14px

16px now, which is what tools people compare this to use. It is the most-read text in the product and the one people sit and read rather than glance at. Text tucked beside something else — a note on an hour, an excerpt in a timeline — stays at 14.

Fixed: five tests passed Monday to Friday

Absence is recorded on working days, so five tests that asked to be away today recorded nothing at all on a Saturday or a Sunday and then failed looking for the row. The suite was reporting the calendar.

What this release does not do

Markdown is not a storage format. It is a view. There is no way to ask the API for a description as Markdown, and no setting that makes Markdown the thing that is kept.

A pasted table with merged cells cannot be edited as Markdown. The switch says so rather than offering itself and changing the words.

Video has no thumbnail and no duration. It is a row with a Preview, not a poster frame.

The description font is not configurable. One size for prose somebody reads, one for text beside something else, and no setting.


Version 0.91.0

Date: 4 September 2026

The database backs itself up every night, the weeks ahead can be read before somebody promises one, a projected date can become the promise, and a project stops pretending to finish.

New: the database backs itself up, every night

felagi:backup runs at 02:00 and keeps seven nights. It writes four files with one timestamp: a manifest, the database as gzipped SQL, and one archive per file store.

It existed before this and was never run, which is the whole point. The command was a command and not a policy, so the machine holding the data had no copy of it — nobody runs a backup they have to remember.

Worse, it could not have taken one. The dump used to shell out to mysqldump if it was on the path, and say so plainly when it was not. On the one machine whose data matters it never was: production compiles PHP into the application server, and that image carries no mysqldump. The honest warning worked exactly as designed, every night, and nobody had a database.

So the application dumps its own database now, in PHP, as an ordinary SQL file that any client can read.

Tip: It reads the file back before the run is called a success. The failure a backup actually has is a file that decompresses to nearly all of itself, and that is invisible at the moment it is written unless something reads it. One gunzip a night buys the difference between having a backup and believing you have one.

Tip: The dump takes no exclusions — the cache, session and queue tables go in with everything else. Every rule for deciding which tables to leave out is a rule that will one day leave out the wrong one.

Tip: --keep=7 is passed by the scheduler and is not the command's default. A backup tool that deletes backups by default deletes them on a machine where somebody was keeping them by hand. Retention removes whole sets, matched on the timestamp: removing three files of four leaves a listing that looks like a backup and restores nothing.

Tip: Seven nights on the application's own disk is a floor, not a strategy. It survives a bad migration and a dropped table. It does not survive the disk, the machine or the building. The off-site copy is still yours to arrange, and nothing here pretends otherwise.

New: a restore that can be checked rather than trusted

php artisan felagi:restore <file> --check    # reads it end to end, touches no database
php artisan felagi:restore <file>            # drops and rewrites, and asks first

The documentation used to argue against a restore command: loading a dump is three lines an operator can read, and the one operation where trust is least affordable should not be hidden inside a tool. That was right, and it assumed a client. There is no mysql on the production image either — so on the machine holding the data those three lines cannot be typed. The choice was never between a tool and an operator.

It refuses a file that does not end in the marker a finished dump writes, a file written by another engine, a database that already has tables, and any statement that is not SET, PRAGMA, DROP, CREATE or INSERT.

Tip: When it finishes it counts every table against what the dump says it holds and reports the restore as failed on any disagreement. A restore that ran without throwing has proved the file was acceptable SQL, which is not the same sentence as the data is there.

Tip: --check is the mode meant to be used far more often than the other one. It answers "would this restore" without restoring, and it is what the nightly backup runs against every dump it writes.

Tip: A restore is asked for confirmation even with --force. --force answers "the database is not empty", which is a different question from "into this one", and a restore into the wrong database has no undo.

New: the weeks ahead, before somebody promises one

Reports → Weeks ahead. What each week holds for the next quarter, with leave and public holidays already taken out.

Week  Dates        People   Of a normal week      Machines  Why
13    30 Mar–5 Apr    24h   ███░░░░░░  60%            40h   Maundy Thursday · Good Friday
14    6 Apr–12 Apr    32h   ██████░░░  80%            40h   Easter Monday

Every other report in Félagi looks backwards. This is the same capacity model pointed the other way, for the question nobody asks until they have missed: Easter and the week between Christmas and New Year are systematically thin, and a cycle planned across one of them is short before it starts.

Tip: A public holiday counts whether or not anybody has logged it. A report that read only the timesheet would show Holy Week as a full week until somebody entered the leave, which is exactly the moment it stops being useful. Computed holidays are counted once, never twice — the computed subtraction only ever takes what the recorded one left.

Tip: Machines do not take holidays, which is why the two columns diverge around Easter and Christmas. That is a routing argument rather than a curiosity: the thin weeks are the ones to give the agents.

Tip: The week in progress is never called thin. Two days left of five on a Thursday is a fact about Thursday, and a warning that appears every week from Wednesday onwards is a warning nobody reads.

Tip: Everything is measured against each actor's own week. Somebody on three days is not a permanently thin week; they are a full week of three days.

Tip: The one thing it cannot see is leave nobody has entered yet, and it says so on the page rather than leaving you to find out.

New: turn a projected date into the promise

On an issue, Assignee → When would this land?, every row with a date now carries Promise this — or Take it and promise for a candidate who is not holding the issue, because a date projected through Freya's queue is not a promise about anybody else.

The flyout could always say when work would land and never write it down. A date that can only be read and then typed into the sidebar by hand is how a schedule and a due date come to be two different numbers — and once they are, the schedule is a curiosity and the due date is a guess.

Tip: The date is recomputed when you click, not taken from the screen. A finish arriving from a browser is a date anybody can choose, and the whole value of the button is that the number is the schedule's rather than the clicker's.

Tip: Promising a date changes these dates, including this one. Work is scheduled in due-date order, so the issue moves ahead of everything due later. The flyout says so rather than letting it be a surprise.

Tip: It is recorded as a date that came from the projection, not as somebody typing one. Six months later the difference between we committed to the schedule's own answer and somebody picked a Friday is the whole of what the promise was worth.

New: did it land when we said?

On Throughput, under the weekly bars:

On the day or before   62%     Within 2 days   81%     Median slip  +1d     85% within  +6d

The trust number, and the one nothing in the product had ever measured. Every projection here — the Gantt's bars, the routing flyout's dates, the Timeline's health rule — is an argument about the future, and a schedule nobody has audited is a schedule people quietly stop reading.

Tip: Measured against the due date, which is a deliberate limit rather than a shortcut. A projection is a function of the capacity on the day it was made, and none of that is kept — so recomputing one for a finished issue would produce a date today's calendar implies and yesterday's never said. The due date is the only forecast that was written down, which is exactly what Promise this above is for.

Tip: The slip is signed, so early and late do not cancel. A team that is systematically early has a different problem from one that is systematically late, and an absolute figure reports them as the same team.

Tip: Work finished with no date is counted apart, never as a success. It is usually the larger population, and folding it in either direction would be the whole finding.

New: a New epic button on the Epics page

The page that lists epics could not make one. Its empty state sent people to the board to set a Type nobody guesses, which is a list people read once.

Tip: The form is shorter than the board's on purpose. An epic takes no estimate — the issues inside it are what get estimated, and the model refuses one — and it cannot be filed under another epic. Its description is written on its own page, where there is an editor for it.

Tip: Only projects that have the epic type enabled are offered. A project that switched it off would refuse the epic, and offering it would be offering a choice that fails on submit.

Changed: a project has no target date, and no completion percentage

Projects here are systems. Every one of them will still be running in three years, so:

  • A target date was either ignored or quietly wrong. A target permanently in the past teaches everybody to ignore the colour red.
  • A completion percentage described the backlog, not progress. It drifts towards a hundred as old work is closed, whatever is happening now.

Both are gone. A project now reports how many issues are open on it — a fact somebody can act on. What bounds work in time is the epic, which has a start date, a due date and milestones.

This finishes a decision the code had already made: milestones were built on the epic rather than on the project, and that migration says why in its own words — "an epic is the thing a client asks about and the row the Timeline draws". The project target date was the field that decision left behind.

Tip: Two things went with the date. The Gantt no longer draws a yellow diamond on a project's group row, and the health rule "Later than its project" is gone. There is deliberately no substitute comparing an issue to its epic's date: that would be a new rule rather than the same one, and inventing a rule while removing a field is how a removal quietly becomes a feature.

Tip: Breaking for the API. target_date is gone from the project resource, and a POST or PATCH that sends one is refused with a message rather than ignored. Laravel drops an unvalidated key silently, so an integration still sending a date would get 200 and believe a deadline had been recorded — and somebody believing a date is stored that is not is the worse of the two outcomes.

Tip: The migration drops the column, and the dates in it are gone. There is nowhere to read them back from, so down() returns an empty column rather than pretending. They are in last night's backup, which is the honest answer — and, as of this release, there is one.

Fixed: time entries on a meeting could not be corrected

The Time spent panel on a meeting could add and remove and nothing else, so fixing a wrong duration or a wrong date meant deleting the row and logging it again. Correcting in place landed for the timesheet and the issue page; the meeting page did not get it.

Tip: Delete-and-retype was the wrong repair for a reason that is easy to miss: it loses when the work was recorded and turns a measured entry into a remembered one. Those are counted apart, because the time report says what share of its hours are leaning on recall. A corrected figure stays measured — whether a clock was running is a fact about how the number was arrived at, not about the number.

Tip: Your own entries only, and never an agent run's. An owner can read every entry on the page and correct none of them: somebody else's hours are their statement, and a run's duration is evidence the timeline already carries.

Fixed: the Gantt dated work by two columns that were not dates

The green bar's finish read updated_at, so editing the title of an issue closed in March moved its bar to today — and a bulk write moved every bar in the chart. Its start read the first agent run, which exists only for work a machine did, so every issue a person worked on began, according to the chart, at whatever its planned start happened to be.

Tip: The finish is closed_at now, extended to cover hours logged after the closing. The start is the earliest of three recorded facts: the first day hours were logged, the moment the issue first entered a status somebody works in, and the first agent run. The earliest rather than a preferred one, because an hour logged before anybody moved the card is still work.

Tip: An open issue's bar stops on the last day worked rather than running to today. Running it to today would claim every open issue is being worked on right now.

Fixed: a status change did not take effect in the request that made it

Saving a workflow state cleared one memo and left the per-workspace one standing, so every reader for the rest of that request answered with the classification from before the save. Nothing looked broken, because the pages that write also cleared it by hand — which made the correctness of a status change depend on the calling page remembering to.

Tip: "Working" — started and not paused — was also written out in three places. It is one method now, so renaming a status changes nothing and marking one paused moves its time out of working everywhere at once.

What this release does not do

No off-site backup. The nightly run keeps seven nights beside the application. Getting a copy onto another machine is not done for you, and it is the largest remaining gap on that page.

Nothing rehearses a restore on a schedule. The nightly run proves the file is readable, not that it reconstructs the database. Rehearsing that means restoring into a scratch database, which needs somewhere to put one.

Weeks ahead cannot see unbooked leave. A fortnight nobody has entered is a full fortnight as far as the report knows. It says so on the page; it cannot do better than the timesheet it reads.

Projection accuracy is about promises, not projections. It compares what was written down against what happened. A projection nobody committed to is not in the figure — which is the argument for committing them.


Version 0.90.0

Date: 3 September 2026

Idle machines pick from the pool, the board admits when "in progress" is not, and four small numbers nobody was adding up.

New: idle machines pick from the pool

On the dashboard, under Workforce:

Idle machines
Freya has 1d free today
  ACME-12  Rotate the signing keys     2h   Hand over
  ACME-31  Drop the legacy webhook     45m  Hand over

An agent has weekly hours like a person, and the board has a pool of unassigned backlog like every board does. Nothing put the two together: an agent with a free afternoon sat idle while three small issues nobody had taken sat in the pool, and the person who could have connected them was looking at neither. This is the product's thesis — machines are in the capacity number — made operational: an idle machine is visible idle capacity in a way an idle person never quite is.

Tip: Idle means three things at once: the runtime is online, the agent has no run in flight, and it has time left in its day — after what it already carries is subtracted. An agent that is online but mid-run is working; one with a free slot and a full day is not idle.

Tip: Offered: unassigned backlog with a size that fits the time left, never an epic or standing work, at most three, urgent first and then oldest. An unsized issue cannot be said to fit anywhere, so it is never offered — which is one more reason to size things.

Tip: Where the agent has finished work before narrows the offer to those projects. It is the only signal about fit the arithmetic has; an agent with no history is offered anything that fits.

Tip: It suggests; a person confirms. One click assigns the issue and queues the run — exactly what assigning to an agent has always done. Whether this agent is the right pair of hands for this particular issue is a judgement about the work, and the arithmetic knows nothing about the work.

Tip: If somebody took the issue between the page being drawn and the click, the hand-over is refused rather than reassigning their work. And the click is what is gated, not the list: the one role without the right to assign never reaches the dashboard at all.

New: stalled — started, and nobody has touched it

A started status is counted as work everywhere: capacity, Flow, the standup. An issue that has sat in one for a week with nobody touching it is not work; it is a question about who is doing it. Waiting is somebody else's problem; stalled is the assignee's — and the two looked identical on every screen.

Three places now say it:

The board card An untouched 6d badge on a started card
The standup A count per person beside past estimate and overdue, and untouched 6d on the row
Flow A third figure between working and waiting — Working 47% · Stalled 12% · Waiting 41%

Tip: Touched means any activity of any kind, or a day with hours logged. updated_at deliberately does not count: it moves when the sweeper renews a lease or a report caches a column, which is nobody touching anything.

Tip: Three calendar days. Long enough that a weekend on its own never trips it; short enough that "started Monday, nothing since" is caught by Thursday's standup.

Tip: A paused status never stalls. Paused is waiting, and nobody was meant to be on it.

Tip: In Flow, stalled comes out of working, not out of waiting. It was in a working status — that is the whole point: the status said work was happening and nothing was. So the three read apart: time in a queue, time being worked, and time the board claimed was being worked. The first three days of any silence are the ordinary rhythm of work and are not held against anybody; only what runs past them counts.

Tip: Flow measures it on finished work; the board and standup on open work. Same allowance, different question: how much of a lead time was stalled, against what is stalled now.

New: carried three times is a decision, not a delay

The cycle page names the issues carried three windows or more, and says the two honest moves:

2 issues have been carried 3 times or more. They are not going to happen by being carried again. Close them, or make one of them next.

Tip: Past three windows the carry count stops describing lateness and starts describing a decision nobody has taken. The work is either not wanted or not next, and every fortnight it is carried again the plan says it is both.

Tip: Three, because that is where the carry column already turned red. Finished work that was carried a lot is history, not a decision — it is not listed.

New: what a standing meeting has cost, and produced

On the meetings page, per recurring series:

Weekly platform sync · 14 held · 21h · 6 action items, 2 done

Tip: A recurring meeting is the one cost nobody totals: each occurrence is an hour, and an hour is nothing, and forty of them are a working week that produced three action items of which one got done. Every part of that sentence was already recorded — occurrences, hours logged against the meeting, action items as issues carrying a meeting id. Nothing added it up.

Tip: Held occurrences only. A series generated three months ahead has not cost anything yet, and one that has never met says nothing for itself either way.

New: how round the recalled hours are

On the Time report, under observed against recalled:

80% of recalled entries land on a half-hour; 10% of observed ones do.

Tip: Memory lands on the half hour — somebody typing in Friday's hours writes 2h, not 1h 50m — and a stopwatch never does, because it does not know what a round number is. The gap between the two shares is a measurement of recall bias the timesheet has always contained and nothing read out.

Tip: About the figures, never about a person. A whole team recalls in round numbers; the point is knowing how much of a report rests on them.

Tip: Withheld under ten entries of either kind. Four recalled entries, three of them round, is 75% and means nothing.

New: what a finished issue costs

Under a project's cost line, once anything has finished:

$204 per finished issue, over 3 · $300 when a person finished it (2) · $12 when a machine did (1) — not adjusted for the size of the work

Tip: Per issue, not per hour: an hour is an input and an issue is what somebody actually wanted.

Tip: Finished work only. The open half is money spent on outcomes that have not happened yet, and dividing it by the issues that have would make every project look cheaper the more unfinished work it carried.

Tip: The two per-population figures side by side are the counterfactual in its plainest form — and they are not adjusted for the size of the work, which the line says. A machine that is handed the small tasks looks cheap per issue for a reason that has nothing to do with the machine.

Fixed: the dashboard counted the week's closed issues by updated_at

An issue finished in March and retitled on Monday counted as closed this week. By closed_at now — the third place this release cycle has removed that particular trap.

What this release does not do

Nothing hands work to a machine on its own. The dashboard offers; a person clicks. An agent that could be trusted to pick its own work is a judgement about that agent, and this is not the release that makes it.

Stalled does not know why. Untouched for six days because the person is on leave, because they are blocked, or because they forgot — the badge is the same. The absence record says the first; the other two are the conversation the badge exists to start.

The rounding share is descriptive. It says how much of a report rests on recalled figures; it does not correct them, and should not — a corrected recollection is a guess with a decimal point.


Version 0.81.0

Date: 2 September 2026

An issue can say when it is wanted and how big it is at the moment it is filed.

New: due date and estimate on the New issue form

Both used to live only on the issue's own page, so filing an issue with a deadline was two steps — and the second was the one that got skipped. Neither is required.

Tip: Not for an epic. It has no estimate of its own — the issues inside it are estimated — and its dates are set on its page beside the start date it also has. Choosing that type takes both fields off the form, and clears whatever was typed into them, so nothing is sent from a field nobody can see.

Tip: The estimate is written the way it is said: 2h 30m, 1d, 1w, 90m. A week is five days and a day is eight hours, as everywhere else here.

Tip: An estimate that cannot be read is refused with a reason, not saved as nothing. "A couple of hours" parsing to null and the issue arriving unestimated is the outcome worse than an error — somebody believing a size is recorded that is not.

Tip: Why it matters more than a convenience: an issue without an estimate is counted apart on every report, and an issue without a due date joins the back of every queue in the projection — When would this land? schedules in due-date order. Both were easy defaults to arrive at by omission.

Tip: An estimate arriving on an epic by any route is refused by the server too, since a form field that is hidden is still a property that can be set.


Version 0.80.1

Date: 2 September 2026

Every photo taken on a phone answered with a broken thumbnail. Screenshots were fine.

Fixed: a phone photo ran out of memory, and nothing said so

A twelve-megapixel photo is well inside the forty-megapixel ceiling, so it was decoded — and GD holds a truecolor image at four bytes a pixel, twice over while it rotates, which is a hundred megabytes on top of what the request already had. Under a stock memory_limit of 128 MB that is a fatal error, not an exception: the catch never saw it, the log said nothing, and the browser showed a broken image.

The pipeline now works out what the decode will need before it starts, and raises the limit for that request only, as far as the arithmetic says.

Tip: Never past 512 MB, and never for the next request — php-fpm hands it the configured value again. Half a gigabyte covers everything the pixel ceiling lets through with room to spare, and anything past it is served as the original, exactly like every other refusal in that pipeline.

Tip: The estimate is deliberately generous — two full-size copies, the encoded source held twice, plus a margin. Running out is fatal rather than caught, so guessing low is the one mistake this cannot afford.

Tip: A host that forbids changing memory_limit gets the original instead of a thumbnail. Said in the operations notes, because the failure it replaces left nothing in any log anybody reads.

Tip: MAX_PIXELS alone could not have caught this. It refuses images too large to be worth a thumbnail; this is about images small enough to try and large enough to die on. Two limits, two questions.


Version 0.80.0

Date: 1 September 2026

Three things the projection and the history could always answer and were never asked.

New: when would this land, if each of them took it?

On any issue: Assignee → When would this land?

Freya Lund      soonest    nothing in front of it                            8 Sep
Arne Berg                  3 issues in front · 1w 2d                        21 Sep
Bragi                      nothing in front of it                            8 Sep
Kari Vold                  Committed to standing work for the whole week         —

The Gantt has always been able to work this out and nothing ever asked it. It schedules every assignee's open work forwards through their real capacity — their queue, their leave, their standing work, their dependencies — so given what Arne already owes, the 14th; give it to Freya and it is tomorrow is a sentence the projection already contained. The assignment that decides the date was being made from a name in a dropdown.

Tip: This is the comparison no other tracker can make, because no other tracker has the machines in the capacity number. An agent's weekly hours count exactly as a person's, so a queue of one is a queue of one whoever is standing in it.

Tip: Every date travels with the argument for it. What is in front of it, because a date alone invites "why is Arne slow" rather than "Arne is carrying eleven days of work". And what the date would costtomorrow is not a good answer if it puts two other things past their dates, so the row says which and by how much.

Tip: Whatever was already late is never counted against a candidate. It stays late whoever takes this, and blaming the assignment for it would make every candidate look ruinous.

Tip: Due-date order decides the answer, and nothing on screen would otherwise imply it. Work is scheduled in the order somebody actually works in when they are behind, so putting a date on this issue moves it up every queue — and an undated one lands behind everything already promised and displaces nothing. The same issue can be tomorrow or the 21st depending only on whether it has a deadline.

Tip: An unestimated issue gets no dates at all, and says so. With nothing to schedule it lands the moment anybody picks it up, identically for everybody — and a screen of matching dates reads as a comparison that came out level. It is not level, it is unknown.

Tip: Where two would land it on the same day, neither is marked soonest. They are level, and badging whichever sorted first would invent a distinction the projection did not find.

Tip: A candidate with no plannable hours is named with the reason rather than dropped. A shorter list presented as the whole field is a comparison somebody acts on without knowing who was left out of it.

Tip: It is the Gantt's own projection, asked once per candidate — not a second, simpler calculation. Two screens giving two dates for one issue would leave nobody able to check either.

New: a person's board is their work and the pool

Narrow the board or the list to a person — Assigned to me, or a named colleague — and the backlog nobody has taken now comes with it.

Before this, the one view somebody works out of all day was the one view that could not answer what should I take next: the answer sat on a board they had to un-filter to see, among everybody else's work.

Tip: Assigned backlog stays where it is, on the board of whoever has it. That is what assigning it meant, and putting it on everybody's board would answer "who is doing this" four times.

Tip: The backlog category, not a status named backlog. Rename it, or classify a second status that way, and the pool follows.

Tip: Only the backlog. An unassigned issue somebody has started is not work waiting to be picked up — it is a question about who is doing it. The Unassigned filter still asks that one, across every column.

Tip: No pool on an agent's board. An unclaimed issue is work waiting for somebody to pick it up, and a machine does not pick things up — an agent runs what it is handed.

Tip: It narrows with everything else. A board filtered to one project shows that project's pool and not the whole workspace's.

Tip: And it says so, because the total on a narrowed board is no longer that person's work: a line above the columns counts what was added, and each pool card carries an unassigned badge. A card with no avatar is otherwise the whole signal, and "no avatar" is not a sentence — somebody reads their own board, counts eleven things and believes eleven are theirs.

New: Flow — where the time actually went

A new report. Every tracker says how long work takes; almost none say how much of that was work, because almost none keep the status history to answer it. The answer is usually that most of the time was nobody doing anything.

Lead time 15.5d      Cycle time 5d      Working 47%      Waiting 53%

Backlog        waiting   24d   41%       In review     working   23d   40%
Todo           waiting    7d   12%       In progress   working  4.1d    7%

Seven per cent of that was somebody working. Eleven days, of which three were worked is a different conversation from eleven days, and it is the only one that points anywhere: you cannot make the three much faster, and the eight are often a decision.

Tip: Working and waiting are your own classification, not a list of names in the report. A status counts as working when the workspace classifies it as started and does not mark it paused — exactly what in flight already means in the status report. So the lever is one you already have: a team that thinks a review queue is not work marks that status paused, and its time moves. Nothing in the report changes.

Tip: The per-status table is the actionable half. "Waiting 8 days" says something is wrong; "6 of the 8 in review" says what.

Tip: People and machines, side by side — the comparison no other tracker can make. Machines do not wait: on the figures above, people waited 31 days against a machine's 13 minutes. Split by what is doing the work rather than by who, because this is an argument about what to route where and not a league table.

Tip: Calendar days, weekends included, and this is the only report here that does not count working days. Capacity is about how much somebody can do, so a weekend is not capacity. This is about how long a thing sat, and a thing that sat over a weekend sat for two more days — nobody waiting on it experienced a four-day week.

Tip: Beside each figure is what 85% finish within, because a median is no basis for a promise. "Half finish within four days" is not something to tell anybody; "85% within twelve" is.

Tip: Work with no recorded history is counted apart, never as a zero. An issue closed before any of this existed has a lead time and no breakdown. Filing its whole life under whatever status it is in now would invent a figure; dropping it would make the lead time a median of the recent half. So it is in the lead time, out of the split, and the page says how many.

Tip: Below half coverage the split is not drawn at all. A breakdown from a fifth of the work, presented as the shape of the whole, is worse than no breakdown.

Fixed: the Throughput report's cycle time was wrong twice over

It read first_executed_at, which is set when an agent run starts — so every issue no machine ever touched was silently absent from the median, and a workspace with no agents saw the figure with a count of zero beside it. And it ended at updated_at, which moves when somebody edits a title, so an issue closed in March and tidied in June reported three extra months.

Tip: The board's ageing-off rule names that exact trap in as many words — updated_at moves when somebody edits a title or logs an hour — and this had walked into it anyway.

Tip: Throughput now reads the figure from Flow rather than computing it again, so the decomposition on one page adds up to the number on the other. Two implementations of one definition drift the first time either changes.

What this release does not do

Nothing moves the work for you. The projection will tell you which pair of hands makes an issue land soonest and what that costs; it will not reassign anything, or rebalance a queue on its own. And it knows nothing about the work — whether a candidate is the right one to do this particular thing is a judgement it cannot make and does not pretend to.

Flow measures where the time sat, not whether anybody was on it. A day in a started status counts as working even if no hours were logged against it. "Started and untouched for a week" is a different pathology from "in review for a week", and telling them apart means reading the timesheet beside the history — the material is all there, and the report does not do it yet.

No trend on Flow. It answers "where does the time go" for a period, not "is the waiting getting worse".

Dragging a pool card does not claim it. It changes the status, and joins the current cycle if it entered a started one, but the issue stays unassigned.


Version 0.79.1

Date: 31 August 2026

The notes you are reading now render the way they were written.

Fixed: tables, numbered lists and quoted lines in these notes

The notes are markdown, so their styling lives in the stylesheet under one class rather than as classes on elements nobody writes by hand — and three elements had no rules at all.

Tables inherited the panel's font size instead of the prose's, so the right-hand column came out half again as large as the paragraph above it while the left one, being mostly inline code, stayed small. One row read as two different documents. The empty header band that a headerless table draws is gone, a route no longer breaks after the verb, and a term now sits level with the first line of its explanation.

Numbered lists lost their numbers. The reset strips list-style from both kinds of list, which is why bullets were spelled out — and ordered lists never were. A five-point list read as five unnumbered lines.

Quoted lines had no margin, no indent and no rule, so an example of what something looks like on screen was indistinguishable from the sentence describing it.

Tip: A sweep now renders this whole file and checks that every element it can produce has a rule. Both defects had shipped, for releases, and neither looked broken enough for anybody to report — which is the kind of thing only a sweep finds.

Tip: A header row that says something is kept. Two tables here head a row-label column with nothing and the columns beside it with something, and the first version of the rule that hides an empty header matched "any empty cell" — which would have taken those words off the screen.


Version 0.79.0

Date: 31 August 2026

A week off is one entry instead of five, and absence finally has an API.

New: absence takes a range

Log time → Absence now asks for a first day and a last day. The last one is optional — leave it out and it is the single day it always was.

Before this, a week of vacation meant opening the dialog five times and picking the kind and the hours again on every one. The fifth is where somebody picks Sick leave by accident, and the timesheet then says something about them that is not true.

Tip: A week is five days. Weekends are skipped, and not because a Saturday is refused: nobody is expected to work one, so there is no gap to fill and no entitlement to spend.

Tip: A public holiday inside a week of leave is recorded as the holiday, not as the leave. Both halves of that matter. It is recorded, because skipping it leaves exactly the hole absence exists to close — a blank Friday reads as somebody who forgot rather than somebody who was away. And it is not vacation, because the figure deciding how much leave somebody has left should not charge them for Good Friday.

Tip: The dialog says what the range comes to before you press the buttonVacation: 3 days · 2 weekend days skipped · Maundy Thursday, Good Friday as public holiday. The count is rarely the number of days that were picked, and a plain "Recorded" beside a figure that disagrees with the range is how somebody concludes it did not work and does it again. The preview and the write walk the same days by the same rules, so the promise is the one that gets kept.

Tip: The holidays are named, not counted. "1 public holiday" is a number somebody then has to go and look up; Good Friday is the answer they were going to look for.

Tip: A day already carrying absence is left alone, and counted in the sentence. A range is the easy way to record the same Wednesday twice, and two entries of eight hours is a sixteen-hour day that nothing downstream can tell from a day somebody worked. Hours of work on a day do not block absence on it — somebody who worked the morning and went home ill is making two true statements about one day.

Tip: The duration is per day, not over the range, and the label says so. Read as a total, "1w" over five days would record five weeks.

Tip: Correcting an existing entry is still one entry. The range is how absence is written, not a property of a row — widening one into five would be a new decision dressed as a fix.

New: the holiday calendar

Norwegian, and computed rather than tabulated. Ten of the twelve days move with Easter, so a table would be a list somebody has to extend every few years — and the year it ran out is the year every range quietly started spending leave on Good Friday again.

Tip: Christmas Eve and New Year's Eve stay ordinary working days. Most of Norway does not work them, but neither is a public holiday in law — and a day wrongly called one takes a day of leave back from somebody who did take it off, which is the harder error to notice.

Tip: Where two land on one date — Whit Monday falls on Constitution Day in 2027 — the day has one name, and it is the fixed one. On 17 May a Norwegian says Constitution Day whatever else the church calendar is doing that week.

New: absence over the API

Three routes, and the range is reachable through all of them:

POST /absences kind and a duration; date for one day, or from and to for a range
GET /absences Filter by from, to, kind, mine
DELETE /absences/{id} Your own hours only

Tip: The rules are the interface's, because they are the same object's. A script that had to work out for itself which of fourteen days were weekends was a script reimplementing the calendar — badly, and differently from the interface.

Tip: The response carries summary and skipped. A client that sent fourteen days and got ten entries has to be able to tell why — otherwise the sensible thing for it to do is retry, and the retry is a double entry.

Tip: A range is capped at 366 days. A guard on a mistyped year rather than a policy about how long somebody may be away: the alternative is several thousand rows across a decade of somebody's timesheet, taken back one at a time.

Tip: date and from are one field under two names, both accepted. A script logging today says date; a range says from. A 422 for a request that means exactly one thing is a 422 nobody can act on.

Fixed: a day of leave was reported as a meeting

GET /time-entries described every absence as "subject": "meeting" with a null meeting, so a client grouping hours by meeting dropped a fortnight of somebody's leave without a word.

subject is now issue, meeting or absence, and an absence field carries the kind.

Tip: This field has now been wrong twice for the same reason — it was added when hours belonged only to issues, and each new kind of hour kept getting the previous answer. A sweep would not have caught it: it was a plausible value, just the wrong one.

Tip: Absence does appear in GET /time-entries, deliberately — leaving it out of the whole record of somebody's hours is how a fortnight disappears. It is separable there with ?absence=work, ?absence=absence or a particular kind. GET /absences exists so a client that wants leave does not have to know that filter, and does not silently get issues if it forgets it.

What this release does not do

Nothing counts how much leave anybody has left. A range spending more vacation than somebody has is recorded without comment, because there is no balance to compare it against — no entitlement per person, no year boundary, no accrual. Recording the days correctly, and not charging a public holiday to somebody's vacation, is the half a balance would have to be built on.

Work still takes one day at a time. A week on a single issue is a plan rather than a record of one, and the Gantt is where a plan belongs.

One holiday calendar, and it is Norwegian. A per-workspace calendar is an administration page, a table, and a decision about what happens to hours already recorded on a date somebody later removes — the same call the five absence kinds already make.


Version 0.78.0

Date: 31 August 2026

Epics get a start date, lose their estimate, and the Timeline answers one question instead of another.

New: a start date on an epic

Set it on the epic's page, beside the deadline. Together they are the bar on the Timeline — and the window the health judgement is made in.

Tip: Epics only. An ordinary issue's span comes from its estimate and its assignee's queue, which the Gantt projects; a start date typed onto one would be a second plan competing with that.

Changed: an epic has no estimate of its own

The issues inside it are estimated. A second figure on the epic is two answers to how long the same work takes, with nothing to say which the reports should believe.

Tip: Nothing was thrown away. Every epic that carried an estimate had it turned into a start datedue date − estimate — which is exactly what the Gantt had already been inferring from it whenever an epic had a deadline and no start. Seventy days of somebody's planning became seven dates you can read and correct.

Tip: The API refuses an epic estimate on both doors rather than dropping it silently, for the reason that field's own documentation already gives: a client that sent a number and got a 200 has no way to learn it is gone.

Changed: health is now "will the work fit in the days left"

It used to compare how much was closed against how much of the calendar had gone. It now asks the question somebody actually has:

On track The work left needs no more days than remain
Tight It needs more days than remain
Will not fit It needs more days than remain, and under a working week is left
Not estimated It has open work and nothing in it is estimated

The row shows both halves — 15d left · 10d — so the colour is an argument rather than something to trust. And on track is green now, because the question has a yes and a no, and blue is a colour that means nothing on its own.

Tip: Working days, Monday to Friday, as capacity is counted everywhere else here. Comparing calendar days against estimated work days compares unlike things: eight days of work with nine calendar days left reads as fitting on a calendar count and not fitting on a working one — and three of those nine days are a weekend.

Tip: Work left is estimate − spent per open issue, floored at zero. An issue past its estimate has no work left on paper, and counting the overrun as remaining would make an epic look worse the closer it got to done.

Tip: An epic with open work and no estimates anywhere in it reads Not estimated rather than green. Zero days of work against any number of days would answer on track confidently, about an epic nobody has sized — and a green bar somebody believes is worse than an honest grey one.

Tip: Where only some of it is estimated the judgement is made on the part that is, with the unsized count beside it in amber. Never folded in: an unestimated issue counted as zero makes an epic look comfortable.

Tip: A week is the line for will not fit because a week is the shortest notice that is any use — long enough to move a date, drop something or ask for help.

What this release does not do

The health rule counts days, not people: an epic with ten days of work and ten days left reads on track whether one person or four are on it, and nobody's holiday or support rota enters the comparison. The Gantt's own projection does account for both, and is the place to look when the answer matters to the day.


Version 0.77.0

Date: 31 August 2026

Absence now reduces capacity, which was the half missing from 0.76.0.

Changed: a week off moves the dates

A holiday was visible on your timesheet and invisible to everything that plans against time. It is not any more.

What you'll notice

  • The Gantt's projected finish steps over an empty day. Two days of work starting Monday finishes Tuesday; with that week booked off, it finishes the Tuesday after
  • A cycle's capacity drops by the leave inside its window, and the card says so: 40d · 20d people · 20d machines · less 5d away
  • Half a day off leaves half a day of capacity

Tip: By the hour, not by the day. A whole-day switch would either lose the other half of a half day or invent it — and half days are most of what a timesheet actually contains.

Tip: Standing work and absence are deliberately not the same subtraction. A support rota is time that exists and is committed; leave is time that is not there at all. A cycle counts committed time as capacity on purpose, and does not count days nobody is present for.

Tip: A day never goes negative. Nine hours of leave on an eight-hour day leaves nothing, not minus one hour — otherwise every projection after it would be meaningless rather than merely odd.

Tip: With enough leave booked there is no finishing date worth drawing, so the projection draws none instead of walking for ever. That mattered as soon as a day could be empty: with a flat daily rate the walk always made progress, and asking per day means an empty day advances the calendar and absorbs nothing.

Tip: Absence on a weekend still shows on the timesheet and still does not reduce capacity. Weekends were never capacity, and a figure that quietly dropped the hours would disagree with the sheet they came from.

What this release does not do

Nothing tracks how much leave anybody has left, and nothing is approved: absence is a record of hours, not an entitlement. Absence still cannot be recorded over the API, and the five kinds are the same in every workspace.


Version 0.76.0

Date: 31 August 2026

Holidays, illness, and where hours belong.

New: record absence on your timesheet

Log time has a Work / Absence toggle. Absence takes a kind, a duration and a date: Vacation, Sick leave, Child's sick day, Time off in lieu, Public holiday.

Tip: A holiday is not the absence of a record. A week off with nothing written down looks exactly like a week somebody forgot to fill in — so the person on holiday gets chased and the person who forgot does not. That is the whole reason this exists.

Tip: Two rules, and everything follows from them. Absence fills the day's expectation, so a fortnight off no longer reads as a fortnight of under-reporting. And it is never work: it is absent from the time report, the cost report, the status report, the calibration, the Gantt's actual bars and Time logged on the dashboard. A holiday cannot end up in an invoice.

Tip: Because it is both, the total says which is which — 37h 30m recorded of 5d · of which 30h absence. One figure answering two questions would be wrong for both.

Tip: Sick leave and a child's sick day are separate on purpose. They are different entitlements, counted separately by every employer that has to count them at all — and filing one as the other is the sort of thing nobody notices until a report is wrong.

Tip: Correcting a logged hour into a holiday works, and repairs the issue's total on the way out. Opening an absence for correction shows the kinds, not a picker of issues.

Tip: It is a record, not a request. Nothing is approved, no balance is tracked, and a week of holiday does not yet reduce what the Gantt thinks you have available.

Changed: hours no longer go on an epic

Hours belong to the work, not to the container. An hour logged against a plan with twenty issues under it cannot be attributed to any of them: it lands in the project's total and in nobody's estimate, so the only figure it feeds is the one nobody can act on.

An issue or a meeting, and nothing else. Epics are gone from the timesheet's picker, the epic's own page has no clock and no plus, and both the log endpoint and the timer endpoint refuse with a reason.

Tip: Hours already logged against an epic are left alone — still on the timesheet, still correctable and still removable by whoever wrote them. There were twenty such entries and eleven days of real work when this landed, and closing a door is not a reason to freeze what came through it before.

Tip: The clock is refused where it starts, not where it stops. A timer that ran for two hours and then logged nothing is worse than one that would not start.

What this release does not do

Absence is recorded on the timesheet only — the API's time endpoints still take an issue or a meeting. The five kinds are fixed rather than per-workspace. Nothing reduces capacity: a week of holiday is visible on the timesheet and invisible to the Gantt and to cycles, which is the next thing worth doing.


Version 0.75.0

Date: 31 August 2026

The board shows the work that is live.

Changed: epics are no longer cards on the board

An epic is an issue, which is why it sat among the issues — and why it read as one more card rather than as the thing twenty of them belong to. It has a page of its own and a bar on the Timeline, and now it stays there.

Tip: The List view leaves them off too, for the same reason. Filtering the type to Epic brings them back on either.

Changed: work finished over a fortnight ago leaves the board

The Done column grew without limit, so the one thing on it worth reading — what landed this week — was at the bottom of a hundred cards.

Tip: This needed a date the product did not have. updated_at is not it: that moves when somebody edits a title, adds a label or logs an hour, so a six-month-old issue tidied yesterday would have come back and one closed last week would have stayed for ever. Issues now record when they closed, and the existing ones were filled in from their own status history rather than left blank.

Tip: Reopening an issue clears that date, so it comes straight back to the board. Cancelling something that was already done keeps the first one — the tidy-up is a correction to the record, not a second closing.

Tip: An issue with no closing date stays on the board. Absent is not old, and a card that vanished for a reason nobody could see would be worse than a long Done column.

New: the board says what it is not showing

A strip at the end of the columns names each reason with its own count — 3 cancelled · 9 epics · 12 finished over 14 days ago — and pressing one lifts that default on its own.

Tip: Pressing it does not touch your filters. Lifting a default and narrowing the board are two separate statements, and the first used to overwrite the second.

Tip: One total across three reasons would leave you guessing which explains the gap you noticed, which is why each is counted separately.

New: the project on a card, with a way into it

Cards carry the project beside the epic, in its own colour, and both are links.

Tip: There was a badge for this already and it had rendered on no card for some time: its condition was written when the project filter held a single value, and stopped being true when the filters became lists. The condition it was reaching for is kept — with the board narrowed to one project, every card would say the same word.

What this release does not do

The fortnight is not configurable, and neither of the new defaults is a per-person setting: the board leaves this work off for everybody in the workspace, and the way to see it is the strip or a filter. Cancelled work is still absent from the List view's own default only insofar as its column is gone from the board — the list shows it.


Version 0.74.0

Date: 31 August 2026

Changed: the board draws no column for cancelled work

Cancelled work is not work. A handful of abandoned issues used to take a full-width column beside the empty ones and read as the busiest thing on the page.

The column is gone. What was left off is counted where it used to be — 3 cancelled, not shown on the board — with Show them to bring them back.

What you'll notice

  • One fewer column, and the board is about work that is still live
  • The count sits at the end of the columns, which is where you look for it
  • The List view is unchanged and still shows everything

Tip: Show them works by filtering to the cancelled statuses, so it appears as a filter chip like any other and Clear puts the board back. There is no second hidden setting to remember.

Tip: It reads your workspace's own classification, not the word. Rename Canceled to Dropped and it is still left off; classify On hold as cancelled and that one is left off too. Statuses are data everywhere else in Félagi, and this is no exception.

Tip: An explicit filter always wins. If you filter to a cancelled status the column is drawn — a filter the board then refuses to honour is a filter that looks broken.

Tip: The board has no status menu on a card, so dragging was the only way to cancel something there and that way is now gone. The issue's own page still does it, and the List view can do several at once with the selection bar.

What this release does not do

There is no per-person preference for this — the board hides cancelled work for everybody in the workspace, and the way to see it is the filter. Finished work still gets its column: done is the evidence of progress, and cancelled is the absence of it.


Version 0.73.1

Date: 29 August 2026

Naming a colleague in a comment.

New: type @ and pick somebody

The comment box has an @ picker. Start typing a name, use ↑ ↓ to move, Enter or Tab to choose. The colleague gets an inbox row saying they were named — whether or not they were following the issue — and starts following it from then on.

What you'll notice

  • Colleagues and agents in one list, saying which is which
  • Escape closes it and leaves what you typed alone
  • Typing a name by hand still works, exactly as before

Tip: The delivery half of this has worked for a long time — the missing half was that you had to spell the name exactly and nothing told you so. If a mention of yours never seemed to arrive, this is probably why.

Tip: Naming an agent hands it the thread and queues a run. That is a different act from telling a colleague, and the two look identical while you are typing — so the list marks the agents runs before you press anything.

Tip: You cannot name a director. The Management role has no inbox, so a mention would file a row nobody can open and send an email pointing at a page that refuses them.

Fixed: a shared name reached the wrong person

With two colleagues called Knut, @Knut quietly notified whichever of them had the longer full name — and told the other nothing. The same was true of two people with the same whole name.

Now a name more than one person answers to reaches neither, and no inbox row is filed.

Tip: Reaching nobody is the better failure. Telling the wrong colleague looks exactly like telling the right one, and the person who was actually needed never finds out they were named.

Tip: For those, the picker inserts an exact form — @user:12 — and shows the address beside the name so you can tell two Arne Bergs apart while choosing. The posted comment reads @Arne Berg like any other; the key is only ever visible in the composer, and only where a name genuinely is shared.

Tip: It also applies across people and machines. An agent called Arne in a workspace that has a colleague called Arne makes @Arne a sentence with two readings, and Félagi will not pick one for you. Agent names are already required to be unique; this is the same rule, one step wider.

Fixed: the picker offered private agents

The list showed agents hired privately by somebody else — and naming one would have queued a run on an agent you are not allowed to see. It reads the same visibility rule the assignee picker does.

What this release does not do

The picker is on the issue comment box only. The description, the comment box on a whiteboard note and the knowledge base do not have it yet, and mentions are still resolved by matching the text of a name rather than by a handle somebody chooses for themselves.


Version 0.73.0

Date: 29 August 2026

The Timeline, from a manager's chair.

New: group the Timeline by person

A leadership group asks five questions in order — what are we working on, will it land, who is on it, what is X working on, and where are the issues inside. The Timeline answered the first two. It answers all five now.

By project · By person · Flat sits beside the zoom. Each person's heading carries what a manager asks in the order they ask it:

Arne Berg · 22 open · 1 overdue · 7d 7h · 1.7w · +5 unestimated · 8 epics

What you'll notice

  • Every epic row now shows who is carrying its open issues — three faces and a number, busiest first
  • Grouped by person, an epic appears under everyone working in it
  • Machines are counted beside people, in the colour Félagi uses for them everywhere else
  • By person is restricted: owners, admins and management

Tip: The weeks figure divides estimated hours by the hours somebody actually has for plannable work, not their contracted week. A person with a standing support rota does not have their whole week for this, and a plan made against the contracted number was never possible.

Tip: Unestimated issues are counted beside the hours, never inside them. Three unestimated issues on a plate is not a light plate, and a figure that quietly omits them makes somebody look free.

Tip: An epic under three people is one epic three times, on purpose. "What is Arne working on" has to show all of his, whether or not somebody else is on two of them.

Tip: Unassigned work has no face on a row, and shows as in the issue list. An epic somebody is asking about with unassigned work inside it is a finding, not a gap in the chart.

New: open an epic to see where its issues are

The chevron beside a title opens it in place: the epic's own issues as thinner bars on exactly the same axis, each with whoever holds it, its number and its title. Red means past its date, blue means started, amber paused, grey closed or not begun.

Tip: Grouped by person, opening an epic shows their issues in it, not everybody's.

Tip: Issues with no dates — or dated outside the window — are counted under the bars rather than dropped. Work that cannot be placed in time is the finding, the same reason the unscheduled epics are listed by name.

Tip: Nothing is fetched for an epic nobody has opened, so a Timeline you never expand costs exactly what it did before. Open ten and it costs ten more queries, not ten thousand.

Changed: the Standup report is open to management

It moved from the owners and admins gate onto a new one that names the question: who may read who is behind. Owners, admins and management have it; a developer deliberately does not.

Tip: That is the same split the roles have always drawn. Running the agent workforce and reading people's performance are different kinds of trust — a contractor can be handed the machines without being handed the team's standing. The old gate was simply the nearest one that existed when the report was written.

Fixed: a button with no name

The chevron that opens an epic carried its label as a bound attribute, which Blade only evaluates on a component — on a plain button it is emitted as an attribute literally called :aria-label. It shipped for an hour with no accessible name at all.

Tip: A sweep now reads every view for that shape and can tell it apart from Alpine's identical-looking bindings, which are correct and everywhere: the difference is that Alpine's expression is JavaScript and Blade's is PHP, so it looks for PHP inside the value.

What this release does not do

The Timeline still cannot be filtered — every epic in the workspace is on it, and narrowing by project, label or cycle is what Reports are for. Grouping by person shows open work only, so somebody whose plate is empty has no heading. And nothing on the page can be edited.


Version 0.72.0

Date: 28 August 2026

Milestones.

New: a milestone is a name, a date and a status

An epic's page has a Milestones section. Each one is a checkpoint somebody has committed to — Beta with two customers, Legal sign-off, Production — and it appears as a named diamond on that epic's bar on the Timeline.

That is the whole object. Everything a milestone could also have — an owner, an estimate, a conversation — is a reason to have filed an issue instead, and the epic already holds those. This exists so a plan can be stated without inventing a piece of work to stand for it.

What you'll notice

  • Three statuses: Planned, At risk and Reached
  • Missed is worked out, not set. Past its date and not reached reads as missed
  • Marking one reached records today, and the gap to its target shows beside it as +10d
  • On the Timeline: filled green for reached, hollow red for missed, amber for at risk
  • The Timeline's panel lists them, which is where somebody who cannot open the epic reads them
  • An optional note, for what has to be true before it counts as reached

Tip: At risk is the only status arithmetic cannot produce, and the reason the column is worth having at all. Everything else on the list can be inferred from a date; "somebody has looked at this and thinks we will miss it" cannot, and it is the only early warning a plan can carry.

Tip: Missed is derived on purpose. A stored one would still say missed after somebody moved the date — which is exactly the moment a plan most needs to be read correctly.

Tip: Correcting a target date after a milestone was reached keeps the day it actually happened. That is how a plan gets corrected after the fact, and the gap between the two dates is the only honest measure of how it went.

Tip: An epic with no dates of its own now takes its span on the Timeline from its milestones. Three committed dates are a plan; that epic used to sit under Not scheduled while carrying the clearest one on the page.

Tip: A milestone dated after the epic's own due date is drawn past the end of the bar, on purpose. It is a plan that does not add up, and stretching the bar to cover it would hide exactly that.

Tip: Epics that have no milestones still show their dated issues on the Timeline, drawn smaller and fainter and named as such in the legend. Adopting milestones is not a cliff — and the two are never mixed on one bar, because a milestone is a decision and a dated issue is the work that gets there.

Tip: Taking an epic off the board keeps its milestones. They come back with it.

What this release does not do

Nothing is sent when a milestone is reached or missed — subscribers hear about the epic, not its checkpoints. They are not in the API. Nothing depends on one: a milestone does not block work, hold a status or gate a release. And they belong to epics only, so a project-level milestone spanning several epics is not expressible.


Version 0.71.1

Date: 28 August 2026

The Timeline, redrawn.

Changed: the Timeline reads as a timeline

The label column is gone. Each epic's title now sits above its own bar and moves with it, which gives the whole width of the screen to time — and turns a stack of bars into a shape you can take in at a glance rather than a table that happens to have bars in it.

What you'll notice

  • Two-tier axis. The month over the day each week begins on, the quarter over the month, the year over the quarter — so a column can be a week without the header becoming a list of dates nobody can place in a year
  • Today is a dated pill, not just a line. A column label that would sit under it steps aside
  • Bars are earliest first, so each one begins at or after the one above it
  • Diamonds on the bar for the epic's own dated issues, with each issue's name underneath
  • A dashed red tail from an epic's deadline to today, when it has run past it
  • Health is still said in words beside every title, not only in colour

Tip: The diamonds are not milestones — Félagi has no such object. They are the child issues inside the epic that carry a due date, which is what somebody actually means when they ask what happens between now and the deadline. Filled means closed; hollow and red means its date has gone by.

Tip: Five diamonds per epic at most, and when there are more it is the five nearest today, with the rest counted as +N more at the end of the bar. That is the half a meeting is about. Hover any diamond for the issue's full title and date.

Tip: The bar still stops at the deadline when an epic is late, because that is what was promised. The dashed tail is the distance since — the difference between a chart that says late and one that says how late.

Tip: Red is spent on lateness here, so today is drawn in the accent colour rather than in red as it is on the Gantt. Two readings in one colour on one chart is one reading lost.

Tip: Want a diamond to say something short? Name the issue short. The label gets the width of the gap to its neighbours and truncates to fit, so Beta survives where Pilot with two customers in the Nordics becomes an ellipsis.

Fixed: a sort that was not sorting

Three places in Félagi sorted a list by several criteria at once using a form that quietly does something else: a closure handed to sortBy([...]) is treated as a comparator, not as a way of reading a value. So the criteria were called with one item, returned a date or a number, and the result was compared as though it were the answer.

Tip: If the member list at Administration → Members has ever looked oddly ordered — an admin below a developer, names out of order within a role — this is why. It sorts properly now, and by asking the role for its own order rather than keeping a second copy of it.

Tip: Also how the crash fixed in 0.71.0 hid for as long as it did. Because the criterion only ever saw one of the two rows being compared, a workspace with two members never reached the missing case — so the first test written for that bug passed against it. Three members reached it, and so did every size above.

What this release does not do

The diamonds still cannot be named or dated independently of the work: a milestone object, with its own name, date and status, is a bigger change than a chart. Nothing on the Timeline can be dragged, and no avatars appear on the rows — who owns an epic is in its panel.


Version 0.71.0

Date: 28 August 2026

A role for people who read the work rather than doing it, and a page for them to read it on.

New: Management, a role that is not a smaller Member

Administration → Members offers a fifth role. Somebody invited as Management gets the Timeline, the reports, the manual and their own account settings. No board, no epics, no projects, no inbox, no timesheets, no meetings, no knowledge base, no whiteboard, no agents, and no administration.

The other four roles are a ladder — each does everything the one above it does. This one is beside the ladder rather than at the bottom of it.

What you'll notice

  • Signing in lands on the Timeline instead of a dashboard
  • The sidebar has three entries, and no ⌘K search — there is nothing behind it they can open
  • They are never offered as somebody to hand an issue to, on any screen
  • The Gantt is not among their reports; the Timeline is what they have instead

Tip: This is what makes the role worth having rather than just restrictive. A director handed a board and an inbox turns up in the Workload report as somebody carrying nothing, and that makes the report wrong for everybody who reads it. Management cannot be assigned work at all — not from a picker, not through the API, not by an agent.

Tip: Invite by what somebody will do, not by their title. A head of department who actually picks up issues is a Member. Management is for the people who ask how it is going.

Tip: An API token issued by a Management account is refused with a message that says why. The whole API is about issues, and the role has none — so the token section is not offered on their settings page either.

Tip: Changing somebody to Management does not touch anything they wrote. Their comments, their hours and their name on past work all stay exactly where they are.

New: Timeline — every epic against the calendar

A new page in the sidebar for everybody, not only Management. One bar per epic across the coming months, filled as far as its issues are closed, with the ones that will not land on their date coloured before anybody asks.

Zoom to weeks, months or quarters; move the window with the arrows; group rows sit under their project, and whatever has something late leads.

What you'll notice

  • Past its date, behind the pace, on track, not started and finished, each said in words as well as in colour
  • An epic with only a due date is still drawn — at that date
  • An epic nobody dated whose issues carry dates is drawn across them, and says so
  • Not scheduled lists, by name, the epics that cannot be drawn at all
  • Clicking a bar opens a panel: project, status, dates, owner, progress, and how many people outside the company are following it

Tip: Behind the pace means less of it is closed than the calendar has spent, by more than 25 points. The threshold is generous on purpose — work does not arrive evenly, and a narrow one would paint half the page amber every month and teach everybody to ignore it.

Tip: The Not scheduled list is the half of a portfolio a timeline usually hides, and it is the half worth reading. Work nobody has committed to a month is exactly what a management meeting should be asking about.

Tip: Clicking a bar opens a panel rather than the epic's page. Deliberately: the role this page was built for has no issue pages at all, so a link would refuse the person most likely to press it. Anybody with the board can still reach an epic from Epics.

Tip: Every control is in the address. A link to one particular window — quarters, two quarters out — is a link that can go straight into a board paper.

Tip: Export on the reports index gives every epic with its dates, progress and health as a spreadsheet — the whole portfolio, not the window on screen.

Fixed: the administration page no longer breaks when a role is added

Adding a fifth role made Administration → Members throw for any workspace with three or more people, on the page you go to in order to add one. It sorted its rows with its own copy of the role order — four cases, no fifth — instead of asking the role.

Tip: Worth knowing how that kind of crash hides: with exactly two members the sort never compared the new role at all, so the first test of it passed against the bug. Three members failed, and so did every size above.

Tip: One more of the same shape, fixed before it shipped: the check for may this person run the agent workforce asked "is this not a Member", which would have handed the entire administration section to the new role for the sole reason that it was new. It is a list of the roles that may, now.

What this release does not do

Management is a workspace role, so somebody can be Management in one workspace and a Member in another — that is deliberate. It cannot yet be given a narrower slice: it is all the reports or none. The Timeline has no milestones, no dependency arrows and no initiative level above the project, because Félagi has none of those objects to draw.


Version 0.70.19

Date: 28 August 2026

Three things you touch every day: filing an issue, confirming a deletion, and reading this page.

New: paste a screenshot straight into a new issue

Copy an image — a screenshot, a region, a picture from a browser — and press ⌘V anywhere in the New issue form. It attaches to the issue you are filing, before that issue exists.

Reporting something visual used to mean saving the screenshot to disk, filing the issue, finding the file again and uploading it. Three of those four steps are gone.

What you'll notice

  • The paste works from the title field, the description, or the form's empty space
  • The attachment appears in the list with its size, and can be removed before you file
  • The image is named for when you pasted it, so several in one issue stay apart
  • Pasting text is untouched — only image data on the clipboard is intercepted

Tip: The clipboard is a screenshot's shortest path. On macOS ⌘⇧4 copies a region straight to it with held; on Windows ++S does the same by default. Neither writes a file you then have to find.

Tip: This is the issue form only, for now. The comment box, projects and knowledge pages still want an upload — they are next.

Fixed: a confirmation closes when you confirm it

Removing a second entry from a timesheet asked for confirmation, removed the entry, and left the dialog standing over the page. It closed on the first removal and on every removal from a fresh page, which is why it read as random.

The rows had no identity of their own, so after the first removal the page reused the second row's dialog for the third row's — and the button you pressed belonged to a dialog that no longer existed.

Tip: If you have met this, the entry was removed. The dialog was over a page that had already changed underneath it; Escape or a click outside dismissed it and nothing was left half-done.

Fixed: What's new opens on what's new

This page now opens showing the releases you have not read — three at a minimum, ten at most — with Load older releases underneath, saying how many are left.

It used to render all 127 entries on every open: 4,971 elements, 771 KB down the wire, and half a second in which the page answered nothing, for a dialog most people close after the first screen. It is 114 elements and 18 KB now, and the browser does not stall.

What you'll notice

  • The dialog opens immediately, and on the release you are actually being told about
  • Older releases arrive ten at a time, and the oldest is still reachable
  • Closing it lets go of everything it drew

Tip: The line above the older ones marks where you had read to. Everything above it is new since you last opened this dialog; everything below you have seen before.

Tip: Skipped several releases? You get all of them, up to ten, rather than three — the minimum is there so the dialog reads as a page of history even when only one thing shipped.

Tip: Full history is in RELEASENOTES.md in the repository, and that file is what this dialog reads. Nothing is summarised or dropped on the way here.


Version 0.70.18

Date: 28 August 2026

Two things in the Issues header.

Fixed: the filter search box now takes your first keystroke

Opening Filter puts the cursor in Find a filter…. Type, use ↓ and ↑ to move through the matches — the highlight moves and the cursor stays where you are typing — then Enter to apply and Escape to close.

Until now the box was visible and did nothing with the first letter you typed. Focus stayed on the menu, so the letter drove the menu's own jump-to-first-letter instead of arriving in the field: typing pro looking for a project walked you to Person…, where a later Enter could apply something you never chose.

Tip: Half of this was fixed in 0.70.6 — the part where the box took one letter and then lost focus. The other half survived because the check for it clicked the box before typing, which is the one thing a person opening a menu does not do.

Tip: A search box that repurposes your keystroke is worse than no search box. The box was never the problem; nothing focused it.

Fixed: the toolbar no longer rearranges itself when you filter

Search, Filter, Clear, Views, the view tabs and New issue stay where they are whether nothing is applied or six things are, and the header keeps one height.

It used to move on every filter change. Applying one filter shifted Search 440 px to the left and New issue 308 px; applying a second shifted them back as the filter chips wrapped to a line of their own, and the header grew from 56 px to 118 px. The button you were about to press had moved a third of a screen.

Tip: Search moved furthest of anything — and it moves while holding your typed text and your cursor. The ordinary loop is search, look, narrow with a filter, refine the search; the field you were about to return to had gone somewhere else.

Tip: Chips that do not fit now collapse into a +3 beside them, which opens a list of the rest — each removable from there. One row, one height, and no label is ever cut mid-word.

Tip: Clear now sits to the right of the count it undoes, and appears when there is something to clear without moving anything beside it.

Tip: Filtering a board changes how many cards it holds, so the page scrollbar used to come and go — and 15 px of viewport width moved every right-anchored control in every header. Room for it is reserved now, everywhere in the application.


Version 0.70.17

Date: 28 August 2026

Fixed: a release no longer shows anybody an error page

Nothing in the application changes. What changes is that a release stops deleting the files the running site is reading.

Preparing the templates for a release cleared the compiled ones first and then rebuilt them, and the site keeps serving throughout — so for as long as that took, requests could arrive for a file that had been there a moment before. Three did during the last release.

The previous attempt at this narrowed the window and could not close it. It rebuilt the part that was missing after the clearing finished, and the clearing happens at the start — so the gap was the whole length of the job rather than the moment at the end. It has now been measured: 2.1 seconds, 754 templates.

Nothing is deleted now, so there is no window to be caught in.

Tip: For an operator: a release no longer runs view:cache. It compiles the templates and the components without clearing either. view:clear still exists for sweeping files whose template is gone — housekeeping, at a moment you choose, rather than in the middle of every release.

Tip: A test compares what the new step produces against what view:cache produced. The one thing it must not do is compile a different set of templates: that would trade a visible fault — a few error pages during a release — for an invisible one, where something is quietly never compiled at all.


Version 0.70.16

Date: 27 August 2026

The whiteboard, brought up to what a board is expected to do.

Things line up

Drag a note and a blue line appears when an edge or a centre meets another element's — that is where it will land. Nothing to line up with, and it lands on the grid the surface draws, so a board assembled over an afternoon is not a few pixels out everywhere.

Tip: Hold to put something exactly where you mean it, snapping ignored.

Named areas, and a board that starts as something

Frame (F) draws an area with a title, and it carries what is standing inside it — move the frame and the notes come along. That is the difference between a frame and a box: a box says these belong together and then stays behind when you rearrange the board.

Board → Start from a template lays out a retrospective, a flow, a decision or a plan.

An empty surface is the hardest thing here to use. Everybody knows what a retro is and nobody wants to draw three columns and label them first — so the first five minutes of every session went on the same rectangles, badly aligned, by whoever opened the board.

Tip: A template lands to the right of whatever is already there, never on top of it. It is most useful halfway through a session, when somebody says let us structure this.

Arrows stay between the things they join

Draw one from a note to another and it stays fastened: move either, or resize either, and it follows, meeting the edge of what it points at.

An arrow is a line between two points, and points do not follow — so before this a diagram was right until somebody touched it, which is not what a whiteboard is for.

Tip: An end pointing at something that was deleted stays where it was. A loose end where the note used to be is readable; a line snapping back to the corner of the board is a mystery.

Group, lock, and stack in both directions

Group two or more things and selecting any of them selects all of them — no more rubber-banding the same five notes. Ungroup takes it apart.

Lock makes something scenery. A box drawn round some notes gets dragged by accident every time somebody reaches for what is on top of it; locked, it stays put and Delete skips it.

Send to back exists now. Only Bring to front did, so anything covering what you wanted was dealt with by raising everything else in turn. ⌘] and ⌘[.

Tip: A locked element still selects, or there would be no way to unlock it, and shows a small padlock — a background that will not move and does not say so reads as a broken drag.

Saying why, not just how many

A thread on a note, opened from the speech bubble at its corner. Votes count how many of us care about something; they cannot say why — and the why is what somebody reads a board for a week later, when the argument is gone and the note is three words.

Tip: You can remove your own comments; an administrator can remove any. Words a colleague can quietly remove are words people stop writing.

Knowing where you are, and taking the board with you

A minimap, bottom right, showing everything on the board and a rectangle for the part you are looking at. Click it to look elsewhere. An endless surface makes it possible to be nowhere: pan far enough and there is nothing on screen and no direction that obviously leads back.

Board → Export as PNG gives you the whole board rather than the part that happens to be on screen. A board is a conversation, and the conversation usually has to be reported to somebody who was not there — so the answer to what did you decide was a screenshot of a third of it.

Tip: Press ? and the list now includes the board's own keys while you are on it, which is where somebody looks.

Not there yet: PDF. The honest route is the browser's own print dialogue, and printing this page today gives you the sidebar and a clipped board. That wants a print stylesheet rather than a button, so there is no button.


Version 0.70.15

Date: 27 August 2026

Fixed: setting up two-factor authentication was impossible for some accounts

An account that had to set up a second factor was supposed to be sent to the setup page. It was sent an error page instead — on every page it tried, so there was no way through and no way to do the one thing being asked. It had been happening for a week: 768 error responses in the log.

The redirect was asked for rather than built, and what answered was not the kind of thing a redirect has to be. It is built now, so it cannot be the wrong kind whatever else is going on in the request.

Tip: Nothing was at risk and nothing was let through. The requirement held the whole time — it held people at an error rather than at the page that would have satisfied it.

Tip: Found while verifying the previous release, not reported. Reading the log after a deploy is the only reason anybody knows: an error page on the way into two-factor looks like a broken product rather than a fixable bug, and nobody had said anything.


Version 0.70.14

Date: 27 August 2026

Whiteboard.

Fixed: elements you could not delete

An element that ended up underneath the toolbar could not be deleted, and could not be moved or selected either. Nothing was wrong with deleting: the toolbar, the palette and the other floating panels take the pointer, so anything beneath one of them cannot be clicked — and everything you can do to an element starts with selecting it. A note that landed there was stuck, with nothing on screen to say why.

Fit to content is the way back, and it now fits to the part of the surface you can actually click rather than to the whole of it. It used to centre content in the full width with an even margin, and the clickable space is not even — the toolbar is on the left — so on a wide board it put the left-hand notes back underneath it. Fit could leave you exactly where you started.

Tip: It also counts drawings and arrows now. They were left out of the reckoning, so a board of nothing but pen strokes fitted to nothing at all.

Boards: a list instead of a dropdown

Boards in the header shows every board in the workspace, with what a list has to say to be an overview: what it is called, how much is on it, who made it, when it last changed, and whether anybody else can see it. Search it by name; click a row to open it.

A dropdown of names could answer none of that — and with four boards called Untitled board, not even the one question it was asked.

Tip: It was also capped at fifty. Past fifty boards the oldest could not be opened at all: the link worked, the board was there, and the page came back empty. A board is looked up directly now.

The title is the name

Click the board's name in the header and type. Enter or clicking away saves, Escape puts it back. A new board opens with the cursor already in its title, so naming it is the next keystroke rather than an errand — which is why boards stayed called Untitled board: renaming was a modal behind a menu.

Tip: Until you name it, the second one is Untitled board 2. Two boards with one name is a list that cannot help you.

Tip: Nothing about saving changed, because nothing needed to: everything on the surface is written the moment it happens.


Version 0.70.13

Date: 27 August 2026

Fixed: a tab left open since before 0.70.6 failed on every click

If the Issues board had been sitting open in a tab since before filters took several values, everything you did in it failed. Not the board — that tab. Opening the page again fixed it, which is why it looked like a one-off.

Filters that used to hold one value now hold a list, and a tab that has been open across that release keeps sending the old shape. It was read as a list of nothing on a fresh page load and as a single value from a saved view — both handled — but a tab sends its state before either of those repairs can run, and that path failed outright rather than quietly.

It is read as the one value it is now, wherever it arrives from: a link, a saved view, a tab that has been open for a day.

Tip: Nothing was at risk. The board is a way of looking at issues, and the failure was in the looking. Reloading always worked.

Tip: Twelve of these reached the log before it was found, from one person's tab. It was the release before this one that surfaced them — a queue check that had been silent for weeks started reporting again, and reading the log for that turned these up.


Version 0.70.12

Date: 27 August 2026

Fixed: pages update on their own again

A board stopped moving by itself. Somebody else's change appeared only when you reloaded, which is why this was never reported as a fault: reloading always showed the truth, so it read as a product that simply did not do that.

Live updates are queued on a lane of their own, and the application server was consuming four of the five lanes. Nothing was draining that one, so the updates piled up and were thrown away at the next release. Fourteen were waiting when this was found, the oldest three-quarters of an hour old.

It is a setting on the server rather than anything in the application, and it is now written down along with the rest of the recommended configuration — including why that lane has to come first in the list, not merely be in it: a live update queued behind a batch import arrives minutes late, and for a live update that is the same as never.

Tip: Nothing was lost that mattered. The lane carries updates to pages that are already open; every one of them was a change already saved, and a reload always showed it.

A release now checks the queue before destroying it

Queued work lives inside the application server, so a release throws away whatever is still waiting. The check that lists it needs a token, and the release had been looking for that token under the wrong name in the wrong file — so on every release for weeks it printed nothing at all, which looks exactly like a release that checked and found nothing.

It reads the token from the file that owns it. A release now lists what is pending and asks before going ahead, and says so plainly when it cannot look.

Tip: This is also how the stuck lane above was found. A check that says nothing is indistinguishable from a check that passed, and both had been true for weeks.

Documentation

Running it gains Recommended setup — every setting with a worked example, and a table of what each one costs when it is wrong — and The admin token: where it lives, what it unlocks, and what it deliberately does not.


Version 0.70.11

Date: 27 August 2026

Nothing changes in the application. Two things change about how a release checks itself, and one of them was quietly not checking anything.

Fixed: a release now proves it replaced the running process

The last step of a release read worker process ids from the application server's admin interface to prove the old process was gone. That interface requires a token now, so it answered unauthorised, and the step printed "could not read worker pids, so the restart was not verified" and carried on. It had done that on every release for weeks.

It asks Docker instead — the same tool the release uses to do the restart — and compares the container before and after. A release where the process was not replaced now fails rather than warning.

Tip: That is the point of the change. A verification step that degrades into a warning is worse than one that fails, because nobody reads the seventh warning.

And a release says when it cannot check the queue

Sessions and the response cache moved to the database in 0.70.5 and survive a release. The queue did not: queued jobs live inside the application server, so a release destroys anything still waiting.

The check that would list them needs the same token, so it was being skipped — in complete silence, on a release that looked identical either way. It now says so.

Tip: For an operator: set ADMIN_TOKEN in docker/.env and the queue is checked before a release, with the option to wait. Left unset, a release may drop queued work — which is worth deciding on purpose rather than by default.


Version 0.70.10

Date: 27 August 2026

Fixed: a deploy showed a few people an error page

Caching the templates during a release deleted the compiled components and did not put them back, and the workers were still serving for the thirteen seconds it took them to restart. A worker that has already drawn a page remembers where its compiled files are, so it went straight at files that had just been removed. Four requests failed that way during the last release.

A deploy now compiles them again before restarting anything.

Tip: This closes the window rather than shortening it. The compiled files are named after where their source lives, not after what it contains, so building them again produces exactly the files the running workers are still asking for. A test holds that property, because names derived from contents would make the step useless without changing anything it reports.

Tip: The same shape was already handled once, for stylesheets: a worker holding an asset list from before the build serves a page referencing a file that is no longer there. That is why the build runs before the restart, and why a release asks for the stylesheet ten times afterwards before calling itself done.


Version 0.70.9

Date: 27 August 2026

Fixed: the meetings page answered 500

Any visit to Meetings in a workspace with something scheduled failed. It had been failing since 0.70.6 shipped.

Removing the attending replies took a method with it and left one caller behind — on the row that draws each upcoming meeting, which is only drawn when there is one to draw. The row read 2/6, accepted over invited, and the accepted half no longer existed.

It now says how many people are coming, which is the whole answer since 0.70.6: somebody who cannot make it comes off the list rather than replying to say so.

Tip: Nothing was lost or written wrongly — the page could not be drawn, and everything behind it was untouched.

And the reason nothing caught it

Every test that touches that page mounts it and then schedules a meeting through the form, which leaves for the new meeting rather than returning to the list. So the list was only ever rendered empty — the one shape in which the broken row is never reached.

Two guards were missing rather than one, and both are now in place:

  • The page is rendered with meetings on it, an upcoming one and a minuted one, and the attendee count is asserted.
  • Every page under /app is now rendered by somebody who is allowed in, with records present. The existing sweep walked the same routes but asserted only refusal — a redirect for a signed-out visitor, a rejection for an unverified account — so a page answering 500 to the person it is for passed it.

Tip: The new sweep takes its records from the demo seeder, so it gets richer as the seeder does instead of drifting behind it. That turned up a third thing: the seeder had been broken since meetings were added, so restoring a local database from it stopped part way. A workspace can be asked for its meetings now, which is all it was missing.


Version 0.70.8

Date: 26 August 2026

Every applied filter is on the page, and each comes off on its own

With two filters applied there was no way to see what was narrowing the board without opening the dropdown and scanning for ticks. A person and a project both applied, and the project one was visible nowhere — its only evidence a tick three groups down inside a menu somebody has to open.

Applied filters are now a row of chips under the controls, labelled dimension: value, each with an × that removes only that one.

Clear keeps its job of removing everything. What was missing is removing one, which is the common intention once a board has been narrowed too far.

Tip: One chip per value, not per dimension. Two labels are two chips — labels narrow with AND, so dropping one and keeping the other is the whole point.

Tip: The order is fixed, so a new chip lands at the end and nothing already on screen moves under a cursor on its way to an ×. The value truncates and the dimension never does: Project: Inside NEXT Datacen… still says what kind of thing it is, where …enter - Eviny does not. Full value on hover.

Tip: Every × is a button, so the row works from the keyboard, and each one says which filter it removes rather than eight identical Removes.

Tip: The search box has no chip. It sits on the same row with its own clear, and it was never the filter you had to open something to see.

The Filter button counts instead of naming one filter

It read Person: Knut W. Horne with a project filter also applied, which does not merely omit the second one — it states the first as though it were the whole answer. Before that it read Filtered, which said less and implied nothing, and was the better of the two for it.

A label cannot express a set, so it stopped trying. Filter, or Filter (2), and the number agrees with the chips.

Fixed: a board could leave the saved view it was still naming

Apply My open bugs, tick one more status, and the Views button went on naming the view while the board showed something the view never described. Silently — which is the kind of thing somebody notices a week later and stops trusting the feature over.

It now reads My open bugs · changed, with a line underneath saying what to do about it: save it again to keep this, or reapply the view to go back.

Tip: Leaving a view is allowed rather than refused. Refusing would make a named view a worse place to start from than an empty board — the point of applying one is to get somewhere quickly and then adjust.

Tip: Ticking the same two statuses in the other order is not a change, and neither is applying a view saved before a filter existed. Both would otherwise announce themselves as changed the moment they were opened.


Version 0.70.7

Date: 26 August 2026

Attachments, from one report with three separate things in it.

Fixed: a markdown file could be refused for quoting a tag

An SEO report in markdown was rejected. Its body discusses what a page returns and contains the literal string <head>, so the type read from its bytes came back as HTML and the file was refused — while a 400-byte excerpt of the same document, with no tag-like text in it, uploaded without complaint.

The type depended on whether the writing happened to mention a tag, which is not a property of the file.

A text extension now overrules that one reading, and nothing else defers to the name.

Tip: It can only move a file towards text, so an extension still cannot talk its way into being a PDF or an image. A .png that is really a script is refused exactly as before.

Tip: The file is also filed as text rather than as HTML, which is what lets it be read in place instead of downloaded. Stored under the sniffed type it would have uploaded and then refused to preview — the same bug one step later, and a harder one to see.

HTML files can be attached

A generated report, an export, a saved page — refusing them only moved people to converting to PDF first.

The question was whether that is safe, and it is decided by how attachments are served rather than by what they are. Nothing is served inline except PNG, JPEG, GIF and WebP; everything else is handed over as a download. So an HTML attachment cannot run as though it were part of Félagi, and it lands on the same footing as the HTML that has always been allowed to arrive inside a ZIP.

Tip: SVG was already on that footing and was never displayed inline — worth saying because it looks like an exception and is not.

Tip: HTML is not shown inside the application either. Reading one in place is a separate question from storing one, and it was left for its own look rather than added as a rider.

Fixed: one refusal instead of two, and it now says something

Rejecting a file printed "That kind of file cannot be attached." twice, at three of the four places files can be dropped.

The message now names the type it read and what would have been taken, and keeps the sentence explaining that the type comes from the contents rather than the name — which is the part that explains why renaming the file did not help. Only the API used to say it.

Tip: The interface and the API say the same thing because there is one message now rather than two.

Tip: The dropzones say what will be taken. They stated the size limit, which is the one limit nobody hits, and named examples — "screenshots, logs and specifications" — which reads well and answers nothing when a file is refused.

Accepted: images, PDF, text, Markdown, CSV, JSON, XML, HTML, ZIP, Word and Excel, up to 25 MB.


Version 0.70.6

Date: 26 August 2026

A filter can hold more than one answer

Status, project and type take several values. Tick In progress and In review and the board shows both; tick two projects and it shows both. Labels always did.

Several values in one filter mean or, and two filters narrow each other — so "a bug or an exception, in Ecommerce" is one board rather than three trips through the menu. Which is how the questions actually get asked: what is on the go is two statuses, not one.

Tip: Labels are the exception, and always were: two labels ask for issues carrying both. An issue has one type and one status, so or is the only reading that returns anything; it can carry ten labels at once, so and is the one that narrows.

Tip: All statuses, All projects and Any type are gone. With ticks, none ticked already means all — and a row saying "no filter" made the menu read as though a value had to be chosen. The menu also stays open while you tick, because a filter built from three values should not cost three trips through the same menu.

Tip: Links written before this still work. ?status=done — a shared link, a bookmark, a saved view holding one value — opens the board it always did; the address bar writes ?status[]=done&status[]=in_progress once there are two. That was the whole risk in the change, because the way it breaks is silent: an unnarrowed board that still says Filtered looks like a working page rather than a broken one.

Assignee and cycle still take one value, deliberately. Both carry rows that are not values — unassigned, assigned to agents, no cycle — and ticking one of those beside a colleague's name asks a question with no answer.

Fixed: the filter search box could not be typed in

The box added in 0.70.4 took no letters at all. You typed "Knut" and read nothing.

A Flux menu runs a typeahead of its own: press a letter anywhere in it and focus jumps to the first row starting with that letter. It moves the focus during the keystroke, before the character is inserted, so the insertion landed nowhere — which is why this read as a dead input rather than a jumpy one, and why focus ended up on a row called Knut Horne.

Letters no longer leave the box.

Down arrow reaches the first match and Enter picks it, so a filter can be set without touching the mouse. Neither did anything before, for the same underlying reason: the menu navigates from whichever row it holds as current, and while the box has focus there is none.

Tip: Escape still closes the menu and Tab still leaves it. Only single characters and the down arrow are held back.

A repeating meeting's schedule can be changed

A series was writable once, at creation, and never again. An occurrence could be edited and the series could be stopped, so moving a weekly sync to Tuesdays meant stopping it and building a new one — which loses the history along with the pattern.

Edit the schedule changes the day, the time, the interval, how long it runs and how far ahead it generates.

Tip: What happens to the occurrences already generated is the same rule Stop repeating always gave: an untouched future occurrence is a plan and follows the new schedule, while anything with notes, with minutes sent, or cancelled is the record of a meeting people are coming to and stays exactly where it is. Past occurrences are never touched — Tuesday from now on does not make last Monday a Tuesday.

An issue's card names its epic

A card said which project it was in and not which piece of work it was part of, which is the more useful of the two once an epic has more than a handful of children. The badge links to the epic.

A finished epic is no longer offered when filing work

Whatever the workspace calls done or cancelled, an epic in either category is work somebody has declared over: filing new work under it either reopens a closed question or hides the work inside something nobody is looking at. It also keeps the list readable, since epics accumulate.

Tip: The epic an issue is already in stays on offer however it is classified. Dropping it would leave the picker empty while the issue plainly sits inside something.

Replies on meetings are removed

Who is coming is who is on the list. Somebody who cannot make it comes off it, and there are no longer four states and an enum saying the same thing less clearly.

Tip: Two consequences worth knowing rather than discovering. The calendar invitation no longer states a participation status and no longer asks for one — sending accepted on somebody's behalf would put a confirmation in their calendar that they never gave. And the guard against inviting somebody twice used to be keyed on their reply; it now reads from when each attendee joined the list against when the invitations went out, so an organiser adding a seventh person a week later notifies that person and not the six who already knew.


Version 0.70.5

Date: 26 August 2026

Fixed: deploying no longer signs everybody out

Every release logged everyone out. Shipping code does not change who anybody is, and with releases a few days apart this was most of the times anybody signed in at all.

The session store lived inside the application server's own process, and deploying recreates that process — so the sessions went with it. The cookie was never the problem: it decrypted fine afterwards and pointed at a session the server no longer had.

Sessions are in the database now, which survives a new container and a new host alike.

Tip: The application key was the first suspect and was innocent. It lives in a .env the deploy never touches — which is worth knowing, because a changing key is the other explanation for the same symptom, and it is the one that also breaks Remember me.

Tip: The switch itself signs everybody out one final time, because the old sessions were in the store being left behind. It happened during a deploy that would have done that anyway, so it cost nothing extra.

And the cache had the same problem, for a sharper reason

Inbound email and webhook deliveries write a short cache entry to record that they have already been handled. That cache lived in the same process, so a deploy forgot those — and a retry arriving afterwards was handled a second time.

Tip: So the risk was never a slower page. It was a duplicate comment from an email that arrived twice, in the minutes around a release.

Tip: felagi:check now warns when either store is one that does not survive a restart, expressed as a list of the drivers that do rather than the ones that do not — the store that caused this would not have appeared on any list of known-bad ones.

Not fixed, and separate

SESSION_LIFETIME is 120 minutes, so people still re-authenticate on a two-hour idle timer. That is a deliberate setting rather than a bug, and changing it is a security decision rather than a repair.


Version 0.70.4

Date: 26 August 2026

Four things on the Issues page, which are really one loop: narrow the board, open an issue, come back, narrow it again.

The board comes back as you left it

Filtering, opening an issue and returning through the sidebar or the breadcrumb landed on an unfiltered board, and the selection had to be rebuilt. It does not any more.

Tip: Nothing was ever lost on the way out. Filters live in the address bar — a narrowed board has always been a link somebody could send — and the two links back simply pointed at a bare /app/issues and dropped the query string. That is why this was a small fix rather than a state store.

Tip: It is remembered per person, not per tab. Two boards open at once share one memory, so the sidebar goes back to whichever you looked at last. That is the honest answer when "as you left it" is asked about two places at once.

A way out of a filter

Clear now sits beside the Filter control, and appears only when something is applied.

Tip: The case that made this worth doing: six columns reading zero, Drop an issue here across the whole board, and the word Filtered as the only explanation — in a workspace holding 131 issues. Reopening the menu to unset things one at a time is the worst moment to ask somebody which thing to unset, because they cannot see what is applied.

Type to find a filter

The filter menu has a search box, and it matches across every group at once — type Ecom and the project comes up, Knut and the person does, without choosing which submenu to look in first.

Tip: Two characters before it answers, and while something is typed the groups give way to the matches, so the menu stays short.

Tip: Picking a label adds it to the ones already chosen rather than replacing them, because labels narrow with AND — two of them ask for the overlap.

A bigger target on a card

Double-click anywhere on a card to open the issue. In List view the title is a link across the whole column.

Tip: Double rather than single, and deliberately. Cards are dragged, and a whole-card single click fights the drag it sits on — which is why the title was the only target in the first place. The identifier and title stay ordinary links, so a middle-click still opens a tab and hovering still shows where a card goes.

Not verified in a browser

The search box lives inside the filter menu, and whether a menu forwards keystrokes to an input inside itself is not something a test suite can answer. If typing there does nothing, that is the one item here to say so about.


Version 0.70.3

Date: 26 August 2026

Fixed: every progress bar read zero, and two reports answered 500

0.70.2 was wrong, and this puts it right. Yesterday's query change was correct on both databases the test suite runs and silently returned nothing on the one production actually runs — so every "N of M closed", every project percentage and every epic bar read zero for about ninety minutes, with no error raised anywhere. If you looked at a project between 11:20 and 13:00 and thought it had lost its progress, that was this.

The counts were never lost; only the reading of them was. Nothing needed repairing.

Tip: A wrong answer that draws a working page is worse than the 500 it replaced, which is the lesson being written down rather than merely fixed. The query is now the plainest expression that says the thing — the statuses a workspace calls finished, resolved before the query is built and passed in as values — and it is also the fastest of the three shapes this has now been.

Fixed: Reports → Time answered 500

A second and unrelated fault on the same page, and this one predates yesterday: the hours- per-project breakdown joined through issues to projects and grouped there, and that combination takes the database connection away mid-query.

It now aggregates on the hours alone and folds them into projects afterwards, which is what the issue breakdown beside it has always done.

Tip: It could not simply become an inner join, though that would have worked. An inner join drops hours logged against a meeting and hours on an issue nobody has filed under a project — and a report quietly missing rows is worse than one that fails loudly.

Tip: It also groups by the project rather than by its name, so two projects that share a name are two rows now instead of one.

And a check that runs where the tests cannot

felagi:check — the nightly integrity command — now asks whether the database still agrees with us about what "finished" means, by asking the same question two ways and comparing. A count that disagrees with itself is a nightly failure instead of a number nobody thinks to doubt.

Tip: This exists because the suite cannot catch this class of problem at all: it runs SQLite and MySQL, and production runs neither. Anything that only shows up on the real engine has to be checked on the real engine.


Version 0.70.2

Date: 26 August 2026

Fixed: every progress bar was asking the same question once per issue

The projects page took 1.36 seconds to render twelve projects. It now renders in a fraction of that, and — more to the point — the cost no longer grows with the number of issues in the workspace.

It was not the assets, and it was not one query per project. It was one query, asking the database "which statuses count as finished in this workspace" once for every issue row it looked at. Seven statuses, 127 issues, every index in place, and 7 ms of fixed cost per row.

Tip: This was never only the projects page. Every figure derived from completion paid it — an epic's progress bar, a project's, the cycle report, the throughput report. If those felt slow, that was why, and they are all fixed by the same change.

Tip: Nothing about what the numbers mean has changed. A status still means what its own workspace says it means: rename In review to QA and every count stays the same; re-classify it as completed and every count changes at once. That is the part a faster query could easily have broken, so it is asserted directly — one status classified as finished in one workspace and as work in progress in another, read correctly for each.

Tip: No caching was added. A cache would have hidden the cost rather than removed it, and invalidating it would have meant work on the write path of the most common action in the product — changing an issue's status. The query count was already right; it was the query.

Tip: The acceptance test is the curve, not the number. It asserts the database receives the same statement whether the workspace holds five issues or two hundred, because a faster single measurement is not a fix for something that grows.


Version 0.70.1

Date: 26 August 2026

Three things, all of them found by using the product and noticing that it said one thing and did another.

An hour recorded wrongly can be corrected

A logged entry was immutable: fixing a figure meant deleting the row and typing it again. PATCH /time-entries/{id} now takes the duration, the day and the note — any subset — and the issue page can correct an entry in place instead of only removing it.

Tip: Correcting is not deleting and logging again, which is why this was worth building rather than documenting the workaround. Re-creating an entry turns a stopwatch figure into a remembered one, silently, in exactly the way this product warns about counting the two together. It also has a window in which the hours do not exist, resets when the work was recorded, and changes the id under anything holding it.

Tip: So a correction changes the number and never the provenance. A timer entry stays measured however often its figure is fixed — the clock was running, and that is a fact about how the number was arrived at rather than about the number.

Tip: The day matters as much as the duration. An hour worked on Monday and typed in on Friday should say Monday, and the wrong day is as easy a mistake as the wrong figure.

Also fixed: editable was wrong in both directions. It read "is this a manual entry", so a stopwatch entry reported false — while the interface edited it happily and the reference promised timed hours were correctable. And somebody else's manual entry reported true, which no endpoint would have honoured. It now means editable by you, which is what a client can act on.

Tip: The timesheet could always do this. It was the issue page and the API that could not.

Filter → Person sits with the other assignee choices

The Filter menu opened with four ways to filter by assignee, fenced off by a divider — and the person picker was four screens below, beside Cycle and Labels. A divider says "this is the whole set", so naming a colleague read as something the board could not do.

It is now a row in that group, directly after Assigned to me, with the Agent picker beside Assigned to agents.

Tip: The names stay behind the submenu, which is what the submenu was for: thirteen of them in the parent menu would push everything under them off a short window. One row is not thirteen names.

Tip: While a person filter is active the menu says who — Person: Alex Berg — and so does the Filter button. A board narrowed to somebody with nothing on their plate should not read as a board that is broken.

Fixed: board filters were listed among the reports

A filter saved on the Issues page appeared under Saved views on the Reports page, under a link that led nowhere — no report answers to the board, so the row fell back to a link to the page it was already on.

Each view is now listed where it was saved.

Tip: Reports can be saved as views, and always could. A workspace sees only board views there because nobody has saved a report one yet, not because it is impossible.

Tip: The list also respects who may open what: a saved Standup view is not listed for somebody who cannot open the standup.

Also fixed: the delete button on that page had never worked — it called a method that does not exist on it, so every click threw. Nothing caught it because the only test of the page drew it rather than pressing the button.


Version 0.70.0

Date: 26 August 2026

Standing work: support and maintenance, without breaking every measurement

Some work never finishes. Support. Datacenter maintenance. A rota. A developer carries it as a share of their week — ten per cent, or one day — and until now there was no honest way to record that. The only tool was an issue that stayed open for ever, and it dragged every figure it touched along with it.

A new issue type, Generic, off by default and switched on per project where a team has some. Instead of an estimate it takes a Share of week: 10%, 1d, 4h.

Tip: The two forms are not the same promise, deliberately. A percentage is resolved against that person's own weekly hours, so ten per cent follows a colleague down to three days a week. 1d is a day whoever else's week it is, because "Thursdays on support" is a rota rather than a proportion.

Tip: It claims the share while the issue is in progress, and not a moment longer. The status is the switch — move it back to the backlog and the capacity returns to whoever was carrying it. Nothing to remember, nothing to expire.

Tip: A generic issue needs a project. Standing work is always somebody's share of one, and without it the hours have nowhere to be reported — which is half the reason for recording them. Finansportalen: 120h delivery, 32h support is the sentence this makes possible.

What it changes

Planning Somebody on 10% support has 36 plannable hours in a 40-hour week. The Gantt's projections, the bottleneck report and a cycle's capacity all use the smaller number
The timesheet Still expects the whole 40. Support hours are logged like any others, so netting them off would nag about hours that were worked and recorded
Progress Left out entirely — an epic's bar, a project's, a cycle's carry-over
Overruns No estimate, so never over one. What it can be is over its share, which is the more useful thing to notice

Tip: Progress is the reason this is a type and not just an issue nobody closes. An epic's bar is closed children over total children, so support sitting in one would stop it short of a hundred for ever, and the epic could never be finished. A thing that is not supposed to end cannot be behind.

Tip: A cycle does not carry it over either. It would have come out of every window with its carry-over count one higher, until the cycle report said "carried over forty-seven times" about the one issue for which that is normal — drowning the number for the issues where it means something.

Tip: Over-commitment is reported, not refused. If somebody's standing work adds up past their own week, the reports say so and let you decide — the same answer cycles already give for a plan nobody believes.

Tip: The share is the plan and the logged hours are what happened, so the two can disagree out loud. A support task with 10% on it that keeps taking a quarter of the week is worth knowing about, and that only shows if both numbers are kept.

Over the API

type: generic with a capacity_share of 10% or 1d, on POST /issues and PATCH /issues/{id}. The issue resource carries capacity_share and claims_capacity, both null on every other type.

Tip: Setting a share clears the estimate. A share and an estimate are two plans for one issue, and leaving both would mean every report picking one.

Tip: Logging hours needs no special case — POST /issues/{id}/time works exactly as it does for any issue.


Version 0.69.7

Date: 25 August 2026

Fixed: deleting a whiteboard froze the application

Deleting a board somebody else had made locked the page up — no error, nothing deleted, and a hard reload the only way out. Two separate faults, and the second was the one that hurt.

The delete was refused, correctly. A board belongs to its author, plus anybody who administers the workspace, and a colleague is neither. That part was working as intended.

The refusal was invisible and the crash was not. Confirmations are a modal, and a modal closes when the action inside it succeeds — so a refused action left it standing over a page nothing could be clicked on, with nothing said. The refusal is a sentence now, naming the board's author, so no request fails and no modal is left open.

And Delete board is no longer offered to somebody who cannot use it. A control that cannot work says the product is broken rather than that the board is not yours.

Tip: If a board changes hands while your menu is open, you still get the sentence rather than a wall. That is the case the hidden control cannot cover.

The same fault, found in the knowledge base

A guard written for the whiteboard fix found a second instance the same minute. Files in a knowledge article showed a Delete to every reader, including files somebody else had uploaded — and clicking one froze the page in exactly the same way.

The attachment list now asks each file who may remove it rather than trusting the page that drew it, which fixes the knowledge base, the project page, the issue page and every comment thread at once.

Tip: The rule has not changed: whoever uploaded a file, or an administrator. What changed is that you are no longer shown a button that will refuse you.

Tip: A file an agent's run produced has no human uploader, so for that one an administrator is the only person who can remove it.


Version 0.69.6

Date: 25 August 2026

Four things, and three of them came from somebody using the product and saying what was missing.

Filter → Person is for everybody now

It shipped in 0.69.3 for owners and administrators, and that was wrong. The release note that came with it gave the reason as menu length, which the submenu it sits in had already dealt with — one row, the same one row Cycle and Labels cost, and neither of those asks about a role.

Tip: The gate also protected nothing. The same colleague could already narrow the board by URL, by opening a shared view that held a person, or by asking the API — which grants exactly what the browser was refusing. It removed the path people could find and left the ones they could not.

Tip: Has anybody got this already, who owns this area, what has she got in review before I touch that file. Those get asked out loud every day, by exactly the roles that could not ask them here.

Reports → Standup is still for owners and administrators, on different grounds: it ranks colleagues by whose work has gone over its estimate, which is a judgement about people rather than a view of the board.

Cleared notifications can be deleted

Clearing moved a row into the Cleared tab and left it there for good. There is now a delete on each row, an Empty for the whole tab, and a nightly purge of cleared rows older than thirty days.

Tip: These are deleted outright, not withdrawn like a comment. A notification is a delivery receipt rather than a record — everything it points at survives on its own, so there is nothing for a tombstone to preserve.

Tip: Only cleared ones, ever. An unread notification from four months ago is somebody's backlog, and a retention window that swallowed it would delete the one kind of row still doing a job.

Tip: The window is FELAGI_INBOX_KEEP_CLEARED_DAYS. Zero keeps them for ever, for anybody who would rather the table only grew.

An inbox row says what actually happened

A status notification read Status changed beside an icon that already meant that, while the move itself sat in a column that truncated before it could say where the issue went. The row now reads backlog → in review, and the right-hand column carries only a name, so it stops being cut off.

Tip: This is why it mattered. done and canceled are both closed and they mean opposite things — one says your bug was fixed, the other says nobody is going to do it. A row that said "Status changed" for both hid exactly the distinction you opened the inbox for.

Tip: The whole sentence is still there as the column's tooltip, where nothing is cut off.

The row is a real link. ⌘-click and middle-click open the issue in a tab, hovering shows where a row goes, and the keyboard reaches it — none of which a click handler on a box can offer. A plain click still marks it read; a tab opened with a modifier leaves it unread, because nothing has been read yet.

A file can be delivered over the API

Attachments could be read and never created. POST /issues/{id}/attachments and POST /issues/{id}/comments/{comment}/attachments take a multipart part called file; DELETE /attachments/{id} takes one down.

Tip: This one is mostly for agents. Producing a file is an ordinary outcome of a run, and without an upload path the only choices were to describe the export or to paste it into a description as markup — which is exactly the bloat an attachment exists to avoid.

Tip: One file per request. A body could carry ten, but then a request half-succeeds or reports one status for several outcomes, and "201 with this object" is a contract a client can act on.

Tip: The limits are the interface's own — 25 MB, and the same allowlist. The type is read from the contents rather than from the name, so a .png that is really text is stored and served as text. The name never decides what something is.

Tip: GET /issues/{id} returns the issue's own files now too. They were documented on a comment and nowhere else, so a file attached to the issue itself was invisible as well as uncreatable — two halves of one omission, and only the write half got reported.

Also fixed: the API answered 302 instead of 422

A validation failure redirected instead of returning an error whenever the caller sent no Accept: application/json header. Every endpoint under /api now answers in JSON whatever was sent, which makes the reference's own table of status codes true.

Tip: This had been the case since the API shipped and was invisible, because a client sending a JSON body usually asks for JSON back. It becomes impossible to miss the moment an endpoint takes a file: curl -F asks for any content type at all, and a redirect is not something a script can act on.


Version 0.69.5

Date: 25 August 2026

A week is a duration you can type

1w now works everywhere a duration is entered: Log time on the timesheet, spent time and the estimate on an issue, hours against a meeting, duration over the API, and an import.

A week is five days — forty hours, not a hundred and sixty-eight. Nobody logging time against an issue means the weekend, and an estimate of "a week" is a week of work. It is the same reasoning that already makes 1d eight hours rather than twenty-four.

Tip: They combine. 1w 2d is fifty-six hours, 1w 4h is forty-four, and 0.5w is twenty.

Tip: It reads back as 5d, not 1w. Days stay the largest unit anything is displayed in, on purpose — "2w 3d" makes a reader carry two conversions before the figure means anything, and these strings are read in reports and exports by people who did not type them. 1w is a way of saying five days.

Tip: The field is still strict about what it will not guess at. 1w30 is refused rather than quietly read as a week, the same way 2h30 has always been — a figure you typed and a figure that was stored should never differ without somebody being told.


Version 0.69.4

Date: 24 August 2026

An issue created in error can be taken off the board, and the API stops accepting questions it does not understand.

An issue can be removed

There was no way to get rid of an issue created by mistake — no route over the API, and no affordance in the interface for anybody, administrator included. Remove issue now sits behind the menu beside Edit, and DELETE /issues/{id} answers 204.

Cancelling was the only tool, and it means something else. Cancelled work was real and then abandoned; an issue created by mistake was never work at all. Progress is closed children over total children, so a junk issue inflates both halves for ever — and the error is invisible. That is the part this fixes.

Tip: The identifier still resolves. WOR-33 quoted in a commit message or a standup does not become a link to nowhere — the row survives, the issue simply stops being anywhere a person works. Its own comments and files go with it, and there is no way back.

Tip: It refuses when anything is attached — children, logged hours, runs, whiteboard notes, a running clock, or a line in a meeting's minutes — and says which. There is no override, deliberately: an issue created in error has none of those, so an override would only ever be used on an issue that was real. If something is in the way, cancel it.

Tip: An epic that still contains issues cannot be removed. That is the same containment rule an epic already follows when you try to file it under another epic, not a second rule invented here.

Tip: The author or an administrator. An issue an agent created has no human author, so for that one an administrator is the only person who can.

The API refuses a question it does not understand

A misspelled query parameter used to answer 200 with the whole workspace in it. ?assignee_id=1 narrows to one person and ?assignnee_id=1 returned everything, and the two responses are indistinguishable to anything reading them.

Every collection endpoint now answers 422 and names what it does accept.

Tip: This matters most for saved views, which is why it was worth doing first. A view whose filter key stopped being recognised would quietly show every issue in the workspace and still look like a working view — and nobody checks a filter that appears to work.

Tip: Request bodies are deliberately not treated this way. An unknown field on a POST or PATCH is still ignored, because clients commonly send a whole resource back including fields they never meant to set. When estimate_minutes was being silently dropped, the answer was to accept the field, not to reject the request.

Asking who has an issue, in one parameter

?assignee= takes an actor key or none:

Written Means
?assignee=user:5, ?assignee=agent:3 That person's or that agent's issues
?assignee=none Nobody has picked it up

none was the real gap — assignee_type=none was the obvious guess and answered 422, and unassigned work could not be asked for at any layer. The two older parameters still work.

Tip: That is the same string the board's address bar carries. A link somebody sends and a query a script runs now say it identically, and the board grew an Agent submenu so either kind of actor can be picked there too.

The views you have kept are readable over the API

GET /saved-views returns the views a token may open — its own, plus what the workspace has shared — with the filters as saved, so a view can be carried out rather than only named.

Tip: Read-only, and that is the whole endpoint. A view is composed by clicking filters in the interface; a second way to build one would be a second thing to keep in agreement with the first.

Tip: An agent works the same board as the people who share it, and a view a colleague kept is the clearest statement of what they consider their work. Until now that statement was legible only in a browser.


Version 0.69.3

Date: 24 August 2026

One addition to the board: it can be narrowed to one colleague.

Filter → Person

The assignee filter offered anyone, assigned to me, assigned to agents and unassigned — and no way to name somebody. An owner wanting to see one developer's work had to read every card on the board.

Person is a fifth choice, and it is offered to owners and administrators. Pick a name and the board and the list both narrow to that person's issues.

It sits in a submenu, beside Cycle and Labels, for the same reason those two do: the list grows with the workspace, and thirteen names stacked in a dropdown push everything under them off the bottom of the window.

Tip: It lists everybody in the workspace, not only the people whose role reads developer. Admins and owners carry issues too, and a picker that could not select them would answer "nothing assigned" for work plainly on the board.

Tip: The address bar carries it, so ?assignee=user:5 is a link that opens the board already narrowed — no saved view needed. That also means a person filter inside a shared saved view keeps working for the colleague who opens it, rather than quietly filtering to nothing in their hands.

Tip: The filter hides cards; it is not a permission. Everybody on this board already sees every member's work, which is why the picker being for owners and admins is about keeping the menu short for everybody else rather than about access.

Tip: The Monday standup report answers the same question as a list, per person, with what has gone over estimate marked. This answers it as the board. Reports → Standup.

Tip: The API could already do this — GET /issues?assignee_type=user&assignee_id=5. Only the interface was missing it.


Version 0.69.2

Date: 24 August 2026

A comment can be corrected and taken back — in the browser and over the API. Until now a comment, once posted, was permanent: there was no way to fix a typo and no way to remove something posted in error.

A comment can be edited by whoever wrote it

The card in a thread carries a menu now. Edit opens the wording in an editor; the thread then shows that it was edited, and the issue's own history says who did it and when.

Over the API: PATCH /issues/{issue}/comments/{comment}, taking body and nothing else. Moving a comment to another issue or under another parent is not a correction — it rewrites a conversation people have already read — so those fields are ignored rather than honoured.

Only the author can edit. A workspace administrator can read every comment and rewrite none of them: an admin who could change what a colleague said, under that colleague's name, is a worse problem than the typo.

A comment an agent's run wrote cannot be edited by anybody. It is the record of what the agent reported, sitting in the timeline it came from, and an agent that can be made to have said something else is not evidence of anything. The hours a run logs already work this way.

Tip: The wording's history is not kept — only that it changed. Articles keep every version because an article is a document people rely on and "what did it say last month" is a real question. A comment is a remark in a thread, where the question people actually ask is "is this what was originally said", and one timestamp answers that. If comment contents ever start being disputed rather than merely corrected, revisions are the answer and this timestamp is the cheap half of them.

Tip: Editing hands out no work. Adding @Freya to a comment by editing it queues nothing, because a mention that dispatched on every save would be a mention nobody could correct — fixing a typo in the name would send the run twice. Post a comment to hand out the work. The API still reports the mentions an edit resolves, so a client can tell a name that matches from one that does not.

A comment can be taken back

Withdraw removes the words and the files and leaves the entry in the thread, marked as withdrawn. Over the API: DELETE /issues/{issue}/comments/{comment}, answering 204.

It is a tombstone rather than a deletion, and the reason is in the schema. A reply points at its parent, and that column cascades — deleting the row would have deleted every reply to it. Nobody taking back their own typo means to delete the three answers underneath. The row keeps their place; the words are gone.

The author can withdraw their own, and so can an administrator. Wider than editing on purpose: editing puts new words in somebody's mouth, while removing takes words out of the room — which is what a workspace needs when a comment carries a customer's name or a key that should never have been pasted. An agent's comment has no human author, so for that one an administrator is the only person who can.

Tip: There is no way back from it. The attachments go with the words, and a withdrawn comment cannot be edited afterwards.

Tip: Taking a comment back also takes back the work it handed out, as far as it can. A run the comment queued that has not started yet is cancelled — otherwise the agent would be sent to read a comment with nothing left in it, which is neither the instruction nor a cancellation. A run already under way is left alone: the agent has read the words, and stopping it mid-run is a separate decision the interface offers on its own.

The API now says when a comment last changed

The comment resource carried only created_at, so a client holding a copy had no way to notice it had gone stale. It now carries updated_at — which moves on any write and is the one to compare a cached copy against — plus edited_at, null until the wording changes, and withdrawn.

Fixed: a comment with no words in it answered 500

POST /issues/{id}/comments crashed on <p></p>. That is what an untouched editor sends, and it is a present, non-empty string, so required let it through — and rich text that carries no words becomes null on the way to a column that forbids null.

It answers 422 now, on both the posting and the editing path. This one was on the endpoint that already existed, and it went unnoticed because a comment's body is the only rich-text column in the schema that refuses null: every other one — an issue description, a project description, a meeting agenda, a time note — accepted the same input harmlessly.

Tip: PATCH follows the same rule as every other PATCH here and takes any subset. A request that does not mention body changes nothing, and is not recorded as an edit.

Notes

  • No webhook fires for editing or withdrawing. comment.created still means a new comment and nothing else
  • A run holds a reference to its trigger comment, not a copy. An agent that has not started yet reads the corrected wording, which is usually the point of correcting it. An agent that has already read the comment will not read it again
  • Nothing was opened in a browser before shipping. The interface is covered by tests that render the page and read the markup, which is not the same as looking at it

Version 0.69.1

Date: 24 August 2026

Six fixes on the REST API, all found by pointing a coding agent at it and driving every write path. Nothing here changes the interface.

Fixed: a comment thread could not be read at all

GET /issues/{id}/comments answered 500 on any thread that had comments in it. An empty thread answered 200, which is why it survived: the only test covered the empty case, and an empty thread never reaches the code that failed.

The same mistake sat in the serialiser, where it failed quietly instead — from_run was null on every comment an agent's run had produced, so a client could not tell a report from a remark. The loud one was reported; the quiet one had been wrong for longer.

Tip: This is the one that blocked reading a thread from an editor. The manual's own advice — read the issue and its thread before starting work — was unfollowable over the API while it was broken.

Fixed: mentioning an agent now works wherever the comment came from

Mentioning an agent in a comment queues a run for it. That is documented as one of the two ways to put an agent to work, and recommended for CI — and it only ever happened when a person did it in the browser. Over the API it did nothing at all, and an email reply naming an agent was silently ignored.

The parsing was never the problem; the response to it lived in one page. It now lives with the comment, so every path gets it.

What else changed about mentions

  • A person's mention renders as one. It notified them and then drew as plain text, which is one mention giving two different answers — and the one on the page is the one people believe
  • @agent:1 and @user:3 are accepted, alongside display names. Two colleagues can share a name and a name can change; an id cannot. They render as the name they point to, so nobody has to read @user:3
  • The response says who it resolved. A mentions array, empty when nothing matched — which is how you tell a mention that landed from one that did not
  • A comment naming "@Freya Dawn" no longer also wakes "@Freya". Longest-first was documented as preventing that and never did: ordering decides which name is found, not that the shorter one stops matching

Tip: Nobody is told about their own mention. If you tested this by naming yourself, the silence was correct — that is what happened in the report that started this.

Tip: An unresolvable @Name is still a 201, with mentions: []. An @ in a comment is usually not a mention at all — an address, a time, a colleague who does not use Félagi — so refusing them would make the endpoint useless for ordinary prose.

Fixed: an estimate sent to the API was accepted and thrown away

estimate_minutes is documented as writable, appears in the reference's own example, and was never in the validated set — so it arrived, was discarded, and the response came back 201 with the field null. Silent, which is worse than a refusal.

It now persists on both write paths, null clears it, and a value it cannot use answers 422 rather than saying nothing.

Tip: Minutes, as the column stores them. The interface takes "2h 30m" and converts; the API takes the number, the same way POST /issues/{id}/time takes minutes.

Fixed: one issue, one identifier

GET /issues/{id} said WOR-32 while GET /timer and the time entries said DEV-32 — the workspace's prefix instead of the project's. Both still resolve to the same issue, which is exactly why nobody noticed; the cost is that a client cannot compare two identifiers, and anything it writes into a commit message is wrong.

Five places loaded an issue without the column that decides its prefix. Two others already had it right, which is how a list of required columns became the fix. The whiteboard was one of the five — an interface page nobody had reported.

Fixed: live work reported as "Deleted work"

A time entry against an issue that exists rendered its title as Deleted work. Same cause as the identifier: the issue was loaded without its title, and from inside a null check a missing column and a deleted subject look identical.

Tip: If you have exported timesheets over the API and seen that label, the hours were attached to the right issue the whole time — only the rendered title was wrong. Nothing needs re-entering.

Changed: GET /projects is paginated

It answered a bare data with no links and no meta, which is not the collection envelope the reference describes for every other endpoint.

If you have a client reading projects, read this one. It now returns 100 per page with links and meta. Below a hundred projects nothing changes; above it, follow links.next rather than assuming one response holds them all.

And links.next is now built from the configured application URL, so it is absolute and identical on every call. It was not: three identical requests returned a relative path, an absolute URL, and the relative form again, because production runs long-lived workers and whichever one answered decided whether there was a root. Follow the link; do not assemble it.

What this release does not do

  • The trusted-proxy setting is still unset in production. Pagination no longer depends on it, but client addresses do — an operator has to decide that one, and it is the other half of why a link could not be trusted.
  • No estimate as a duration string. estimate_minutes takes minutes. 2h 30m over the API would be a consistent addition and is not here.
  • An agent still cannot dispatch another agent by naming it. Only comments a person wrote hand out work. An agent that could do it would mention something on its way out of the run answering the last mention, and there is no bound on that.

Version 0.69.0

Date: 24 August 2026

Standup: who is behind on what

A new report, built for the Monday morning meeting. Every open issue on one timeline, grouped by whoever is carrying it, with anything past its estimate marked.

What you'll notice

  • Sorted worst first — most past estimate, then most overdue, then most work. The top of the page is the agenda
  • Each person's header reads on its own: eight issues, two past estimate, 14h of 9h
  • Every marked row carries both numbers, not just the colour. "Past estimate" is an argument; five hours against two is its evidence
  • Somebody with nothing open has no row. That is a fact worth noticing in a standup — either good news or somebody who has been forgotten

Tip: The hours in a header count only the estimated issues. An unestimated issue that has taken ten hours would otherwise make somebody look ten hours under budget, which is the same rule the cost report follows: an unpriced hour is unknown, never free.

Tip: It groups by whoever the work is assigned to, not by the developer role. Those are different things, and in a real workspace the role was the wrong one — everybody holding an issue was an owner or an admin, and the four people with the developer role had none. Grouping by role would have shown four empty rows and hidden all the work. So if somebody you expected is missing from this page, they have nothing open.

Owners and admins only

This is the first restricted report, and the restriction is the point: it says who is behind. Reading that is the same kind of trust as deciding who works here — and deliberately not the same as running the agent workforce. A developer hires agents, connects machines and writes skills without being handed either.

The tile is absent for everybody else rather than present and refusing. A card that answers 403 reads as a broken product, not as a report that is not yours.

Tip: The CSV export is gated with the page. If you need to hand these numbers to somebody who cannot open the report, export it yourself and send the file — the link will not work for them.

The Gantt cannot be talked into it

Worth stating plainly rather than fixing quietly. The Gantt page keeps its grouping in the address bar, so ?groupBy=assignee was one link away from the same per-person data — and the Gantt's own CSV export read that parameter straight out of the request.

Both now go through one allowlist naming the two open groupings, so a third surface cannot reopen what the first two closed. Nothing was exposed by this that anybody is known to have looked at; it is written down because a restriction with a way around it is not a restriction.

What this release does not do

  • No bars. It is a timeline in the sense of one row per issue with its dates and its numbers — not the drawn Gantt bars of the Gantt page. The reading you want at 09:00 on a Monday is a list, and the Gantt is one click away when you want the shape of a quarter.
  • It does not tell you why. Past estimate is a number, not a diagnosis, and the report makes no attempt to guess between "harder than we thought" and "sized wrongly". The estimate calibration at the bottom of the time report is the place that asks that.
  • No per-person history. This is the present: what is open now. Whether somebody is usually over is a different question and not one a standup should be answering.
  • Still not opened in a browser before shipping. Behaviour is covered by tests and markup by two shape guards, but no test can say whether a page looks right.

Version 0.68.0

Date: 22 August 2026

Laravel Nightwatch

An exception becomes an issue somebody owns, instead of a notification somebody skims.

Administration → Nightwatch produces an address and a signing secret to paste into your application's webhook settings in Nightwatch. From then on:

Nightwatch says Félagi does
issue.opened Files an issue, once
issue.reopened Reopens it and says it came back
issue.resolved Comments, and closes it only if nobody had started
issue.ignored Comments, and cancels it on the same condition

Félagi does not group anything. Nightwatch already decided what counts as the same problem, and two answers to that question diverge on the first message containing a request id. So there is no fingerprinting here — only a mirror of a list somebody else is keeping well.

issue.reopened is the one worth having. "It is happening" is a fact anybody can see; "it came back after we fixed it" is the one nobody discovers on their own.

Resolving does not always close. An engineer resolving an alert has said the errors stopped, not that the work is done. Félagi comments always and finishes the issue only while nobody has started on it — a monitoring tool closing an issue a colleague is working in is a tool overruling a person.

Which project, and which developer

Nightwatch allows one webhook per application, so every application points at the same address. The Applications table fills itself the first time each one reports something, and each row carries a project and the developer its problems are assigned to — a 3am exception needs a name on it before anybody has looked.

The assignee sits on the application rather than the project, because staging and production can be two applications pointing at one project and belong to different people.

Also

Destructive database commands are now prohibited in every environment, not only production. migrate:fresh, migrate:refresh, migrate:reset and db:wipe refuse to run. A developer's local database is where a week of exploring lives, and Laravel's convention of guarding only production assumes otherwise.

The demo seeder now builds a workspace worth looking at — meetings, a whiteboard with arrows, an article with a body, hours on the sheet — and can be re-run without duplicating what is there.


Version 0.67.0

Date: 21 August 2026

Arrows you can actually click, and a board you can find how to delete

Right-click anything on a whiteboard for Duplicate, Bring to front and Remove.

Arrows and drawings can be selected at last. A line is two and a half pixels wide; asking somebody to click it is asking them to miss. An invisible fourteen-pixel copy of each path now takes the pointer. Until this release an arrow could not be selected, moved or deleted at all — the only way to remove one was to undo, in the session that drew it.

Deleting a board was always possible

It was behind an unlabelled gear, with the destructive action at the foot of a settings form. Reported as "it is not possible to delete a board", which is exactly right from outside: a feature nobody can find is a feature that does not exist.

There is now a Board menu beside the board's name: rename, visibility, delete.

The element menu says Remove and the board menu says Delete the board — different verbs, on purpose. Two things called Delete on one screen, one costing a note and one costing everything, is a trap. I walked into it myself while testing this, and deleted the board I was working on.

Two faults found on the way

Group delete never worked. It cloned the element with structuredClone, which throws on an Alpine proxy — an exception in the console and a delete that simply did not happen. Shipped in 0.61.0, in the feature written to make groups possible.

Marquee selection missed every drawing. A stroke's points are stored relative to its own corner, and the selection test read them as absolute — so every drawing was measured as though it sat at the top left of the board.


Version 0.66.1

Date: 21 August 2026

Eight nameless boxes

Issue types on a project showed eight checkboxes with no labels at all. You could tick them, save them, and never know which was which.

The labels were written — as a slot. Flux takes a checkbox's label as a prop and passes only attributes down to the field wrapper, so the slot was dropped without a word. The modal rendered, saved correctly, and identified nothing.

Nothing threw. Nothing logged. Nothing failed. It was found by somebody opening the screen — the same shape as every other fault this project has shipped.

Now: each type shows its name, and how many issues already use it, so turning one off tells you what you would be hiding.

A test now fails if any checkbox, radio or switch is handed a label slot again.


Version 0.66.0

Date: 21 August 2026

What did not happen

Every observability tool answers what crashed. Administration → Silent interactions answers the other question.

Nine faults have reached production in this project and every single one was silent: no exception, no log line, no failing test. A title that would not save because the validation error had nowhere to be drawn. Three rate fields that had never once saved, under a settings page that looked filled in. Forms that rendered perfectly and did nothing at all.

None of those appear in a log. All of them appear here.

What counts as nothing — all five at once, because any four are ordinary:

  1. Nothing was written
  2. No error was shown
  3. There was no redirect
  4. Nothing was dispatched — no toast, no modal, no event
  5. No component property moved

An action that does all five is one where somebody pressed a button and the application, as far as they could tell, ignored them.

It counts rather than alerts. Plenty of actions legitimately do nothing once. The same one four hundred times is a fault nobody has reported, because it does not look like a fault. Fixed something? Forget it and watch whether it comes back — a count still carrying yesterday's occurrences can never show that it stopped.


Version 0.65.0

Date: 21 August 2026

Skills that have stopped earning their place

Félagi already finds skills that ought to exist — a failed run, a colleague's comment, a later run that worked, and the comment in the middle is a skill somebody already wrote. Nothing found the ones that ought to go.

A skill written in March against a framework abandoned in June is still read into every run. It costs tokens, it steers behaviour, and nobody measured whether it still helps. Something that only grows is not a skill base, it is an attic.

Each skill now carries a word about itself: unattached, never read, stale, or nothing at all when it is in use.

It is a fact, not a recommendation. "Nothing has read this in ninety days" is something Félagi can see; whether that means delete it is a judgement about your codebase, and a skill for a release process used twice a year is supposed to look stale.

What had to be built first

Which skills a run received was nowhere. agent_skill says what an agent has today, and that cannot explain last month's runs — worse, inferring one from the other would answer the question using the very thing under review: the attachments you are deciding whether to change.

So it is recorded at the moment the work leaves for the runtime, which is the only moment it is a fact rather than a guess. The measurement therefore starts now. Runs from before are not guessed at, and the page says which date it is counting from.

Where a skill has gone out with some runs and not others, and both groups are big enough, the two failure rates are shown side by side. Where it goes out with everything there is nothing to compare against and nothing is shown — a rate on its own invites you to compare it with a number you have in your head, which is worse than no number.


Version 0.64.1

Date: 21 August 2026

The claim check, over the API and in the manual

Yesterday's feature shipped in the interface and was invisible everywhere else. Second time in one day: a screen and an API resource describe the same object, and only one of them got updated.

GET /issues/{id}/artifacts now returns claim_check and claim_checked_at, with three states that must not be conflated:

claim_checked_at claim_check Means
null null Never checked
set [] Checked, nothing to report
set non-empty The summary and the diff disagree

A client that treats the first two the same calls an unexamined delivery a clean one. That is why the timestamp is a separate field rather than an empty array standing in for both, and there is now a test that says so.

Also documented: the data model gained both columns, and the GitHub guide notes that reading a pull request's files needs no permission the App did not already have.


Version 0.64.0

Date: 21 August 2026

Did it do what it said?

A run ends with a summary. It also ends with a pull request whose files are a matter of record. Until now nobody compared them.

The summary and the diff disagree The summary says it added tests. No test file was changed.

This is not a review of the code and has no opinion on whether the change is good. It asks one question — was this claim kept — and only about claims a list of file paths can settle. The difference is rarely a lie; it is a model summarising optimistically, which is exactly what a person spends ten minutes finding by hand on every delivery.

What it checks: tests claimed, documentation claimed, a migration mentioned, and — worth the most — a run that reported changing nothing and changed something. Nobody reviews a delivery they were told was empty.

What it refuses to do, because a false alarm costs far more than a missed one:

  • "Fixed the failing test" is not a claim to have written one
  • Tests are recognised by any ecosystem's convention. A rule that only knew Laravel would report a false discrepancy on every Go repository
  • If the file list cannot be fetched, nothing is said. An empty list is either an empty pull request or a failed call, and those are indistinguishable from here
  • A pull request a person opened is not checked. No run, no summary, no claim

"Checked and fine" and "never checked" are recorded differently, so an unchecked delivery never looks like one that passed.


Version 0.63.0

Date: 21 August 2026

What it will cost, before you spend it

Félagi has sixteen reports and every one of them looks backwards. The cost report tells you what a run cost once it is over — the right number for an invoice, the wrong moment for a decision.

Assigning issues to an agent already asked you to confirm, because each one queues a run and a run costs real money. It declined to say how much. Now it says:

Assign 4 issues to Bragi? That queues 4 runs. Usually 12.00 NOK–20.00 NOK, based on 10 runs of any kind.

The price also sits beside the agent's name while you are choosing, so it is visible before you commit rather than only after.

Four rules, each one a way of not lying

  • The median, never the mean. One run that burned forty units must not set the expectation for the next twenty
  • A range, not a figure to the cent. Both ends are prices somebody was actually charged — nothing is interpolated, because an interpolated cost is a number nobody was ever billed and it is the one people repeat
  • Silence below five completed runs. An agent with no history shows no price at all. Two runs is an anecdote
  • It says what it is based on — "6 similar bug runs" or "12 runs of any kind"

Two warnings sit beside it. Runs that never reported a cost are counted and named rather than treated as free, because an unpriced run is not a free one. And a failure rate is reported separately rather than averaged in: a run that costs money and produces nothing is a different kind of bad news from an expensive one.

For a batch the cost is multiplied and the time is not. Twenty runs queued at once do not take twenty times as long in any way this can honestly predict, and a wrong duration beside a right cost discredits both.


Version 0.62.1

Date: 21 August 2026

Pointing a coding agent at the API

A new section in Working from your editor, in the manual: what to put in your project's instructions file so Claude Code, Codex or Cursor can read issues, log hours and leave comments.

No MCP server is needed. The API is HTTP with a bearer token, and anything with a shell can already use it. The section says so, and says when an MCP server would be worth building — which is not this.

Three things it insists on, each because the failure is easy to picture:

  • Tell the agent not to invent endpoints. One that cannot find what it wants will produce a plausible endpoint and then report the 404 as though Félagi were broken
  • Start with a read-only token. An agent that has misread a conversation will close an issue confidently
  • Keep the token in the environment. An instructions file is committed, shared, and pasted into a support conversation on the one day something is wrong

Version 0.62.0

Date: 21 August 2026

An hour in a meeting, over the API

Meetings gained hours in 0.57.0. The API did not notice.

A time entry logged against a meeting came out of GET /time-entries with no subject at all — the endpoint only ever answered "which issue". Any client grouping hours by issue dropped those entries silently, which is the worst way to be wrong about somebody's time.

What changed

  • Every entry now carries subject (issue or meeting), and a title, so a list can be drawn without joining anything
  • GET /time-entries?meeting=… narrows to one meeting
  • POST /meetings/{meeting}/time logs an hour against a meeting. Without it, a script recording a team's week had to leave the meetings out, which is how a report ends up saying meetings are free

Documentation that had fallen behind

The timesheet guide said nothing about meeting hours, three releases after they arrived. Somebody reading the page about timesheets would not have learned they exist.

Both gaps are closed, and the API reference now states plainly that a client summing per issue has to handle meetings or drop them on purpose.


Version 0.61.0

Date: 21 August 2026

Selecting more than one thing on a whiteboard

Drag on empty surface and a rectangle follows the pointer. Everything it touches is selected — not only what it entirely encloses, because the note at the edge is the one you notice missing after you have already moved the group without it.

Until now the board could hold exactly one selected thing, which meant no moving a cluster, no recolouring a column, and no copying anything at all. On a surface whose whole point is arranging things, that is most of the arranging.

What you'll notice

  • Shift-click or -click adds one at a time, and ⌘A takes the lot
  • Dragging any selected thing moves all of them. Grabbing something outside the selection picks up that one instead, so a click never drags four notes by accident
  • A colour swatch recolours everything selected
  • ⌘C / ⌘V copies and pastes, ⌘D does both in one press
  • One undo reverses one gesture, however many notes it carried — four undos to reverse one drag is not what you did

Copies land slightly offset rather than under the pointer. A copy directly on top of its original looks like nothing happened, and one that jumps to the cursor loses the arrangement, which is usually the reason a group was copied.

Still not there

The whiteboard's list of gaps is shorter by three, and the rest of it stands: undo still lives in the browser and starts empty after a reload, arrows do not stay attached to the notes they point at, there are no frames or templates, no comments on an element, and a dropped connection still needs a reload rather than catching up.

Two of those entries are deliberate rather than pending, and stay that way: boards are out of the import and export, and text on a note is plain. A toolbar per note, on a surface where six people are typing at once, is a worse problem than the one it solves.


Version 0.60.0

Date: 21 August 2026

The manual, inside the application

Manual in the sidebar. Thirty-five pages, arranged in the order somebody actually needs them: what Félagi is, then the screens you are in every day, then agents, then everything an administrator does once, then the reference.

The documentation was always there — fifty-five thousand words of it — and reachable only by someone browsing the source code on GitHub. Which is to say: reachable by developers, on a different screen, in a different tab, for a product used by project managers.

What you'll notice

  • Contents down the left, grouped and ordered rather than alphabetical — alphabetical is the arrangement of a filesystem, not of an explanation
  • On this page down the right, so a nine-hundred-line reference is navigable
  • A search box that looks inside the pages, not only at their titles
  • Links between pages work, including the ones pointing at a particular section

None of it is a copy. The manual renders the same files the repository ships, so there is nothing to keep in step: a correction to the documentation is a correction to what you read here, in the same commit. A manual kept in two places disagrees with itself within a month, and the copy nobody edits is the one people read.

A test fails if a page of documentation is ever written and not placed in the manual — the ordering is by hand, and the cost of doing it by hand is a page that quietly does not appear.


Version 0.59.0

Date: 21 August 2026

Changing who is coming

The attendee list was decided at the moment a meeting was created and could never be touched again. Anybody remembered a minute later could not be added at all, which is not how anybody plans a meeting.

Add someone now sits under the list, and remove takes somebody off it.

What you'll notice

  • Only people who are not already invited are offered. Adding somebody twice is a duplicate row and a second invitation, so the option is not there to take
  • Nothing is sent when you add somebody. Adding a person and telling them are separate acts, exactly as they are when a meeting is created — an organiser building a list over a minute should not be sending an email per click. If the invitation has already gone out, the message says so
  • Removing somebody who has already replied says, before it happens, that their reply is being discarded

Available to the organiser, and to anybody who can manage members. Not on a cancelled meeting.

Still members only. An external participant has no account, and inviting an address Félagi cannot show a page to would be an email pretending to be a feature. If you need that, it is a different piece of work and worth asking for on its own.


Version 0.58.2

Date: 21 August 2026

The rest of the rich text

0.58.1 tightened bullet spacing wherever rich text is drawn — except it only reached the places that used the shared component. The knowledge base and the note on a logged hour each had their own copy of the same styling, written at different times, so an article still arrived as a wall with a full line of air between every bullet.

Three copies had drifted, as copies do. One carried dark:prose-invert where the others used the palette, and each would have needed the same fix applied by hand.

There is now one. Everything that shows what somebody typed into the editor goes through it: issue descriptions, agendas, minutes, briefs, knowledge articles, the timeline, and the note beside an hour.

What you'll notice

  • Knowledge articles read as lists again rather than as walls
  • Notes on logged hours match everything else
  • A test fails if any page starts writing its own prose styling again

The measure — how wide a line is allowed to get — is the one thing that legitimately differs between them, so it is asked for by name rather than by adding a second width that quietly fights the first.


Version 0.58.1

Date: 21 August 2026

Saving an all-day meeting did nothing, and said nothing

Change the title of an all-day meeting, press Save, and the title did not change. No message, no error, no toast.

The form loads the meeting's length into a box that accepts up to eight hours, and an all-day meeting is twenty-four. The rule rejected it — correctly — but that box is hidden while All day is on, so the complaint had nowhere to appear. The save stopped, and the page looked exactly as it had a moment before.

Shipped this morning in 0.58.0. Every test passed, because a test reads the error it is handed and a person cannot.

Two fixes, and the second is the one that matters

  • An all-day meeting no longer loads a day into a field offering hours, and the length is now excluded from checking entirely while All day is on
  • Any error is now shown at the top of the form, whether or not the field it belongs to is on screen. A form that can hide a field can hide a reason

An agenda arrived as a wall

Every bullet in a rich-text list had a full line of air above and below it, because the editor writes each one as a paragraph inside the bullet and a paragraph is given room to breathe. An eleven-point agenda was two screens tall.

Nothing looked broken enough to report, which is why it took somebody saying the text was "not ideally formatted". Fixed in one place, so it is fixed everywhere rich text appears — issue descriptions, minutes, project briefs, the timeline.


Version 0.58.0

Date: 21 August 2026

The organiser can change a meeting

Title, location, when, all-day, agenda. Until now a meeting was whatever it was when it was created, and a typo in a title was permanent — the only way to fix a time was to cancel and schedule a second one, which is two entries in everybody's calendar and a cancellation nobody understands.

Available to the organiser, and to anybody who can manage members.

What you'll notice

  • Edit beside Send invitations on the meeting
  • A meeting in a series says, on the form, that you are changing this occurrence only
  • A cancelled meeting cannot be edited. It is the record of a meeting that will not happen, and tidying it up would produce a plausible entry for an event nobody is going to

The part that is easy to miss: changing the time here does not move it in anybody's calendar. What updates a calendar is a second invitation — and it arrives as an update to the entry they already hold rather than a duplicate beside it. If the invitation has already gone out, the form tells you, and so does the message after saving.

Nothing is sent unless you send it, for the same reason nothing is sent when a meeting is created: a typo in a title should not already be an email everybody has.


Version 0.57.0

Date: 21 August 2026

All-day meetings

A workshop, an offsite, a course. Turn on All day and the form stops asking for a start time and a length, because neither of those is the answer to when it is.

Underneath it is stored like any other meeting — it begins at midnight and lasts a day — so the calendar, the .ics and the overlap check need to know nothing about all-day meetings to place one correctly. Only the wording changes, and it changes in one place.

Hours spent in the room

Log time on a meeting now records an hour against the meeting itself, and it lands on your timesheet beside the hours you logged against issues.

Until now the only place to put an hour was an issue. So a morning of planning was either recorded against a task it was not spent on, or not recorded at all — and people choose the second, which is why meetings looked free. A tracker that cannot hold an hour teaches people to put it somewhere untrue.

What you'll notice

  • A Time spent panel on every meeting, with a total
  • Meeting hours on your timesheet, linked back to the meeting
  • The date offered is the day the meeting was on, not today, because these are written down afterwards

Two things it deliberately does not do. It does not add the meeting's hours to the issues the meeting produced — an hour in the room and an hour doing what the room decided are different hours, and adding them would make the meeting look free again. And it does not touch anybody else's entry: you can remove your own, and somebody else's hours are their statement, not yours.

The scheduling form

The agenda editor opened a quarter of a screen tall with the full toolbar, for what is usually three bullet points. It now opens at the size of the answer.


Version 0.56.3

Date: 21 August 2026

Modals look like the rest of the application

Flux dresses its dialogs in a grey panel with a large radius, a drop shadow and a ring. Félagi is paper, 1px lines and small corners. Twenty-four of the twenty-six modals in the application wore the grey, because a modal is written once and never seen beside a screen it does not appear on.

Two had been corrected by hand — which is how a house style stops being true and becomes something you have to remember.

What you'll notice

  • Every dialog now sits on the same paper surface as the page behind it, with a line instead of a shadow
  • Flyouts stay square, because they meet the edge of the window
  • Log time in particular: the note editor opened a quarter of a screen tall with twelve formatting buttons, for a line like "Monday tasks + Møte". It now opens at the size of the answer, with the four buttons a note of that shape uses

It is said once, in the stylesheet, so the next modal is right before anyone looks at it. Three tests hold it there, including one that fails if any modal starts correcting the surface by hand again.


Version 0.56.2

Date: 21 August 2026

A rate box that looked full when it was empty

The hourly rate and the dollar rate sat in their boxes as grey example figures — 1200 and 10.50 — in the same font, the same size and the same position a saved figure occupies. The only thing separating an example from an answer was a shade of grey.

A workspace ran for weeks with no rate at all while this screen appeared to show one. Nine saves were recorded, every one of them storing nothing, and the cost report said the hours were unpriced the whole time. Its owner believed the form over the report, which is the right instinct — a form is where you put things.

What you'll notice

  • The examples now read e.g. 1200, so they cannot be mistaken for something you typed
  • When no rate is set, the settings screen says so, in the same words the cost report uses — the two screens disagreeing is what made the emptiness invisible
  • Five new tests cover these fields, which had never been covered at all

If your cost report says 0.00: open workspace settings and look at the rate box. If the figure is grey, it is an example and no rate is stored. Type it, save, and it turns white.

Nothing about saving was ever broken. The fields were simply always empty, and every screen that could have said so stayed quiet.


Version 0.56.1

Date: 20 August 2026

A provider named without a key now says so

Turning tidying on takes two settings, not one: FELAGI_TIDY_PROVIDER is your decision about whether the server may be handed a paragraph of somebody's prose, and the provider's own API key is a separate variable belonging to the AI SDK — ANTHROPIC_API_KEY, OPENAI_API_KEY, and so on.

Set only the first and the button appeared and failed when pressed, with a message from somewhere inside an HTTP client that named nothing you could act on.

What you'll notice

  • Administration → Workspace now states both exceptions in one place — minute drafting and tidying — and says which of three states each is in: off, configured, or named without a key
  • The half-configured state is called out in as many words: the button appears and will fail when somebody presses it
  • A failed attempt names the missing key instead of the HTTP error

Tip: If somebody reports that the button does nothing, that page answers it in one line. The person who set half the configuration is usually not the person pressing the button, which is exactly why it belongs on a settings screen rather than in a log.

Tip: The button is not hidden when the key is missing, and that is deliberate in two directions. An operator who set the variable and deployed would otherwise find nothing, which looks like a failed deploy rather than a missing key. And ollama needs no key at all — it runs against your own machine — so "no key, no button" would switch off the one provider that makes no outbound call whatsoever.

The test suite can no longer talk to a model

For contributors, and worth saying plainly because it was found rather than foreseen.

A test set a provider's key to null and expected the call to fail. It came back in 3.2 seconds with a real answer: the provider had already been resolved holding the key from the developer's own .env, so changing the configuration afterwards changed nothing — and the suite had made a live, billed request against somebody's account.

Every agent is now faked by default, with stray prompts refused. A test that means to prompt registers its own responses; one that prompts by accident fails loudly.

Tip: The guard that holds this in place reads app/Ai/Agents rather than listing agents by name, so it fails the day a third agent is written without the setup file being told. A guard that lists what it knows about goes quietly out of date.

Tip for anyone checking this from a console: ask config('ai.providers.<name>.key'), never env('ANTHROPIC_API_KEY'). Production caches its configuration, and env() outside a config file returns null once it has — so the obvious check reports an empty key on an installation that is working perfectly. That mistake is why this release exists in the shape it does.

What this release does not do

  • It does not check that the key works, only that one is configured. A wrong key still fails at the moment somebody presses the button — there is no way to know without asking the provider, and asking on a settings page load is a request nobody agreed to.
  • Nothing else changed. No new behaviour in the tidying itself, and nothing to migrate.

Version 0.56.0

Date: 20 August 2026

Tidy up the minutes, before they go out

The button the knowledge base got yesterday is now on meeting minutes too, at the same boundary: before anybody else has read them. For an article that boundary is publication; here it is circulation.

Structure and mechanics only — a wall of text gets paragraph breaks, a run of lines that is obviously a list becomes a list, a line acting as a section title becomes a heading, spelling and punctuation are corrected. It will not add anything, will not rewrite your sentences, will not translate, and will not guess: a passage too unclear to structure without deciding what it means is left exactly as it was, and the question comes back to you.

Tip: Nothing new to configure. It is the same FELAGI_TIDY_PROVIDER switch — if tidying already works on your articles, it already works here. An operator is deciding whether the server may be handed a paragraph of somebody's prose, and that is one question however many editors offer the button.

Tip: Tidy before you circulate. Minutes are sent once, and after that they are the record — the editor closes and so does this. That is deliberate: minutes that quietly change after people have read them are worse than late ones.

Tip: The left alone, and why list matters more here than anywhere else in Félagi. A plausible sentence in a record is a decision nobody made, and each line on that list is a passage the model would have had to invent one for.

It does not touch the notes

Your notes are what you typed while listening. They are kept after the minutes exist so the minutes stay checkable, and tidying them would be editing the evidence — a summary whose source has been overwritten is an assertion nobody can check.

Turning notes into prose is what Draft the minutes already does, and it is a different act: that reads your notes and writes minutes. This one takes minutes you have and puts them in order.

What this release does not do

  • No tidying after circulation, and no override. Minutes are the record from the moment they are sent, and the button is gone along with the editor.
  • Nothing is saved. Accepting puts the result in the editor; Save is still a separate press, and Circulate another one after that. Three deliberate presses between a model's suggestion and somebody's inbox.
  • The agenda is not offered it. It is written before the meeting and carried in the invitations, which is a boundary of a different shape. Ask if you want it.
  • Still not opened in a browser before shipping. Behaviour is covered by tests and markup by two shape guards, but no test can say whether a page looks right.

Version 0.55.0

Date: 20 August 2026

Tidy up with AI

While an article is not published, the editor has a Tidy up with AI button. It is for the page that was typed in a hurry: notes from a call, a paste out of a chat, a wall of lines that should have been a list.

Structure is the work it does. Lines that are obviously a list become a list, a wall of text gets paragraph breaks, a line acting as a section title becomes a heading, a path or a command becomes code, and spelling and punctuation are corrected.

What it will not do, which is the whole design:

  • Add anything. Not a step, not a caveat, not a number — however obviously true. It is not the author and it does not know what your team does
  • Rewrite your sentences. It fixes typing slips. It does not make your prose smoother, more formal, or better worded
  • Translate. An article written in Norwegian stays Norwegian, heading for heading
  • Guess. A passage too unclear to structure without deciding what it means is left exactly as it was, and the question comes back to you

Nothing is written. You get a list of what it changed, the result goes into the editor only if you accept it, and then you still press Save with Cancel beside it. Two deliberate presses before your own words are replaced, because they are your own words.

Tip: The left alone, and why list is worth more than the tidying. Each line is a passage it would have had to guess at — which usually means the passage is unclear to a colleague too, and you are the only person who can fix it.

Tip: It is offered on unpublished articles only. Reshaping your own draft and reshaping a page the workspace is already reading are different acts. If you do want to tidy something published, send it back to In review first — the button appears, and the article stays readable while you work.

Tip: Off unless an operator sets FELAGI_TIDY_PROVIDER. On an installation where nobody set it there is no button at all, rather than a button that fails when you press it.

The server now calls a model in two places, and says so

Félagi's claim is that every model call touching your code happens in an agent CLI on your own machine. That claim had one named exception — drafting meeting minutes — and the architecture notes said plainly that a second one would have to pass the same test.

It was held against that test before it was written, and it passes on every row: no checkout, no execution, no credentials, the input is prose somebody just typed, and it is off unless an operator names a provider. The front page now says two rather than one, and names both.

Two is not a slope to three. What both have in common is that they are not agent work — a text transformation on text a person typed, in a part of the product a team with no agents at all still uses. Anything that touches a repository, runs something, or holds a credential is the architecture changing, and would be said in a much shorter sentence.

Tip: What is sent is the article's text and its title. A runbook is often more sensitive than the source it describes, so it is worth deciding that deliberately rather than discovering it — which is why there is no default provider and never will be.

What this release does not do

  • It does not tidy anything on its own. There is no automatic pass, no tidying on save, and no queue. Somebody presses a button and somebody accepts the result.
  • No diff. You get a list of what changed in words, not a marked-up before-and-after. The list is what a person actually reads; a two-pane diff of your own prose is not.
  • Published articles are not offered it, and there is no override. Move it to In review if you mean to.
  • Still not opened in a browser before shipping. Its behaviour is covered by tests and its markup by two shape guards, but no test can say whether a page looks right.

Version 0.54.1

Date: 20 August 2026

Fixed: a published article could be invisible to everybody

An article that was published, and permitted to the whole workspace, could be reachable by nobody. If it was filed under somebody's draft, the tree had no branch that led to it — the draft is private, so the path through it did not exist, and the article was missing for its own author as well. Only a search or a saved link found it.

It now comes up to the top of the tree and says that its parent is not one you can see. It loses the grouping, not the visibility, which is what happens everywhere else in Félagi when a container is unavailable.

Tip: If something you published went missing this way, it is back in the tree without you doing anything. Look at the top of the list — that is where an article whose parent you cannot see now sits.

An article has a state, not a switch

Published used to be a single switch, and it was answering three questions at once: how far the writing had got, who was allowed to read it, and whether the article was part of the tree. That is why publishing something could hide it.

There are now four states, and the arrows run both ways:

draftin reviewpublishedarchived

What you'll notice

  • Archived is new, and it is the one people have been missing. It takes a page out of the tree while leaving it readable to anyone who searches for it or follows an old link — the old deploy process stops being part of how the base is organised without being deleted
  • In review is readable by the whole workspace and marked as unfinished, so "have a look at this before I call it done" is finally a thing the product can say
  • Draft is unchanged: yours alone until you send it on
  • Published carries no badge, anywhere. A tree where every row says "published" has taught you to skip badges, and the one row that needed a mark is the one you would miss

Tip: The state control sits where Publish and Unpublish used to. If you have been unpublishing pages to get them out of the way, archived is the thing you actually wanted — those pages were hidden from everybody, and archived ones are not.

One search field, where there were two

There was a Filter by title box in the sidebar and a Search articles box in the pane, each doing half of one job, with nothing on the screen to say which half.

There is one box now, at the top. It searches titles and text and filters the tree at the same time.

Tip: The address bar carries what you typed, so a search is a link. Paste it to somebody instead of describing what to look for.

The page you land on

Opening Knowledge without picking anything now shows what changed and what is waiting: the articles touched most recently, and the ones sitting in review. Drafts and archived work are left out — a list with everything in it is a list ordered by accident.

It used to say Nothing selected, which was true and no use.

A title can come from the writing

If an article is still called Untitled and its body opens with a heading, that heading becomes the title when you save. This existed because the two were never connected: an article sat in the tree as Untitled while its own page read Kick off / task force 2H2026.

A title you typed is never overwritten. And a body that opens with a paragraph leaves the title alone — the heading has to be the first thing in the document, or an article would end up named after a section halfway down it.

The article header names who answers for it

Owned by, and when it was last touched, in place of when it was created. When a page is wrong, out of date, or about to be relied on, who answers for it is the question being asked — and created by answered a different one, in the past tense.

For anyone using the API

Articles carry a state field: draft, in_review, published or archived. Writing accepts either that or the old published boolean, which still means what it meant — true publishes, false returns it to a draft. When a request sends both, the state decides, because it is the one that can say all four things.

published is still in every response, derived from the state rather than stored beside it. Nothing that worked yesterday stops working.

What this release does not do

  • No review dates. Nothing tracks when an article was last checked or warns that it has gone stale. It is the next thing worth having, and it is not here.
  • No draft of a page that is already published. Sending one back to in review edits it in place, and readers see the work in progress. That is the price of not having two copies to reconcile.
  • The tree is still the only structure — no tags, no categories. An article is where somebody filed it.
  • This release was not opened in a browser before it shipped. Its tests cover the behaviour and two guards cover the shapes that silently fail to render, but no test can say whether a page looks right. If something here looks wrong, that is worth reporting rather than working around.

Version 0.54.0

Date: 19 August 2026

An issue can name its epic while you are writing it

New issue carries an Epic field, beside Project. Filing an issue under an epic used to mean creating it, finding it, opening it and moving it — four steps for a decision that was already made before the form was opened.

What you'll notice

  • The field is not shown when there is nothing to choose: a workspace with no epics, or a type of epic, which cannot be filed under another epic
  • Choose an epic and epic leaves the Type list. Choose that type and the epic you had picked is given up at once, while you are still looking at the form rather than reading a refusal after you press the button
  • Filing at creation adds no separate history line. The epic is part of what was created, and a second line would only push the creation itself up the thread

Tip: The epic's own Add issue has not gone anywhere, and it still inherits the epic's project. Use the field when you start from the board, and Add issue when you start from the epic — both end in the same place.

Projects open their own issues

New issue now sits beside the Issues heading on a project's page, and in its empty state. Until now that page could list issues, group them, price them and take them out of the project — and to add one it sent you to the board. Its empty state said so.

What you'll notice

  • No project field on that form, because the project is already the answer
  • The types offered are the ones Project → Issue types turns on, and they are now enforced rather than only displayed
  • It lands in your first column, the same as an issue opened inside an epic

Tip: A project that has switched task off now opens its form on a type it actually uses. Worth knowing if you have narrowed a project's types and wondered why the board and the project page seemed to disagree about what you could create.

Fixed: drafts under Knowledge could not be opened

Clicking an article in the tree did nothing at all — no error, no change, the panel still reading Nothing selected. Drafts are where people met it, because a workspace with an empty tree has nothing else to click.

Nothing was ever wrong on the server. The page had a field and an action sharing one name, and in the browser the field wins, so the click never left the page.

Tip: Anything you started and gave up on is still under Drafts, most recent at the top. Nothing was lost — it could only not be opened.

Fixed: two forms that drew themselves and then did nothing

Both had been broken on screen while every test passed, because the tests called the code behind them and that code was fine.

  • Add issue on an epic opened with two tabs and nothing underneath them. It had been that way since the page was first written
  • Log time could not be opened on anything that was not an epic. The + beside Spent time had nothing to open

The API enforces the epic rules

POST /v1/issues accepted parent_id after checking only that the epic was yours. Belonging is not the same as being usable: it would file an issue under an ordinary issue, and nest an epic inside an epic. Every screen in Félagi refuses both.

It now answers 422, and the message says which rule was broken.

If you use the API: a request that was accepted yesterday may be refused today. That is the point of the change. Nothing that already respected the hierarchy behaves differently, and PATCH is untouched — it has never accepted parent_id, so an issue still cannot be moved between epics through the API.

What this release does not do

  • The project's form has no description or status field, and the board's still has no description. Both are quick capture on purpose: the issue's own page is where prose gets written.
  • An issue cannot be moved between epics through the API. Naming one at creation is all there is, on that path.
  • Three new shape checks, and none of them can tell whether a form works. They catch a tab panel outside its group, a modal inside another modal, and an action named after a field that hides it — three mistakes that render perfectly and do nothing. Each was checked by putting the fault back. What no test caught, and what found all three bugs in this release, was somebody opening the page and pressing the thing.

Version 0.53.0

Date: 19 August 2026

Nothing you can see changed

This one is housekeeping. No screen looks different and no button does anything new — it is here so the version in the corner matches what is actually running on the server.

What changed is the material the development of Félagi is steered by.

The instructions the agents read opened with an impossible order. They began with a MUST: read a directory that has never existed in this repository. An instruction that cannot be followed is worse than a missing one, because it teaches whoever reads it that the instructions in that file are not the kind you check. It was turned off where it is generated, not edited out of the file, since the next dependency update would have written it straight back.

The architecture notes claimed five things were missing, and three of them shipped months ago — the inbox, autopilots, and the invitation emails. Each was verified before being removed rather than assumed, and the two that are genuinely still missing were verified as still missing.

Three new guards so those particular claims cannot rot quietly again: no page may draw a comment thread, neither guideline file may name a path that does not exist, and the hierarchy limit now says which line on which page it is holding up. Each was checked by putting the fault back and watching the test fail.

None of that can tell whether a sentence is still true. The three that had rotted were found by reading the page, and no test was going to do that.


Version 0.52.0

Date: 15 August 2026

Change twenty issues at once

Switch the issue board to List, tick what you want, and a bar appears saying how many you have chosen. Status, priority, assignee, project — all of them at once.

Until now, tidying a backlog meant opening every issue in turn. It is the first thing anybody hits on a real backlog and the last thing anybody files as a bug, because it does not look like a fault. It just takes all afternoon.

What you'll notice

  • The bar appears only when something is ticked, and carries the count
  • Every change is recorded on each issue's own history, marked as a bulk edit — twenty issues moving at once is exactly when somebody later asks what happened
  • The statuses offered are your workspace's own, under the names you gave them

Tip: Assigning several issues to an agent queues one run each, and a run costs real money — so that action asks first and says how many. It is the only one that does, because it is the only one that spends anything.

The keyboard

Press ? anywhere for the list. That is the only one worth learning, because it tells you the others.

c opens a new issue from any page. / searches. g followed by a letter goes somewhere — g i to issues, g e to epics, g p to projects, and six more.

A tool people live in all day should not need the mouse for the things they do fifty times a day.

What you'll notice

  • Two keys for going somewhere, one key for acting on what is in front of you
  • A g pressed by mistake is forgotten after a moment
  • Nothing fires while you are writing. In a field, a comment or a description, the keys are just letters

That last point is the rule the rest depends on. A shortcut that goes off inside a comment is worse than no shortcut at all — after that happens once, people use the mouse for everything.

Tip: g then i is worth learning first. Most days start by going to the issues, and it is two keystrokes rather than finding the sidebar.

What this release does not do

  • No j/k to move between rows, and no e to edit. Both need a notion of "the current issue" that does not exist yet, and half of it would be worse than none.
  • Bulk editing is on the list view only. The board is drag and drop, where the gesture already means something.

Version 0.51.3

Date: 14 August 2026

Documentation for what shipped this week

Two features arrived without a page to read about them, and one reference page had gone quietly wrong.

New guides

  • GitHub — connecting a repository, naming the issue in the branch, what happens when a branch is cut or a pull request merges, setting up the app that lets Félagi report back, and how to tell whether any of it is working
  • Keeping clients told — subscribers: who hears, when, and which of an issue's two text fields is the one a client actually reads

Corrected

The data model described a table under a name it stopped having earlier today, listed none of the three added this week, and did not mention that a delivery no longer needs a run. A wrong entry in a reference is worse than a missing one: an omission sends somebody to read the schema, and an error sends them off confidently in the wrong direction.

One link pointed at a page that had never existed.

Tip: The documentation lives in docs/ in the repository, and every page is plain Markdown. If something there is wrong or missing, that is a fixable thing rather than a fact of life — and a wrong page is worth reporting more urgently than a missing one.

Guarded

Two checks now run with the tests: every table named in the data model must exist, and every link between pages must lead somewhere. The second found a broken link on the day it was written.

Neither can tell whether a sentence is still true. That part is still a person noticing — this guide said there was no GitHub integration and no plan for one, a fortnight after that stopped being the case.


Version 0.51.2

Date: 14 August 2026

The reports page, properly this time

The fix in 0.51.1 mended one line of the same rename and missed another, which showed itself the moment there was a saved view to draw.

The reason both slipped through is worth saying plainly: the test that covers the reports index has no saved views in it, so the part of the page that lists them never ran during testing. A page can be covered by a passing test and still be broken in the half nobody exercised.

There is now a test that puts a saved view of every kind on that page before drawing it. It was checked by reintroducing both faults and watching it fail on each.

What you'll notice

  • Reports opens, with your saved views on it

Version 0.51.1

Date: 14 August 2026

The reports page is back

Renaming saved reports to saved views in 0.51.0 left one line pointing at the old column name, and the reports index answered with an error page for everybody.

It reached production because of a difference between the database used while developing and the one that runs the product: SQLite quietly accepts a sort on a column that does not exist and sorts by nothing, while MySQL refuses it. The test suite passed locally, failed the moment it ran against MySQL, and the page was broken in between.

What you'll notice

  • Reports opens again. Nothing was lost; the page could not be drawn.

Version 0.51.0

Date: 14 August 2026

The board remembers a filter you want back

Narrow the issue board and Filter → Save this view. It returns from the Views button, and the address carries it — so a link opens the board somebody else was looking at, not merely the page it lives on.

Reports have had this for a while. The board is where people actually spend the day, and rebuilding "my open bugs in this cycle" every morning is the kind of small tax nobody reports as a bug.

What you'll notice

  • Views appears once you have saved one, and names the view currently showing
  • Private until you share it. A view is a working note until its author decides it is a team tool, so sharing is a deliberate act rather than the fallback
  • A shared view can be opened by anyone here and deleted only by whoever made it — one person's tidying should not remove a colleague's tool
  • Saving the same name again replaces it, rather than leaving you with "Mine", "Mine 2" and "Mine final"
  • Clearing the filters lets go of the view, so the button never names something the board is no longer showing

Tip: Share the ones the team argues about. "Which bugs are actually blocking the release" is worth agreeing on once and pointing at afterwards, rather than each person rebuilding their own version of nearly the same question.

Under the surface

Saved views for reports and for the board are one thing now, stored together. What was called a saved report is called a saved view, because a board is not a kind of report — both are simply surfaces you can narrow, and naming it after the first one to need it would have puzzled every reader after today.

Nothing changes for existing saved reports. They are the same views, in the same place, under a name that will still make sense next year.


Version 0.50.0

Date: 14 August 2026

A branch starts the issue it is named after

Cut a branch called ACME-12-fix-the-loop and that issue moves to in progress on its own. No pull request needed, no board to update — the branch is the earliest honest signal there is that somebody has begun, and it usually happens hours before anything else says so.

What you'll notice

  • A new branch named after an issue appears on it, and starts it
  • It lands on whatever your workspace calls "started" — a renamed status is still your status
  • It never drags an issue backwards. A follow-up branch on something already in review changes nothing
  • Deleting the branch takes it off the issue, because a link to a branch that is gone is a link to a 404
  • Ordinary pushes are ignored. A commit-by-commit log belongs in GitHub

Tip: Copy the branch name from the issue and you never have to think about any of this. The identifier at the top of an issue is the only part that matters — ACME-12-anything-you-like works.

Félagi can report back on the pull request

Optional, and off until you set it up. With a GitHub App installed on the repository, each pull request gets a check naming its issue and where it stands, and one comment that is kept current rather than added to.

Two readers, two places: the check for whoever is scanning a list of pull requests, the comment for whoever is reading the conversation. It is the same sentence twice, on purpose.

What you'll notice

  • Administration → GitHub takes an App ID, an installation ID and a private key
  • It asks GitHub whether the credentials work before telling you they do
  • The check reports and does not judge. It is always neutral — a red cross would block somebody's merge over a link, which is not Félagi's place
  • One comment, edited in place. A comment per event turns a pull request into a changelog of a system the reader may not even use

The connection now says which of three things is true, because two of them used to look the same: listening only, replying, and cannot reply. A connection that can hear GitHub and cannot answer it is not a healthy one, and it should not look like one.

Tip: A GitHub App rather than a personal token, and it is worth the extra five minutes. A token carries one colleague's entire account, stops working the day they leave, and signs every comment with their name. The app needs exactly two permissions and is nobody in particular.

What this release does not do

  • Nothing is written to your code, ever. Félagi reports and never pushes, merges, labels or closes anything in GitHub.
  • The check is informational. It cannot be made required, and is not meant to be.
  • Reviews and comments on a pull request stay in GitHub. They are not mirrored onto the issue.

Version 0.49.0

Date: 14 August 2026

A pull request finds its own issue

Connect a repository and the work people do in GitHub shows up on the issue it belongs to. Name the issue in the branch — ACME-12-fix-the-loop — and that is the whole of what anybody has to remember.

The issue then shows the pull request, follows it from draft to open to merged, and finishes itself when the merge lands, exactly as it already did for an agent's work.

That last part is the point. Agents have been delivering pull requests to their issues since the beginning. A person doing identical work delivered nothing, because a delivery had to belong to a run. In a product built on the claim that people and agents are one workforce, that was the wrong asymmetry to have.

What you'll notice

  • Administration → GitHub gives you an address and a secret to paste into a repository's webhook settings
  • The page says whether GitHub has ever called, and whether it is being turned away — because a mistyped secret produces silence that reads exactly like "not set up yet"
  • The issue's timeline says when a pull request opened, merged or closed, and who on GitHub did it
  • An issue in review closes itself when its pull request merges

Félagi never calls GitHub. It holds no token, asks for no permissions and reads nothing — GitHub posts, and Félagi listens. Each workspace gets its own secret, so one can be rotated without touching anybody else.

Tip: The branch name is the most reliable place to name the issue, and Félagi looks there first. Titles get rewritten and often mention a neighbouring issue in passing; a branch is named once, by the person starting the work, and then left alone.

What this release does not do

  • Nothing is sent to GitHub. No comments on pull requests, no status checks, no labels.
  • GitLab and the rest are unchanged: the existing generic delivery hook still takes a URL and a state from any forge.
  • A pull request naming several issues attaches to the first one it names.

Version 0.48.7

Date: 14 August 2026

Changes to two-factor authentication are written down

Turning a second factor on, proving it works, and taking it off again left no trace at all. A change of status on an issue was recorded; the removal of the protection on somebody's account was not.

This came up the plain way: a grace period was found to have gone missing on a live account, and nothing in Félagi could say who had done what, or when. That is exactly the question an audit trail exists to answer, on the one control where it matters most.

What you'll notice

Five events now appear in the activity log:

  • Setting up a second factor, and confirming it
  • Turning it off — the entry somebody will come looking for
  • Abandoning a setup that was never finished, which is deliberately a different event from removing a protection that was
  • Replacing your recovery codes

Each names the account by email address as well as by id, so reading the log does not require a second lookup while you are already worried.

Tip: This starts from today. It cannot explain anything that happened before, which is the ordinary cost of adding a log after you needed one — and a good argument for adding the next one before.

The QR code is back

The square to scan renders again, now that the server has the piece of PHP it needs. Typing the key still works and produces exactly the same result; nothing changes for anybody who already set up their authenticator that way.


Version 0.48.6

Date: 14 August 2026

What's new uses the whole panel

The release notes panel wasted the bottom third of itself. The notes scrolled inside a short window with a large empty area beneath — 350 blank pixels on a 1000-pixel screen.

Two smaller things were wrong in the same panel. A single long command in an older release was wider than the column, and it dragged every paragraph in the notes sideways with it. And once you scrolled, the title and the close button scrolled away with the text.

What you'll notice

  • The notes fill the panel from top to bottom
  • What's new and Got it stay put however far you scroll
  • Commands scroll on their own if they are long, and stay on one line so they can still be copied

Tip: You can reopen this panel any time from the version number at the foot of the sidebar. It is not only shown once — if you skipped past something and want to find it again, that is where it lives.

Fixed

  • The New issue panel had the same fault, unnoticed since it became a flyout: its action bar came to rest a fraction short of the bottom, so the last line of the form could slide underneath the buttons. Both panels were measured this time rather than eyeballed.

Version 0.48.5

Date: 14 August 2026

Setting up two-factor authentication no longer fails

Both screens that turn on two-factor authentication — the one in settings and the one you are sent to when your first days are up — answered with an error page instead of a code to scan.

The library that draws the square needs a piece of PHP that is present where this software is built and missing where it runs. So it worked in every test and failed for every real person, on the one screen somebody is sent to when they cannot get in without it.

What you'll notice

  • The screen works. Where the square cannot be drawn, the key is shown to type into your authenticator instead — 1Password, Bitwarden, Aegis, Google Authenticator all take a typed key
  • The wording changes to match, so "where is the QR code?" is answered rather than left hanging

The square itself comes back once the missing piece is installed on the server. Typing the key produces exactly the same result in the meantime.

Tip: An office on the IP allowlist is never asked for a second factor — which is why this went unnoticed. If you are testing whether something works for everybody, test it from outside the office. A trusted address hides failures rather than fixing them.

Fixed

  • Running the project locally with npm run dev produced an unstyled page. The security policy Félagi sends, which is right in production, was refusing the local development server's stylesheet and JavaScript. Nothing said so except the browser console.

Groundwork, not yet switched on

Preparation for connecting GitHub landed in this release: an issue can now hold a pull request that no agent produced, which is what a person's work needs. There is nothing to configure yet and nothing changes until there is — it is mentioned only so the schema change is not a surprise.


Version 0.48.4

Date: 11 August 2026

The password gate looks like a gate

The screen that asks you to replace a password an administrator chose was drawn inside the full application: a sidebar listing Dashboard, Projects, Issues and the rest, where every single link brought you straight back to the same screen.

Worse, the What's new dialog opened on top of it — covering both fields and the button — with notes about a version the reader had never used. Until the previous release it could not even be dismissed.

The two-factor screens have always been drawn plainly, without the application around them. This is the same kind of screen: a gate in front of the product, not a room inside it.

What you'll notice

  • Choosing your first password is a single quiet screen, like signing in
  • Nothing appears on top of it, and nothing offers to take you somewhere you cannot go

Tip: If you make an account for a colleague, the password you hand over is meant to be used once. They are held on this screen until they replace it — so give them the password by voice or in person, and let the product take it from there.


Version 0.48.3

Date: 11 August 2026

The two screens that could hold you had no way out

Two screens can stop you: choose a new password, when an administrator created your account, and set up two-factor authentication, when your first days are up. Both are built so that the only thing you can do is the thing that releases you.

Neither worked. The form saved nothing and the button did nothing — no error, no message, just the same screen again.

Both screens are built with Livewire, and Livewire talks to the server through an address of its own. The guard on each screen was checking which page you were on, did not recognise that address, and turned the request away. So the page built to release you was the one page that could not run.

What you'll notice

  • Choosing a new password works, and lets you into the application
  • Setting up two-factor authentication works: the code appears, and the six digits are accepted

If you were stuck on either screen, try again — there is nothing to redo.

Tip: If a screen ever seems to ignore a button entirely — no error, no spinner, nothing — that is worth reporting as a bug rather than retrying. A refusal says something; silence usually means the request never arrived.

Settings is no longer reachable before you have proved who you are

While you are being held for two-factor authentication, the settings page used to stay open, so that you could turn the protection on from there. Enrolment has had a page of its own for some time, and settings carries the API tokens panel — and an API token bypasses the second factor completely.

Anyone holding a stolen password could have made one from the very screen that was supposed to be stopping them.

What you'll notice

  • Held for a second factor, you are sent to the setup page, and settings waits until afterwards. Nothing changes once you are through, or during your first five days.

Version 0.48.2

Date: 11 August 2026

An account made for you now gets the same five days as one you asked for

Where two-factor authentication is required, a new colleague has five days to set up an authenticator app before it is demanded of them. That grace was granted when somebody accepted an invitation — and nowhere else.

So an account created for a colleague by an administrator began with no grace at all. They met a demand for an authenticator app on the login screen, before they had seen a single page of the product. The grace exists precisely for those first five minutes; how the account came to exist has nothing to do with it.

What you'll notice

  • Anyone an administrator adds gets the same five days as anyone who accepts an invitation
  • Nothing changes where two-factor is not required — no countdown starts that would outlive the setting

If you created an account for somebody and they were asked for a second factor straight away, that was this. Their five days start from the upgrade.

Tip: The office IP allowlist under Platform → Security exempts an address from two-factor entirely. It is the right tool for a known office, and the wrong one for covering up an account that was never given its grace — the first person to work from home finds out which you did.


Version 0.48.1

Date: 11 August 2026

Three fields that never saved

An issue's due date, its estimate, and a department's hourly rate could be typed into, and the value stayed on the screen when you tabbed away. It was never anything more than text in your own browser. Nothing reached the server — not once, since the fields were built.

Livewire 4 changed what one modifier means. In version 3 it controlled when a change was sent to the server; in version 4 it controls when the browser updates its own copy, and sends nothing. We upgraded and did not follow it through on these three.

There was no error to notice. No message, no entry in a log, no failing test — just a number that was still there when you looked at it, and gone when you came back.

What you'll notice

  • Due dates, estimates and department rates save when you tab out of the field, and are still there tomorrow
  • The small saved note beside the field now means what it says

If you set any of these before today, they were not recorded. They are worth setting again.

Tip: The properties panel is meant to be typed into directly — there is no Edit and no Save for a due date. If a field there ever doesn't show saved after you tab away, that is a bug worth reporting rather than a step you forgot.

Guarded

A test now fails the build if any field is bound with the modifier that only updates the browser. The first version of that test passed with the bug deliberately put back — it is only a guard if it fails on the thing it guards against.


Version 0.48.0

Date: 11 August 2026

Attaching a file is no longer editing

Issues and projects have taken attachments since 0.41.0, and the dropzone was inside the edit form. So leaving a log on an issue meant clicking Edit, scrolling past the title, the description and the note that goes to subscribers, dropping the file, and pressing Save on all of it.

Attaching a file is not editing. It is the same errand as commenting: additive, and finished the moment it is done.

What you'll notice

  • A dropzone on every issue and project while you are simply reading it
  • Files upload the moment you drop them — there is no Save
  • New issue takes attachments, which it never did at all
  • Cancelling an edit no longer takes a file back, because the file was never part of the edit

Tip: Screenshots belong on the issue, not in a comment. A comment is a moment in a conversation and scrolls away; an attachment on the issue is still at the top of the page in March, which is where the person picking the work up will look for it.

The new-issue form no longer outgrows the window

It was a centred box eight fields tall. On a laptop the heading scrolled off the top, the buttons sat below the fold, and it covered the board the issue was being filed against.

What you'll notice

  • New issue opens as a panel from the right, at full height, with the board still visible beside it
  • The buttons stay at the bottom however far you scroll
  • Attachments moved to the end, where an optional thing belongs — it used to sit between Project and Type and split the form into two unrelated halves
  • The panel is the same colour as the rest of the application, which it was not

Fixed

  • An epic's progress counted children whose status was literally called done. It was missed when statuses became data in 0.45.0, so a workspace that re-classified its statuses saw every other figure agree and the epic quietly disagree.

What this release does not do

  • Comment attachments are unchanged. They still belong to the comment.
  • There is no drag-and-drop onto the page as a whole; the dropzone is a target, not the whole surface.
  • Files still cannot be renamed or replaced once uploaded. Delete and attach again.

Version 0.47.0

Date: 8 August 2026

Epics you can find

Epics have worked since the beginning, and nobody could find them. An epic is a type of issue, so it sat on the board among the issues with nothing to mark it out and no way to ask for one — which is why this installation had none at all.

There is now an Epics page in the sidebar. It leads with progress rather than status, because a status chip says "in progress" for a month while the question about an epic is how much of it is left.

What you'll notice

  • Each epic with a progress bar, a count of its issues, its target date and how many people outside the company are subscribed to it
  • Open, Finished and All, opening on the open ones
  • A Type filter on the issue board, which was missing entirely
  • An empty page that says where epics come from, since "set the Type to Epic" is not something anybody guesses

Nothing changed about how they work. An epic is still an issue with issues inside it, which is why it can be assigned, estimated, commented on and subscribed to like anything else.

Tip: The envelope count on an epic is the number worth watching. It means somebody is waiting for this who cannot see your board — and it is the one figure that tells you an epic has an audience before you change its date.

A progress bar that had stopped agreeing with everything else

When statuses became data in 0.45.0, an epic's progress was left counting children whose status was literally called done. Every other figure on the page had moved to asking what a status means.

A workspace that renamed or re-classified its statuses would have seen a project's progress, a burndown and a cycle report all agree with each other, and an epic quietly disagree.

What you'll notice

  • Epic progress now follows the same rule as everything else: a child counts as finished when its status is in a finished category, whatever that status is called

What this release does not do

  • There is no roadmap or timeline view of epics. The Gantt already draws them; this page is a list, not a second Gantt.
  • Epics cannot be nested. An epic inside an epic is a hierarchy that has to be kept in step with the projects, and one is enough.
  • The board filters by type but does not group by it.

Version 0.46.0

Date: 8 August 2026

Keeping people outside the company in the loop

A client waiting on a piece of work had to be told by hand, or not at all. You can now add somebody to an issue, an epic or a project, and they hear twice: when the work starts, and when it is finished.

They are not given an account. An external stakeholder wants an email, not a password — and an account would put them in your member list and in every permission question from then on.

What you'll notice

  • Told about this in an issue's properties panel: an address, a name, and that is all
  • A separate box, What to tell people outside — one sentence, written for a stranger
  • Every message carries your installation's name, in the heading, the signature and the footer
  • One-click unsubscribe, including the button Gmail and Outlook show at the top of a message
  • Somebody who has unsubscribed is shown as such rather than hidden, so you do not add them again next week

Tip: The description is never sent, and cannot be. It is written for colleagues and will one day say something nobody meant a client to read. The What to tell people outside box is the only text that leaves the building — there is a test that puts an unflattering sentence in a description and fails if it reaches the email.

What a piece of work cost

Every issue and project now shows what it cost, counting people and machines in one figure. Nothing else can: only Félagi knows that an agent spent four minutes and eighty øre while a person spent forty minutes.

Rates are set per workspace, and per department where a department charges differently.

What you'll notice

  • A cost line on an issue and on a project, split into people and machines
  • A currency of your choosing; agent providers bill in dollars and are converted once, when the run finishes
  • Hours worked by somebody who has since left are still counted at the workspace rate, because their work did not become free when they resigned

Tip: An unpriced hour is reported as unknown, never as free — and a total that is missing something says so, in as many words. A cost report that quietly treats people as costing nothing is the most misleading thing it could say, so the figure always travels with what it could not price.

What these releases do not do

  • Rates are not stored per person. A per-person rate is a salary, and a tracker that stores salaries is a different product with obligations this one has not taken on.
  • Subscribers hear about status, and nothing else. Not comments, not attachments, not who is working on it.
  • There is no digest. Follow a project with forty busy issues and you will get forty emails, which is why the epic is usually the right thing to follow.
  • Nothing converts a past cost when a currency moves. Last quarter keeps the figures it was recorded with.

Version 0.45.0

Date: 8 August 2026

Statuses are yours to name, and yours to define

Félagi's seven statuses were written into the code. A team that works in QA, or waits on a customer, or ships before it closes, had to pretend one of the seven meant that.

Administration → Statuses now holds them. Rename them, reorder them, mark which ones mean nobody is moving the work — and, more importantly, say what each one means.

That last part is the whole feature. Every status carries a category: backlog, unstarted, started, completed or canceled. Reports never read the name, they read the category. So renaming In review to QA changes nothing anybody counts, while re-classifying it as completed changes every count at once — progress, burndown, velocity, throughput, the project tabs — with no report edited and no code changed.

What you'll notice

  • The name you choose appears everywhere an issue's status does
  • Changing what a status means moves the numbers immediately, and is recorded in the workspace's history with both values, because a graph that changes its mind should be able to say why
  • Paused is its own switch, not a category: a stalled issue stays counted as open work
  • completed and canceled both close an issue, but only completed becomes velocity

Tip: The key column is shown and cannot be edited, and that is deliberate. An issue stores the key, the API sends it and your scripts read it, so a rename must leave all of them working — the same reason renaming a project does not renumber its issues.

What this release does not do

  • You cannot add or remove statuses yet. The seven can be renamed, re-classified and reordered; a new one could be shown but not stored, because the database still holds a fixed set. A form that offers what the database refuses is worse than one that does not.
  • Statuses are per workspace, not per team or project. There are no teams yet.
  • Nothing migrates issues when a status changes meaning. The issues stay where they are; what changes is how they are counted.

Version 0.44.0

Date: 8 August 2026

The same bug, reported twice

An issue can now be marked a duplicate of another. Search for the one it repeats, confirm, and the two become one: comments, files, hours and labels move to the issue where the work is carrying on.

The duplicate is closed and kept, never deleted. By the time somebody notices two issues are the same, one of them is written in a commit message, a standup and somebody's notes. Deleting it turns all of those into dead ends; keeping it turns them into a signpost. Open it and the first thing on the page says where the work went.

What moves is content. What stays is everything the surviving issue had already decided — its estimate, its status, who it is assigned to, which cycle it is in. Two estimates do not make a better one, and an issue that quietly changes hands because somebody merged something into it is an issue nobody is watching any more.

What you'll notice

  • Same as… in the properties panel, next to dependencies but deliberately apart from them: "waits for" is about order, "same as" is about identity
  • The confirmation says what will move before it moves
  • The surviving issue lists what was folded into it, struck through and still clickable
  • Not a duplicate undoes the pointer — what was moved stays where it went, because moving it back would rewrite the same history a second time
  • Marking a duplicate of something that is itself a duplicate lands on the issue at the end of the chain, not in the middle

Tip: Hours move with the merge, and that is deliberate even though it changes what a timesheet says somebody worked on. Hours left on a cancelled issue disappear from every report about the work they paid for. Both totals are recalculated from the entries themselves, so neither can drift away from its own evidence.

An identifier the API hands out is now one it accepts

GET /issues/ACM-231 used to answer 404. The API published ACM-231 as an issue's id, the reference called it "the identifier a person types", and then the route would only take the database key — so feeding a response field straight back into a URL failed.

What you'll notice

  • ACM-231, acm-231 and any project prefix the workspace uses all resolve
  • A bare number still means the database key, which is a different number — both forms are documented side by side now
  • An unrecognised prefix is a 404 rather than a quiet fall back to the number, so XYZ-231 never returns issue 231

Tip: If you script against Félagi, prefer the prefixed form. It is stable across installations in a way the database key is not, and it is what every response gives you.

What this release does not do

  • Duplicates are not detected for you. Nothing suggests that two issues look alike; you mark them.
  • An issue cannot be merged across workspaces, and an epic holding issues cannot be merged at all — move its children out first.
  • There is still no way to split an issue in two, which is the opposite errand.

Version 0.43.0

Date: 7 August 2026

Settings looks like the rest of Félagi

Every card on the settings page was drawn a little darker than the page behind it, in a slightly different shade — grey where everything else is blue. A raised surface that is darker than what it sits on does not read as raised; it reads as a hole. With the input fields in a third shade, three greys argued their way down the page.

This was the one screen that never used the product's own colours. It does now, so it follows the theme instead of guessing at it, and cards sit one step lighter than the page in both light and dark mode.

What you'll notice

  • Cards that look like surfaces rather than cut-outs, in dark mode especially
  • One rhythm down the page: a heading, a line explaining it, then the panel — the two-factor and API token sections used to label themselves from the inside
  • Hairlines instead of drop shadows, matching every other page
  • Two-factor authentication now says what state it is in next to the badge, rather than leaving the badge to speak for itself
  • Your initials use the same avatar as every comment and member list, not a gradient this page invented

Tip: The Change password panel now says out loud that two-factor authentication is left alone. It always was — that is deliberate, because a password reset that also cleared two-factor would make one leaked password enough to take over any account.

What this release does not do

  • Four pages still carry the old hand-picked colours: the three Platform admin screens and the sidebar. They are listed in a test that refuses to let the list grow, so nothing new drifts while they wait.
  • Nothing about settings changed in behaviour. Every form posts where it always did.

Version 0.42.0

Date: 7 August 2026

A project can name its own issues

Every issue in a workspace was called ACME-something. A project can now set its own prefix, so the issues under it read PRE-14 instead — set when you create the project, and changeable afterwards.

Only the prefix belongs to the project. The number is always the workspace's, and that is the decision the whole feature rests on: move an issue to another project and the label in front of it changes while the number behind it does not. PRE-14 and ACME-14 are the same issue, and both still find it.

Numbering each project separately would read better and behave worse. It would mean renumbering an issue every time it moved, or two different issues called PRE-1, and an identifier written into a commit message last month that no longer resolves to anything.

What you'll notice

  • An Issue prefix field beside the name, on the create form and when you edit a project
  • Issues, the board, the search box and the API all use it
  • Typing PRE-14, ACME-14 or just 14 in ⌘K finds the same issue
  • Leave it empty and the project falls back to the workspace prefix
  • A prefix already in use by another project, or by the workspace, is refused

Tip: Changing a prefix does not invalidate anything anybody wrote down. The old identifier still resolves, because the number never moved — and the change is recorded in the project's history with both values, so the log explains itself.

What this release does not do

  • The prefix is not part of the URL. An issue still lives at its numeric address.
  • There is no per-project counter, and there will not be one. See above.
  • Renaming a prefix does not rewrite it in old comments or commit messages. Nothing could.

Version 0.41.1

Date: 6 August 2026

Seven scheduled jobs that had never run

Everything Félagi does on a timer was scheduled by shelling out to a php command. On this server PHP is compiled into the application server and there is no php on the path, so each one exited immediately with "command not found" — reported nowhere anybody looks.

That is every timed feature in the product:

  • Stalled runs were never swept up. A run whose machine went to sleep sat in the queue looking busy, and the queue behind it stopped.
  • Autopilots never fired. A schedule was saved, shown, and never acted on.
  • Timers left running were never closed at the end of the day.
  • Cycles were never rolled, so the burndown never took its daily reading — and a burndown is recorded, never reconstructed, so those days are gone.
  • Recurring meetings were never extended.
  • The nightly integrity check never ran, which is the one that would have said so.
  • Skill proposals were never discovered.

They run now, in the application's own process rather than through a shell.

What you'll notice

  • Autopilots fire at the times they say they will
  • A run whose machine disappears is cleaned up within the minute, instead of blocking everything behind it
  • Cycles close on their own, and the burndown starts recording from today

Tip: The burndown for any cycle that ran before today has no readings and will say so rather than draw a line. That is deliberate: a reconstructed burndown applies today's estimates to every past day, which is a picture of a week that never happened.

What this release does not do

  • It does not backfill. Nothing recovers the readings, the meetings or the proposals from the months when nothing ran.
  • Two configuration warnings remain on this installation, about queue and cache slots. They are settings, not code, and are not changed by an upgrade.
  • Nothing warns you if the scheduler itself is not running. The check that would have caught this was one of the seven that never ran.

Version 0.41.0

Date: 6 August 2026

Files belong to the thing, not to a remark about the thing

A project or an issue could only carry a document by way of a comment. So a brief, a contract, a specification — the papers a piece of work is about — lived inside somebody's passing remark on it, and were found by scrolling.

Both now hold files of their own.

What you'll notice

  • An Attachments box when you edit a project or an issue, taking several files at once
  • The files are shown on the page itself, not hidden behind Edit
  • Uploading adds; it never replaces what is already there
  • Deleting a file deletes the file, not just the row that mentioned it
  • Files chosen and then abandoned go with the cancellation

Tip: On an issue there are two upload boxes and they are deliberately separate — one for the issue, one for the comment you are writing. A file meant for a comment will not quietly ride along with the next save of the issue.

Documents open in the page

Clicking a file used to mean downloading it, opening it in another application, and going back. Images already showed themselves; nothing else did.

PDFs and text files — CSV, JSON, XML, logs, Markdown — now open in a viewer inside Félagi, with Download still one click away when you want the file rather than a look at it.

What you'll notice

  • A Preview button on anything that can be shown
  • PDFs render in place; text is shown as text, exactly as it is stored
  • Images open full size instead of in a new tab
  • Escape closes the viewer

Tip: SVG files still download rather than preview, and that is not an oversight. An SVG is an image and also a document that can carry a script, and the safe thing to do with somebody else's is to hand it over rather than run it.

What this release does not do

  • Office documents — Word, Excel — still download. Showing them would mean converting them, and a converted document is not the document.
  • There is no preview from the list of issues or projects; you have to open one.
  • Attachments still cannot be set through the API, and a CSV export does not contain them.
  • Nothing scans an upload for malware.

Version 0.40.0

Date: 6 August 2026

Departments, and a phone number

A person can now be filed under a department, and departments are kept in Administration → Departments. The picker is searchable, because a company with forty of them has a list nobody scrolls.

The two new fields deliberately live in different places, and the difference is the whole design:

  • A department belongs to the membership. Somebody can be in Engineering in one workspace and Finance in another, and neither statement contradicts the other.
  • A phone number belongs to the person. It follows them into every workspace they join, and three copies of one number is three things to keep current.

A department is deliberately a name and nothing else — no lead, no budget, no parent. One with those is an org chart, and an org chart is a second hierarchy to keep in step with the projects.

What you'll notice

  • A Department column in the member list, with a searchable picker on every row
  • A department field on the invitation form, applied when the invitation is accepted
  • A Phone field beside the name and address when an administrator edits a person
  • Dissolving a department removes nobody: they stay in the workspace and lose the department, and the confirmation says how many people that is first
  • Engineering and engineering are one department; the first spelling wins

Tip: If an invitation is still open when its department is dissolved, the invitation still works. The person joins without one rather than being unable to join at all.

Issues and projects can be rewritten

The title and the description of an issue, and the name and description of a project, were read-only once created. Everything about them could be changed — status, assignee, dates, estimates — but not a word in them.

Editing happens in the page itself rather than in a side panel. A due date is a value and belongs in a panel; a title and a description are the thing the page is about, and a narrow column is no place to rewrite a paragraph.

What you'll notice

  • An Edit button beside the heading on any issue or project
  • Formatting, lists and links survive; anything else is stripped when it is saved, exactly as it is when the issue is created
  • An empty description now says "No description. Write one." rather than showing a blank
  • A rename is recorded with both names, so the history still makes sense afterwards
  • A project icon can finally be set — it was shown on two pages and editable on none

Tip: Renaming a project matters more than renaming an issue. Every activity entry a project writes stamps the name of the day into it, so without both names the log reads as though a different project did the work.

Long pages became tabs

Two pages grew faster than they read.

On an issue, the stream mixed comments and system events. On an issue reassigned nine times, four one-line events between every pair of comments is how a thread becomes unreadable. It is now Conversation, Activity and Everything, and it opens on the conversation.

On a project, finished issues buried the live ones. It is now Open, Closed and All, and it opens on Open.

Both keep a merged view on purpose. Splitting a page must not answer "what was said" and "what happened" while losing "what happened because of what" — the merged stream is the only place an assignment sits directly above the reply it caused.

What you'll notice

  • Tabs carry counts, so you can see what is behind one without opening it
  • On hold counts as open, not closed. Nobody is moving it, but it is still somebody's problem, and filing it with the finished work is how a stalled issue stops being looked at
  • An empty tab says which situation it is: a project where everything is finished and one that has not started are different, and get different sentences

Tip: The Everything tab is where an issue explains itself. Read it when you want to know why something happened rather than what.

A panel with no Save button now says it saved

The properties panel on an issue has never had one, because every field writes itself on blur or on Enter. That is a reasonable design and was an unstated one.

One field was worse than unstated. The label box held its text in the browser and nowhere else until it was submitted, while looking exactly like the fields beside it that save themselves — so a typed label disappeared on navigation without a word.

What you'll notice

  • A small saved appears beside a field that has just written itself, and fades
  • The label box has a + button, because it is the one field that genuinely needs a gesture

Tip: The confirmation is fired by the write, not by the page drawing itself. A confirmation that appears when nothing happened is the kind people learn to disbelieve.

What this release does not do

  • Departments have no hierarchy, and none is planned.
  • Reports do not group by department, and a department cannot be set over the API.
  • An issue's description has no revision history. Articles keep 50 versions; issues keep none, and an overwrite is final.
  • The tabs do not remember which one you were on when you come back to a page.
  • Nothing bulk-edits. Filing forty people into departments is forty pickers.

Version 0.39.1

Date: 5 August 2026

A column header was missing, so 40 looked like a date

The member list had five headers and six cells. The weekly-hours box therefore sat under Joined, the join date under nothing, and the actions under nothing either — so the capacity number that every Gantt projection divides by read as a date.

It has been that way for as long as hours have existed. A table with a gap mislabels every column after it, silently and forever, so there is now a test that counts the rendered headers against the rendered cells.

An administrator can edit a name and an address

Asked for, and missing: only the person themselves could, at Settings.

A name is cosmetic. An address is not, and the reason is specific: a password on an account with an authenticator gets nobody in, but changing the address moves where a password-reset link goes. It is a step towards a takeover rather than a takeover, and it gets the loudest control in the feature.

  • The old address is emailed, because it is the only place the rightful owner can still be reached once the new one is in place. Whoever is at the new address already knows.
  • The new address loses its verification, the same rule the settings page follows. An administrator asserting an address is not the same as that address answering, and a typo marked verified is a person nobody can reach.
  • Both addresses are recorded, so the audit trail says what it was and not only what it became.
  • Two-factor is untouched, as everywhere else here.

An address another account already uses is refused with a 409. Changing only a name sends nothing and logs nothing about addresses — there is no one to warn.


Version 0.39.0

Date: 5 August 2026

Accounts are by invitation. Public registration is closed by default.

An open door is the wrong default for a project tracker. Anybody who found the URL got an account, landed on "you are not in a workspace yet", and created one — which on a self-hosted installation means a stranger with a workspace inside your Félagi.

What stays open

Closing the door does not close the paths that are deliberate:

An invitation Somebody asked by name. This is what by invitation means
A platform administrator creating an account Deliberate, logged, password replaced on first sign-in
felagi:platform-admin Shell access proves rather more than an invitation would, and there has to be a first account
Single sign-on A standing invitation for one domain rather than for one person

FELAGI_OPEN_REGISTRATION=true opens it, for anybody running Félagi as a service rather than for one company.

The page, and the pages that linked to it

/register answers with a page, not a 404. Whoever arrived followed a link — the landing page, a colleague, a bookmark from before the door was shut — and "not found" says the application is broken rather than saying what to do next.

Five places advertised registration. All of them now check first, because "Start free" on an installation that does not let people sign up is true about the product and false about that copy of it — the same lie "Invitation sent" was this morning, in the same shape.

Posting the form is refused as well as hidden. A form that is not rendered is not a form that cannot be posted, and the missing one of those two guards is the whole vulnerability.

The test found a path I had forgotten

The guard is written as a sweep over every place a users row is created, rather than as a list of the ways in. On its first run it found a fifth: felagi:platform-admin.

That is the argument for the shape. A list contains what its author remembered. All five are now enumerated with the reason each is allowed to exist, so anybody adding a sixth has to say which side of the invitation rule it falls on.

Upgrading

Nothing to migrate. If you were relying on open registration, set FELAGI_OPEN_REGISTRATION=true.


Version 0.38.0

Date: 5 August 2026

A platform administrator can now maintain an account from Administration → Members: give somebody a new password, or create an account outright instead of waiting for an invitation to be accepted.

This is the most dangerous thing in the application, so most of the work is in what it refuses to do.

It never touches two-factor authentication

A new password on an account with an authenticator confirmed does not get anybody in — and that is the point. If it cleared the secret as well, one account could take over any other in two clicks and every other control in the product would be decoration.

Somebody who has lost both their password and their authenticator has a different problem, and recovery codes are the answer to it. There is a test whose only job is to assert that the three two-factor columns are byte-identical before and after a reset.

It does not verify an unverified address either, and does not make anybody an administrator. The list of columns it writes is three lines long for exactly that reason.

Not the workspace owner's

is_admin only. A person can be in three workspaces, and owning one of them says nothing about the right to change the password on their account.

Whose
Remove somebody from this workspace The owner's — it is workspace-scoped
Change their role or hours The owner's, same reason
Set a password The platform administrator's

What happens

The password is generated, twenty characters, letters and digits only — this one is going to be read aloud down a phone. It is shown once and stored nowhere it can be read back. The account is marked must_change_password, which the existing RequirePasswordChange middleware already enforces, and the remember token is dropped because every other session was opened with the old password.

The person is emailed, and the message names who did it. A silent password change is how a takeover goes unnoticed for a month; nobody has ever regretted that email. It carries no password — a working credential sitting in a mailbox is worse than one read off a screen — and it says the authenticator was untouched, which is the reassuring half and the useful half at once.

An administrator cannot reset their own. The settings page does it properly, asking for the current password first, which is the check that makes a password change a password change rather than a session hijack.

Creating an account outright

Create account makes the user, adds them at a role you pick, and hands you a one-time password. No link to accept. The honest case is an internal deployment where an operator adds colleagues who never asked to be invited — which is also why it belongs to the platform administrator and nobody else.

An address that already has an account is refused with a 409: two accounts for one address is a person who cannot be told apart from themselves.

The third time a database default bit

is_admin on a freshly created user was null, not false — the database applied the default and the model did not know. Nothing compared it strictly, so nothing was broken; the reason to fix it is that $user->is_admin === false reads as obviously correct and would have been quietly wrong.

MeetingSeries::$active and Project::$status laid the same trap earlier. User now restates its schema defaults, as they do.

Upgrading

Nothing to migrate. must_change_password and the middleware that enforces it have been there since 0.9.


Version 0.37.0

Date: 4 August 2026

Images are now served at the size they are drawn at. Measured, not guessed:

Bytes Share
A phone photo of a physical whiteboard (4032×3024) 9 199 KB 100%
What a board actually draws 615 KB 6.7%
What an attachment list actually draws 67 KB 0.7%

Both places Félagi showed an image scaled it in CSSmax-h-64 in a list, h-full w-full on a board — so the browser downloaded the whole file and shrank it. A board with ten such photos was ninety megabytes per person per opening, re-fetched every five minutes because that was as long as the response was cached.

Verified over HTTP: a board that would have transferred 9 199 KB now transfers 615.6 KB.

Laravel 13's image API

Illuminate\Image ships with the framework, so this is a first-party API rather than a bolt-on. It needs intervention/image and either GD or Imagick.

Three decisions that made it safe to add

  • A derivative is a cache of the original, exactly as search_text is a cache of an article's file. Never backed up, regenerable, and felagi:check has nothing to say about it. It fits the backup work from 0.34.0 rather than complicating it.
  • Made on request, not on upload. No backfill migration, no storage spent on images nobody opens, and every attachment that already exists gets a thumbnail the first time somebody looks at it.
  • It degrades to the original. No image extension, an image too large to decode safely, one already small enough, a file that claims to be a PNG and is not — every refusal serves the original, so the URL always answers with a picture and nothing in a template has to check. GD is an improvement here, not an installation requirement.

Named sizes only

Two variants, thumb at 512 and large at 1400. Nothing accepts a width from the URL. A ?w= parameter is an invitation to fill a disk: a thousand requests for a thousand widths is a thousand files, and it needs nothing but a browser. An unknown size is a 404 rather than a guess.

SVG is excluded twice over — a vector has no size to reduce, and handing one to an image processor is a parser to attack for no benefit. It was already forced to download rather than render inline, for the stored-XSS reason.

EXIF orientation is applied before scaling, or a portrait phone photo arrives on its side: re-encoding drops the metadata that was rotating it.

A derivative is cached in the browser for a week and marked immutable, because an attachment's bytes never change — the only thing that can change a thumbnail is a change to the variant's size, and that comes with a new path.

One silent bug

route($name, $parameters, $absolute) — the third argument is a boolean. So

route('attachments.show', $this->attachment, ['size' => 'large'])

dropped the size, served the whole file, and reported nothing. No error, no warning, and the only symptom is a slow board. There is a test asserting the board asks for size=large and means it.

Upgrading

composer install
npm run build

IMAGE_DRIVER defaults to gd. If neither extension is present, originals are served and everything works.

What this release does not do

  • No processing on upload, and so no queued job. The first person to view an image pays for one resize.
  • No arbitrary sizes, deliberately.
  • The original is never modified. It is the record; a derivative is a copy.
  • Nothing for the knowledge base's inline images, which are markup in an article rather than attachments.

Version 0.36.0

Date: 4 August 2026

The front page promises skills that compound. They did not: a person wrote a skill, an agent read it, and nothing ever came back — even though the material had been in the database the whole time.

The sequence

felagi:skills runs nightly and looks for a shape with its own proof:

  1. A run does not land — it failed, or it finished and had to be run again.
  2. A person says something on the issue.
  3. A later run by the same agent succeeds.

Bragi · ACM-21 · first run failed The suite needs --run or vitest sits in watch mode waiting for input and the sandbox kills it on the timeout.

That comment is a skill somebody already wrote and never filed. Already in English, already specific to this repository, and it demonstrably changed the outcome.

Nothing is generated

This is the part worth being clear about. A proposal assembled from a model's reading of a transcript would be a guess about a lesson. This is the lesson — the offer is to file it, not to write it.

It also means the feature needs no model, no daemon change and no key. It is a query.

What it refuses to call a lesson

Each of these threw away a candidate that looked plausible:

A single run Without a second attempt there is no "then it worked" to point at
A comment after the successful run The order is the argument. That is a reaction, not a correction
An agent's own comment An agent narrating its run is not somebody teaching it something
Under 40 characters "Thanks", "try again", "yes"
More than 72 hours later Wide enough for somebody who looked the next morning; narrow enough that an unrelated remark a fortnight on is not mistaken for a fix
A different agent's success Not evidence that this one learned anything

Filing one

Appended to a skill you choose, with a line saying which issue and when — never overwritten. A skill is a document somebody wrote, and replacing it would make accepting a proposal a destructive act nobody would risk twice. It goes in as plain text, because skills reach a CLI verbatim and the comment box's markup would arrive as content.

Not a lesson records a dismissal rather than deleting the row. Discovery is idempotent on the comment, so a dismissal that left no trace would be the same suggestion again tomorrow morning — and a list that re-proposes what you have already looked at is a list you learn to ignore.

The proposals sit above the skills on the page, because a proposal expires in usefulness: the person who remembers the conversation is the one who can judge it, and that memory has a half-life.

One bug, and it was a good one

The correction window let anything through. diffInHours is signed in Carbon 3, so a comment a fortnight after a run measured −456 hours, which is comfortably under a 72-hour limit — the window was bounded in one direction and infinite in the other. The test that caught it was the one asserting a remark two weeks later is not a fix.

Upgrading

php artisan migrate

felagi:skills is new, daily at 01:15. Nothing is proposed until a workspace has the sequence — which needs at least two runs on one issue and a comment between them.

What this release does not do

  • Proposals are noticed, never drafted. The only lesson Félagi will suggest is one somebody has already written.
  • No proposal from a run's transcript. Repeated failing commands are a signal and they are not read yet.
  • No API, and no proposal from a whiteboard, a meeting or an article.

Version 0.35.0

Date: 4 August 2026

Félagi now knows how wrong its own estimates are, and spends that on the plan.

7d 3h 30m planned has historically meant 8d 6h 13m

Capacity answers "does this fit?" from the numbers on the issues. This answers it from the numbers those estimates have turned into — and the case it catches that a capacity check cannot is a window that fits on paper and has never fitted in practice. When that is what is happening, the page says so.

Whose number this is

An estimate is usually not written by the person who does the work. A lead sizes a backlog, a planning meeting agrees a figure, somebody types "2h" because the last one like it was 2h. So this measures the estimating, not the estimator, and the place to spend it is a plan rather than a person.

What counts as evidence

The design rests on a distinction Félagi already made in 0.26.0. Calibrating an estimate against remembered time compares a guess with a guess and calls the ratio a fact. So an issue is a sample only when:

  • it had an estimate, and
  • every hour logged on it was measured — a stopwatch or a daemon; one remembered entry and the actual is partly a recollection, and
  • all of that time is one actor's, because an estimate is for the work and an issue two people shared says nothing about either of their estimating.

Each condition throws data away on purpose. The remaining samples are the ones that can be compared honestly.

The consequence nobody else can compute

Agents end up the best-calibrated actors in the product — not because they are better estimators, but because every minute a daemon reports was watched, so their column has no recollection in it at all.

In this workspace's own data, the human factor is 1.50× and the agent's is 1.03×. That is a sentence no other tracker can produce, because no other tracker employs the machine and measures what it did.

The arithmetic, and its refusals

The median of the per-issue ratios, never the ratio of the totals: totals let one large issue decide the factor, and a fortnight-long task that ran over would drown twenty small ones that were fine. Every estimate gets one vote.

Three deliberate refusals:

  • Silence below five measured issues. A ratio from two observations is a coincidence, and presenting it as a factor is worse than saying nothing.
  • Capped at 4× — not a correction of the data but a refusal to multiply by it. A factor of twelve says estimates and reality are unrelated here, and the resulting number is one nobody would use.
  • Never narrowed by the report's filters. Calibration is a property of a workspace's whole measured history, and a number that moves when you change a date picker is not a calibration.

Somebody with too little history of their own gets the workspace's factor rather than 1.0, because a new colleague has no history and is not therefore a perfect estimator.

Where to see it

The bottom of the time report: the workspace factor, and a row per actor with their typical estimate beside their typical actual. It is linked from the cycle callout, so the number in a planning conversation can always be traced to the issues it came from.

Upgrading

Nothing to migrate. The factor appears once a workspace has five issues with an estimate and measured time — which, in practice, means using the stopwatch or letting an agent run.

What this release does not do

  • Nothing is corrected automatically. No estimate is rewritten, and the plan is not changed — the calibrated figure sits beside it.
  • No per-project or per-issue-type factor. Both are plausible and both would divide an already-small sample.
  • Not on the API, and not in the CSV export.

Version 0.34.0

Date: 4 August 2026

A restore could look completely successful and leave every document blank. It cannot any more.

The failure

Félagi's state is in three places: MySQL, storage/knowledgebase (one HTML file per article and per revision) and storage/app/private (attachments).

A restore that copied the database and forgot the knowledge base looked entirely successful. Every row present, the article tree drawn with every title, search still finding things — and every document empty. The page even said "Nothing here yet. Write something.", which is how somebody starts retyping a document that still exists in a backup.

That silence came from a decision made deliberately: a missing article file reads as an empty document rather than throwing, because an article has a row before it has a file. The decision is still right. It needed a counterweight.

The signal

search_text — a plain-text copy of the prose, written at the same moment as the file. Text in that column means a file existed, so its absence is a fact rather than a possibility. An article with an empty search_text may simply never have been written, and is not accused of anything.

It also means the two cases are different, and felagi:check reports them separately:

Articles The words survive. Recoverable as plain text
Revisions and attachments No second copy exists anywhere. If they are not in a backup, they are gone

The page says so

An article whose file is missing now shows what happened, quotes what the search index still holds, and offers Recover the text.

The recovery is lossy and the confirmation says so: headings, lists, links and tables are gone, because the search index never held them. It is the difference between an empty page and a readable one, and not a substitute for restoring the file — which is exactly why it is behind a confirmation. Somebody who does it without reading will believe the article was restored.

Nothing recovers automatically. Silently replacing a document with a flattened version of itself would hide the fact that something was lost, which is the failure this whole path exists to stop.

felagi:check

Verifies every file the database refers to. Exits non-zero, so a restore script can stop instead of reporting success.

It also runs nightly at 01:00 with --quiet-when-clean, because nobody runs a check they have to remember — and it says nothing on success, because a nightly command that prints when all is well trains everybody to ignore its output. It writes to the log as well as the terminal: nothing watches a scheduled run, and a problem that only appears on a terminal somebody closed is a problem nobody has.

felagi:backup

Deliberately not a backup product. A tool that appears to take a complete backup and quietly takes most of one is worse than no tool, because the operator stops thinking about it and finds out during the restore.

It does the two things it can do honestly:

  • A manifest — row counts, file counts, byte totals. "The restore finished" and "the restore is complete" are different sentences, and a number is what separates them.
  • The file stores, archived relative to their parent (tar -C), since an archive of absolute paths restores onto the machine that took it and nowhere else. A failed archive is deleted rather than left, because half an archive looks like a whole one.

The database it dumps only when mysqldump is on the path, and says plainly when it is not rather than producing an archive with a hole in it. It did exactly that while I was testing it, which is how I know the warning works. The password travels in the environment, never the argument list where every process on the machine can read it.

An installation that is already missing files is warned about before anything is written. Backing up a broken state without saying so preserves the breakage and hides it in the same move.

docs/reference/backup.md

New, and the page that matters when something has already gone wrong: what the three stores are, what each one costs to skip, how to restore, and how to verify.

Upgrading

Nothing to migrate.

php artisan felagi:check          # after any restore
php artisan felagi:backup --manifest

What this release does not do

  • No scheduled backups. felagi:backup is a command, not a policy — retention and offsite copies belong to whatever already does that on your machines.
  • No restore command. Unpacking two archives and loading a dump is three lines an operator can read, and the one operation where a tool is least affordable to trust.
  • MySQL only for the dump.
  • No consistency snapshot across all three stores. An article saved during the backup lands in one and not the other — which the check will report.

Version 0.33.1

Date: 4 August 2026

An audit of the whole project: query cost, authorisation, injection, indexes. Three performance bugs and one authorisation gap, all fixed, plus three tests that find the next ones.

Three N+1 queries

Found by measuring the same pages against two data volumes. The number of queries a page makes matters far less than whether that number grows — thirty is fine forever, thirty that becomes three thousand when a workspace fills up is the bug that only appears for the customer with the most data, which is always the one who can least afford it.

Whiteboard author was the one relation on an element left out of the eager load, and toCanvas() reads it on every element. A forty-note board was 50 queries; a four-hundred-note board would have been 400. Now 7, whatever the board holds
Timesheets entries() ran its query afresh on every call — the grid, the totals and the header all ask — and days() built a new Capacity each time, which reads every member and every agent in the workspace. 42 queries to 19
Status report Project::progress() falls back to two count() calls when the counts are not loaded. Right on a single project's page, sixteen queries a row here. The report's cost followed the number of projects, which is the dimension a workspace actually grows in

The last one is the interesting shape: the model already offered withProgress(), and this one call site had not asked for it. Nothing was wrong with the model.

One authorisation gap

system/security — the IP allowlist, which decides who may skip two-factor authentication and is the widest privilege in the product — had no authorisation check in its actions. It relied entirely on the route's can:admin.

That was not exploitable: Livewire replays route middleware on an update, and a snapshot is checksummed so it cannot be forged or retargeted. But the boundary rested on Illuminate\Auth\Middleware\Authorize appearing in a list of class names inside a package we do not own, and one upgrade that reorganises that list would remove it with no error and no failing test.

authorize('admin') is in both actions now. The sibling page, system/identity, already did this — this one was the exception. The four workspace administration components got the same treatment, each against its own gate: administer-workspace for members and settings, administer-agents for skills and autopilots.

A test asserts Livewire is still doing its half.

What was already right

Worth recording, because an audit that only lists faults reads as though nothing was considered:

  • No raw SQL carries user input. Every whereRaw is bound with a placeholder or contains no variable at all.
  • No fill() or forceFill() takes unvalidated request data.
  • Indexes cover every hot filter, including the new tables.
  • The fuzzy fallback is bounded. 900 issues and 400 sticky notes, three extra queries, 16ms — affordable because it only runs on a search that found nothing.
  • Every route refuses an outsider. Swept: signed out, unverified, wrong role, another workspace's records. 404 rather than 403 throughout, so a probe cannot learn that an identifier exists.

Three tests that find the next one

  • QueryScalingTest runs the twelve heaviest pages against two data volumes and fails if any page's query count follows the data. It counts rather than times: a timing assertion on a shared CI runner fails for reasons that have nothing to do with the code.
  • AccessControlSweepTest enumerates routes rather than listing cases, because the failure it exists for is always a new route somebody forgot to guard.
  • ActionAuthorizationTest calls the methods directly. A page that refuses to render proves nothing about the methods on its component: a Livewire update is its own request, and this is where an authorisation mistake actually lives.

Not a finding

The health check reports 100 "code debt markers". Every one is the word Todo as an issue status. There are no TODO or FIXME comments in this codebase.


Version 0.33.0

Date: 4 August 2026

Three things: meetings reached the API, meetings can repeat, and a misspelled search finds something.

Meetings over the API

The action items from a meeting were already issues an agent could see. Until now it could see the work and not the sentence that produced it — and "rewrite the deploy runbook" is a different instruction once you know the room decided the current one caused an outage.

felagi GET /meetings                      # upcoming
felagi GET /meetings/01k9…                # agenda, minutes, action items
felagi POST /meetings -d '{"title": "Architecture review", "project": 4,
                           "scheduled_for": "2026-08-09T10:00:00Z"}'
felagi PATCH /meetings/01k9… -d '{"notes": "- Astrid: runbook by Friday"}'

A list is a calendar; agenda and minutes come only when one meeting was asked for. status is derived rather than stored, as everywhere else.

Two restraints:

  • Notes are only for the organiser or an administrator, matching the page. They are fragments typed in a room, and an API that ignored that would make the interface's restraint decorative.
  • No sending invitations and no circulating minutes. Both are irreversible broadcasts to everybody who was invited, and a token that could do it could send half-written minutes to the whole team. Scheduling is safe precisely because saving a meeting sends nothing.

Circulated minutes are the record: a PATCH that includes minutes answers 409.

Repeating meetings

Pick an interval and Félagi keeps the next few occurrences in existence, topped up daily. Generated, not extended — the cycles argument again: a series somebody has to remember to extend lapses in March, and nobody notices until the sync is not in a calendar.

The pattern is kept apart from the occurrences. Conflating them is what makes every calendar application confusing: edit one Tuesday and you are asked whether you meant all Tuesdays, because the Tuesday you clicked is both an event and a description of every event. Here a meeting is always one meeting — edit it, move it, cancel it, and the series carries on producing the ones after it.

  • Attendees are inherited from the newest occurrence, so adding somebody to next week adds them from then on. A list on the pattern would be a second copy of the truth, and the copy that drifts is the one nobody opens.
  • The agenda is not carried forward. A standing agenda that repeats is one nobody reads by the fourth week.
  • The next slot follows the last occurrence, not today, or a fortnightly series topped up on an off-week moves to the other week and stays there.
  • Weekly intervals only. "The last Friday of the month" and "the 31st" are two different intentions and February disagrees with both.

Stopping removes scheduled occurrences with nothing written on them and keeps anything with notes or circulated minutes — that is a record of a meeting that happened rather than a plan.

A typo finds something

Fuzzy matching, and only when the exact search found nothing.

That restriction is the whole design. Mixed into every query it makes precise searches worse — "deploy" starts returning "display", and somebody who typed the right word is reading results they did not ask for. As a second pass on zero results it cannot degrade a search that already worked.

Results appear under Did you mean, labelled, because a result that arrived from a misspelling has to say so or the next thing somebody does is act on it as an exact match.

Two edits at most: three starts matching different words, and offering "display" for "deploy" is worse than offering nothing because somebody will believe it. Four characters minimum, since below that a typo and a different word are the same thing. Compared word by word, so "rate limting" is one edit from "Operations and rate limiting" rather than twenty. Accents folded, which also means "mote" finds "møte" — what somebody without a Norwegian keyboard has actually typed.

Pages are matched first: a misspelled destination is the most common typo there is.

No index and no dependency. Affordable precisely because it only runs on a query that found nothing.

Three bugs, one of them mine twice over

  • A series generated nothing. active has a database default, so the model in memory had null and the generator refused to produce anything for a series it had just been handed. The same trap Project::$status fell into over the API; the schema's defaults are restated on the model now.
  • Stopping a series deleted the page you were on. The occurrence you clicked from is usually one of the empty future ones, and refresh() on a deleted model throws. It navigates to the list instead — sparing the one you happened to open would make the rule depend on where you clicked from.
  • A test failed one run in three, twice. It asserted that "agn" matched nothing, and faker's Latin filler contains "magnam". Pinning one model's prose only moved the problem to the next model's — the fix was to assert the property (no Did you mean group) rather than an empty list. A search test cannot use generated prose as its negative space, and a flake fixed by pinning data is usually a badly aimed assertion.

Upgrading

php artisan migrate
npm run build

felagi:meetings is a new scheduled command, daily at 00:15. Without it a recurring meeting runs out of occurrences a month ahead.

What this release does not do

  • No monthly recurrence, and no series over the API.
  • Fuzzy matching is titles and names only. Bodies are where the volume is.
  • LIKE '%term%' for the exact pass, still.

Version 0.32.0

Date: 4 August 2026

concurrently 9 → 10. A major in the toolchain, given its own release so a regression points at one thing.

The breaking change that matters is the Node floor

Four things broke in v10. Three do not touch this project:

Change Us
ESM-only Invoked as a CLI through npx, never required
Prefix colours default to automatic We pass explicit hex colours, still supported
--name-separator removed Already using commas in --names
Node <22 dropped This one.

Vite 8 already required ^20.19.0 || >=22.12.0, so the documented "Node 20+" was half wrong before this — 20.0 through 20.18 could not build the assets. v10 removes the 20.19 escape hatch and takes the floor to 22.

Node 20 reached end of life in April 2026. Documenting support for an out-of-support runtime is worse than raising the floor, so the floor is raised: 22 in the requirements table, 22 in the README, and engines.node in package.json so npm warns at install rather than letting a contributor discover it inside a dev script.

Worth being precise about who this affects: concurrently is only used by composer run dev. It is not in the build, not in CI's test path, and not in production. Nothing deployed cares.

Verified rather than assumed

The upgrade was checked against the flag combination Félagi actually uses — five processes, hex prefix colours, --names — and against the behaviour the dev script depends on: long-running children, and Ctrl-C leaving no orphans. That last one is what v10.0.4 fixed, and it is the only part of concurrently anybody notices when it goes wrong.

It also carries a fix for a shell-quote vulnerability, which is a reason to move rather than only a cost.

A guard that can fail

Three files state the Node version and none of them is the one npm reads. A test now compares the requirements table, the README and the CI workflow against package.json's engines.

The first version of that test asked str_contains($readme, '22'), which passes because a version string or a date contains it. A guard that cannot fail is worse than no guard, because it looks like the question was asked. It matches in context now, and was confirmed to fail by putting the wrong number back.

Upgrading

npm install

Contributors on Node 20 need to move to 22.


Version 0.31.0

Date: 4 August 2026

Whiteboards are searchable, and every article result now shows why it matched.

Finding a note on a surface with no edges

⌘K searches the words on every sticky note you can see, and board names besides.

A board is where a decision was argued before anybody wrote it down, which makes it often the only place a rejected option is recorded at all. That is the reason to search it, and it is also what makes it hard: the text lives on elements, not on the board.

The result opens the board with the viewport centred on the note. This is the difference between a search that works and one that appears to. A board has no edges, so landing on it without moving the viewport leaves somebody looking at an empty grid four thousand pixels from the thing they searched for.

Two decisions inside that:

  • The zoom is left alone. Somebody who searched wants the note in the context it was drawn in, and a jump to 100% throws away the scale the board was built at.
  • The note is ringed and pulses for two seconds, because centring is not finding: on a board with forty yellow notes, the one in the middle looks like the six around it.

The focus parameter is not kept in history — it describes an arrival, not a state, and leaving it there would make the back button re-centre a board somebody has since panned somewhere else on purpose.

Board names are listed separately from note text: somebody looking for "Retro" wants the board, not whichever note mentions the word. One board cannot fill the list. A private board is not searched by anybody but its author, because hearing about a surface is reading it and a search result is hearing about it.

The passage, not the preview

Article results showed the opening of the document. That is a preview, not a reason — and a preview of an article that matched on page four says nothing about why it is in the list. Five plausible titles meant five documents somebody opened.

Every result now carries the passage around the match, with the term marked, in the palette and on the knowledge page.

Three things the window gets right and a naive one does not:

  • It is anchored on the match, not the start of the document.
  • It keeps the document's own casing. Echoing the search term back makes the snippet look like something the search wrote rather than something the document says.
  • It counts characters, not bytes. One æ before the match and a stripos-based window opens in the middle of a character.

Import and export

Removed from the "not there yet" lists as pending, and stated as a decision. Neither whiteboards nor the knowledge base will be in the import or the export.

Upgrading

npm run build

No migration.

What this release does not do

  • No fuzzy matching, still. A typo finds nothing.
  • LIKE '%term%', still — it cannot use an index, and a full-text one would be MySQL-only, which would make the suite and production disagree.
  • No search inside a meeting's notes or minutes.
  • No search on the API.

Version 0.30.1

Date: 4 August 2026

Wording, and a test that holds it. No behaviour change.

The front page made a promise the code no longer keeps

Until 0.30.0 the claim was true as written: "Félagi never calls a model." Minute drafting made it false, and it was still on the page in three places — a shield-check callout, a FAQ answer, and the README.

It is narrowed rather than footnoted:

Your code never leaves your machine. Félagi never calls a model to do your work. Your agent CLI does, from your own runtime, with your own keys — the server has no model integration in that path at all. One optional feature is the exception: drafting meeting minutes from notes, off unless an administrator turns it on.

An absolute claim with one exception is worse than a narrower claim with none. The exception is what somebody finds, and then they stop believing the rest of the page. The part worth having — code, credentials, keys, the execution path — is still absolute, and that is the part anybody evaluating this actually cares about.

docs/development/architecture.md gained a table comparing agent work with drafting across repository, execution, credentials, input and default, plus a sentence that matters more than the table: if a second exception is ever proposed, that table is the test it has to pass. The moment something on the execution path calls a model from the server, this is a different architecture and the front page needs a much shorter sentence.

The FAQ had its own copy

"It never calls an LLM itself" survived two edits to the callout directly above it, because prose duplicated across a page is prose that goes stale one copy at a time.

The test now checks every absolute form of the claim rather than the one that was wrong most recently. That sentence is the kind that drifts back — it is shorter, it reads better, and whoever edits the page next will not be thinking about a feature that is off by default.


Version 0.30.0

Date: 4 August 2026

Meetings, per project: an agenda before, notes during, minutes after — and the action items become issues on the project's board.

That last part is why this belongs in a tracker rather than beside one. A decision that produced work produces a row somebody is accountable for, and it appears in the workload report, on the Gantt and in a timesheet without meetings knowing any of those exist.

Off by default. Administration → Workspace → Features turns it on per workspace, and FELAGI_MEETINGS=false removes it from the installation entirely — a per-team switch that could override an installation-wide one would not be a kill switch.

The invitation is a calendar entry

An .ics attachment, which is what makes it an invitation rather than an email about a meeting. Without one every recipient copies a time into their own calendar by hand and one of them gets it wrong.

Two details decide whether it works, and both are easy to get wrong:

  • A stable identifier, from the ULID rather than the row id, so an installation restored into a new database keeps its calendar entries instead of issuing duplicates of every meeting.
  • A rising sequence number, which is what makes a second email an update to the entry already in somebody's calendar rather than a second entry beside it. A duplicate is how somebody ends up in the wrong room.

Cancelling sends METHOD:CANCEL, so the entry disappears rather than sitting there looking valid. Nobody is re-invited once they have replied: an organiser adding a seventh person a week later must not reset the six replies they already have.

Written by hand rather than with a package. iCalendar is a text format with about nine lines that matter — though the folding rule counts octets, so a Norwegian meeting title will corrupt the file if you split it by characters.

Status nobody has to set

Scheduled, awaiting minutes, minuted, canceled — all from the clock except the last two. A status somebody has to remember to change is a status that lies within a week.

Awaiting minutes is the list that makes the feature worth having. A meeting whose minutes were never written is invisible in a calendar; it shows up here and nowhere else.

Notes are kept after the minutes exist

Two fields on purpose. Notes are fragments typed in the room; minutes are the version other people read. A summary whose source has been overwritten is an assertion nobody can check.

Minutes circulate once, to everybody who was invited, in their inbox and by email. Minutes that quietly change after people have read them are worse than late ones, so the timestamp both records the event and prevents the second one.

Drafting minutes — and the one exception in this product

Optional, off unless an operator turns it on, and the only place where the Félagi server calls a model.

Everything an agent does still happens in a CLI on hardware you control, and that does not change. The exception is worth stating rather than hiding:

  • It is not agent work: one text transformation on text somebody just typed, with no checkout, no execution and no credentials involved.
  • Running it through the task queue would mean minutes only work for a team that has a daemon, a runtime and a registered agent. Meetings are a human activity — a team using Félagi as a tracker with no agents should be able to write minutes.
  • It is off unless a provider is named. It has no default on purpose: a sensible fallback would make the outbound call the thing that happens when nobody decided anything.
FELAGI_MINUTES_PROVIDER=anthropic
FELAGI_MINUTES_MODEL=claude-haiku-4-5-20251001

What is sent: the notes, the agenda, the title and the attendee names. That is in the guide rather than a footnote, because meeting notes are often more sensitive than source code.

Structured output rather than a summary. Asked for a summary, a model returns a paragraph that reads well and buries the two sentences somebody needed: what was decided, and who is doing what by when. Asked for a schema, it has to separate them — and a separated action item can become a real issue, which a paragraph cannot.

The instructions do the opposite of what a summariser usually does: they forbid invention. A plausible sentence in a record is worse than a gap, because a gap is visible. The draft leads with what the model could not work out, which is the field that makes the rest trustworthy — without somewhere to put an ambiguity, a model resolves it silently and the minutes contain a decision nobody made.

Nothing is saved until you accept it. Owners are matched by name against the people who were in the room, on the full name and then the first name, and never invented: an unmatched owner leaves an unassigned issue, which somebody fixes in a second.

Three bugs found on the way

  • <x-rich-text :value="…"> renders nothing. The prop is html; an unknown one lands in $attributes and the component's blank guard draws an empty div. No error, no warning, and an agenda that shipped invisible under its own heading. A test now checks every use.
  • The command palette offered a page that answers 404 when the feature is off. Somebody who found it once will look again and conclude the search is broken rather than the feature off.
  • The feature switch did not appear to work. currentWorkspace() is memoised per request and the sidebar lives in the layout, which a Livewire round trip does not re-render — so turning meetings on left the navigation unchanged until a reload. The switch navigates now.

Upgrading

php artisan migrate
npm run build

What this release does not do

  • No recurrence. A weekly sync is scheduled one at a time.
  • No inbound RSVP. Replies are recorded in Félagi; replying to the .ics from a mail client does not reach it.
  • No external attendees. Inviting an address Félagi cannot show a page to would be an email pretending to be a feature.
  • Not in the API, the import or the export.
  • Agents do not attend. The attendee column is user_id, not a morph — a polymorphic column where one of the two values can never appear is worse than a plain one.

Version 0.29.1

Date: 4 August 2026

Documentation only. No schema change, no behaviour change.

SQLite is a test target, and every page now says so

Six pages mentioned SQLite. None of them was wrong, and together they were misleading: "SQLite works for tests, but not for concurrency behaviour" is true, and to somebody scanning a requirements table it reads as "SQLite works".

It does not. The task queue is a database table and a worker claims a row with SELECT … FOR UPDATE SKIP LOCKED. SQLite has no such clause and no row-level locking to build one from, so two daemons claim the same task and run the same agent twice against the same repository — no error, no log line, the work simply done twice. The first anybody would know is a duplicate pull request.

That is the most expensive thing this documentation could imply, so it is now stated where each decision gets made: the requirements table, the database section, the README's testing block, the reports guide's note about testing both engines, and .env.example itself, which is the file people copy.

A test holds the line: every file that mentions SQLite must also say which side of it the engine is on. It caught a release-notes page that named both CI lanes without saying either was for tests.

And a correction

The commit message for 0.29.0 said a date-cast bug "would have crashed on every installation not running MySQL". There are no such installations, and Félagi's own requirements page said so at the time. The corrected account is in that release's notes — the bug was real, the claim about its reach was not.


Version 0.29.0

Date: 4 August 2026

A burndown and a velocity, and one of them is built differently from every other one you have used.

The burndown cannot rewrite its own history

Most burndowns are drawn from the issues as they stand right now, which means today's estimates are applied to every past day. Re-estimate one issue from two hours to twenty on a Thursday and the chart moves Monday's point, showing a team that was further behind at the start of the week than anybody thought at the time. Every tracker does this quietly, and it is why nobody trusts the chart.

Félagi records a reading each morning and never recalculates it.

The consequences are honest rather than convenient, and both are visible:

  • A window that ran before you switched cycles on has no line, and the page says so instead of drawing a plausible one.
  • A day the scheduler missed is a gap, and the line breaks either side of it. Two readings a week apart are two observations, not a trend through the middle.

Three lines: remaining, scope, and the ideal. The scope line is there because a cycle that grew mid-window is the usual reason a burndown never reaches the floor, and a chart that cannot show scope moving blames the team for it. When it has grown, the page says by how much.

Weekends are left out, for the same reason capacity leaves them out. Today's point is computed when you open the page — a chart eleven hours stale is one you check against the issue list and stop believing.

Velocity, with its spread

From closed windows only. A cycle halfway through has a partial number that looks like a bad one on a Tuesday.

Counts and hours, because they disagree in a way worth seeing: a team that finished twelve issues instead of fifteen but the same number of hours took on bigger work, not less of it.

The range is shown next to the average, because "14 on average" out of 4, 22 and 16 is a different promise from the same average out of 13, 15 and 14 — only the second team can plan. Range rather than a standard deviation: nobody in a planning meeting has an intuition for a sigma.

Under three closed cycles the page says so rather than presenting a confident average over two windows. Each bar shows what a window finished and what was carried out of it, with the current plan beside them.

Three colours that never existed

Writing the chart turned up text-signal-amber, and no such token is defined. Tailwind cannot warn about that: it produces no rule, the element inherits whatever colour its parent had, and the result looks deliberate.

A test now checks every colour class in every template against the palette the stylesheet actually declares. It found three more that had already shipped:

  • bg-signal-purple — the workload and time reports use it to separate an agent's bar from a person's. Agent bars have been invisible. That distinction is the whole argument of both pages.
  • text-ink-faint, in twelve files, a fourth name for a grey that already had three. Replaced with the existing scale rather than defined, because two names for one intent are two things that can drift.
  • bg-paper-sunken, one inset panel.

One bug, and a claim about it that was wrong

The daily reading has to be idempotent, so it upserts on [cycle_id, on]. A date cast serialises as 2026-08-03 00:00:00 while the lookup asked for 2026-08-03.

The commit message for this release said that would have "crashed on every installation not running MySQL". That is wrong, and Félagi's own documentation says so: MySQL 8 is required, because task claiming depends on SELECT … FOR UPDATE SKIP LOCKED. There are no SQLite installations to crash.

What actually happened is narrower and still worth having caught. The code relied on MySQL's type coercion without saying so: that write and that read only agree because MySQL is lenient inside a DATE column. The suite runs on SQLite in memory, which is not, so it surfaced within a minute of being written. The cast is pinned (date:Y-m-d) rather than the test taught to accept both — "it works because this database is forgiving here" is worth removing even when only one database is supported, because leniency is a setting.

Upgrading

php artisan migrate
npm run build

Readings begin from the next run of felagi:cycles. The current window will show "not enough readings yet" for a day, which is the correct answer.

What this release does not do

  • No burndown for a window that has already run. It cannot be reconstructed without inventing it.
  • Velocity is an average and a range, not a forecast. Nothing extrapolates a finish date, because a range wide enough to be honest is too wide to schedule against.
  • Neither is in the API or the CSV export.

Version 0.28.0

Date: 3 August 2026

Three gaps, and two of them were places Félagi could lose your work.

Articles keep every version

Every save that changed the words keeps what was there before — before the overwrite, never after, because a snapshot taken afterwards is a snapshot of the thing that destroyed what somebody wanted back.

A versions button beside Edit opens the list. Picking one shows a line-by-line diff against the page as it stands; Restore puts it back.

Restoring is itself an edit, so the current text is kept too — a restore done by mistake is as recoverable as the edit that made somebody want one. A rename counts as an edit as well, and the history records the title a version was written under, or it would send somebody looking for a document by a name that never existed.

A save that only moved an article to a different parent adds nothing: the one edit you are looking for should not be buried under fifty copies of the same text. The most recent fifty versions are kept.

The diff is plain text. Comparing two trees of markup means deciding whether a paragraph that gained a bold word is one change or three, and nobody recovering from a bad paste is asking about emphasis — they are asking which sentences went.

The whiteboard has undo

⌘Z, ⇧⌘Z, and buttons beside the zoom controls, because a keyboard shortcut nobody knows about is not an undo and the person who has just deleted the wrong note is the least likely to go looking for one.

Deletes, creates, moves and resizes. A deleted note comes back with its text, its colour and its position — with a new identifier, since the row is gone and inventing the old one would claim an identity the database no longer knows.

Inverses rather than snapshots of the surface: a snapshot would undo somebody else's work along with yours, on a board where two people are drawing at once.

The stack lives in the browser and lasts the session. That is a real limit, and the failure it exists for — a misclick noticed in seconds — is the one it covers.

The two newest features reached the API

They were also the two a developer could not touch from the tool on their desk.

felagi GET '/articles?q=deploy'
felagi GET /articles/01k9…            # one article, with its body
felagi POST /articles -d '{"title": "Runbook", "body": "<p>…</p>"}'

felagi GET /whiteboards/01k9…         # the board, with everything on it
felagi POST /whiteboards/01k9…/notes -d '{"text": "Rate limiting came up again"}'

The article endpoint is the one worth having for an agent. Skills are what an agent is given; the knowledge base is what it can look up, and an agent that reads the team's runbook before it starts is one that follows the team's conventions.

A list of articles is a table of contents — titles, depth and an excerpt, no bodies. A board list is a list of names. Both send the heavy part only when a single one was asked for, the same reason an article's prose is a file rather than a column.

Notes and boxes can be added to a board; arrows and images cannot, because an arrow needs two points a caller cannot see and an image needs an upload this API does not do. Accepting either and ignoring it would be a field that lies.

Drafts and private boards are invisible here as everywhere else. Half a page found through an API is worse than not finding it, because something will act on it without a person reading it first.

Upgrading

php artisan migrate
npm run build

What this release does not do

  • No branching or drafts of a published page. History is linear.
  • Undo does not survive a reload.
  • No moving, resizing or drawing on a board over the API, and no article history over the API.
  • Whiteboards are still not searched, and neither is in the import or export.

Version 0.27.1

Date: 3 August 2026

Dependencies, on their own, so that a regression points at an upgrade rather than at a feature.

Seven packages moved within the constraints already in composer.json, which is untouched — nothing was added, removed or loosened:

livewire/livewire v4.3.3 → v4.3.5
livewire/blaze v1.0.13 → v1.0.14
league/commonmark 2.8.3 → 2.9.0
laravel/pint v1.30.0 → v1.30.3
pestphp/pest v5.0.2 → v5.0.3
sebastian/cli-parser 5.0.0 → 5.0.1
sebastian/recursion-context 8.0.0 → 8.0.1

All patch releases except CommonMark, and composer audit reports no advisories.

Livewire and Pint are the two that could have been felt. The newer Pint asked for no reformatting, and every page was opened in a browser rather than only in the test suite — Alpine and Livewire both present, a live filter round trip working, no console errors. The suite alone would not have noticed a broken front end.

Why this is its own release

The lockfile had drifted locally without being committed, which meant CI was testing one set of versions and the local suite another — precisely the divergence a lockfile exists to prevent. It was reverted rather than swept into a feature commit, and updated deliberately here.

Nothing in Félagi behaves differently.


Version 0.27.0

Date: 3 August 2026

New: a guide for working from your editor

Working from your editor — setting up an IDE or a coding agent against Félagi, end to end, with the commands to paste.

A developer already has an editor open. Switching to a browser to find out what an issue says, and switching back at the end of the day to remember how long it took, is the friction that makes time tracking stop happening after a fortnight.

The guide covers making a token, a shell function worth keeping, reading and narrowing issues, opening projects and issues, logging time the way you would say it, the stopwatch, running an agent, every error code and what it means, and a worked morning from "what am I meant to be doing" to "in review".

The reference page is still the specification; this is the one you read once.

Projects can be created over the API

POST /projects and PATCH /projects/{id}.

They were read-only under the same sentence that keeps agents and skills read-only — "getting that wrong over an API is harder to see than getting it wrong in a form". That argument is about agents: creating one means naming a runtime and a provider that have to exist and match. A project is a name and a description, and getting it wrong is visible from the first page that lists it. The reasoning never applied, so the restriction has gone.

lead takes an actor key — user:3 or agent:1 — and is resolved through your workspace rather than trusted. A PATCH leaves out what it does not mention.

Two tests that keep a guide from lying

A worked example is read as a promise, and one aimed at a route that has been renamed fails for somebody at their desk rather than here. So:

  • every curl example in the guide is matched against the real route table, wildcards and all
  • every write endpoint has an example, because one nobody demonstrated is one nobody uses

Both were checked by breaking them on purpose.

What this release does not do

  • Still no agents, runtimes or skills over the API. That restriction has a reason and it still holds.
  • No labels or cycles written over the API, and no file uploads.
  • No OpenAPI document. The reference is the specification.

Version 0.26.1

Date: 3 August 2026

Documentation, and the tests that keep it from drifting again.

A page that should always have existed

Running it — the scheduler, the queue worker and Reverb, and what stops without each.

Nothing described the scheduler. Four commands depend on it, and installing Félagi without one produces this: autopilots never fire, cycles never arrive, abandoned stopwatches are never closed, delivery logs are never pruned. Nothing throws. No page breaks, no log fills up. A feature simply stops happening, and the first person to notice is the one wondering why their daily triage never ran.

The queue worker's five lanes are on the same page, with what each one carries and what silently stops without it.

The stale claim, again

timesheets.md still said hours were not available over the API. They have been since v0.24.0 — the line was in the same script that died before writing the version bump and the release notes for that release. Three casualties from one error, and I had only found two.

Six tests that hold the mechanical half

Prose fails by being right on the day it was written. Nothing breaks, no test goes red. So the part a machine can check now is checked:

  • every felagi:* command appears somewhere in the documentation
  • every scheduled command is on the operations page
  • every queue lane in composer.json is on the operations page
  • no document is orphaned from the index — releasing.md was
  • no FELAGI_* in the reference is missing from .env.example — seven were
  • and a guard against the whole file passing by finding nothing

The seven missing variables are now in .env.example, commented, where somebody copying it will find them.

What no test can hold is whether a sentence is still true, so the release checklist now names the three things that must be done by hand — a guide, a concept entry, and a pass over the "Not there yet" sections, which are where staleness lives because they describe absences that later releases fill.

Also

The front page mentions the stopwatch. concepts.md gained a Timer entry, the fourth noun added late.


Version 0.26.0

Date: 3 August 2026

Measured, or remembered

Every hour in Félagi now says how it came to be written down, and the reports say what proportion of a figure was watched happening.

A stopwatch knows when it started. A daemon knows when a run began and ended. Somebody typing "about three hours" into a form on a Friday afternoon does not — and a total that adds the three together without saying so is a guess wearing a number's clothes.

Measured   13%
1h 35m observed · 1d 2h 32m recalled

A new source, timer. Until now the stopwatch wrote its hours as manual, so the one distinction worth having was thrown away at the moment it was created. It is its own provenance now.

A new filter on the time report: Measured how. Offered only there, because a source belongs to an hour and an issue does not have one — a control that is displayed and then ignored is worse than one that is absent. ?source= on GET /time-entries does the same over the API.

The trap in adding a third value to an enum

Two places asked source === Manual to decide whether a person may correct an entry. Left alone, timed hours would have become uneditable — the one kind somebody is most likely to need to fix, because the clock was left running through lunch.

Both now ask the enum isCorrectable(), which means "not a machine". An agent's duration is still beyond correction, because the run is the record.

One elapsed duration, one number

A finished agent run rounded its duration up to the next minute while the stopwatch rounded to the nearest, so 7m01s of work was eight minutes if a machine did it and seven if a person timed it. Both now use the same rule: nearest, with a floor of one.

Rounding up flatters every figure by as much as fifty-nine seconds, and a release whose whole point is saying which half of a total was measured had better have an accurate measured half. A run will occasionally report a minute less than it used to.

This surfaced as a test that passed on its own and failed in a full suite — the worst kind, because it looks like whatever ran before it. It was reading the real clock between arranging a seven-minute run and completing it, so a slow suite made it seven minutes and change. The clock is frozen now.

Upgrading

No migration. Existing entries keep the source they were written with, so hours the timer recorded before this release still read as manual — they were not reclassified, because guessing which of them came from a stopwatch would be inventing data.

Two releases that shipped without notes

v0.24.0 and v0.25.0 were tagged with no entry here, no version bump and no README change. The script that writes all three died on an unrelated error before reaching them, and the test that pins the configured version to the newest heading saw nothing wrong — because both files are written by that same script, so when it dies they stay consistent at the old number and agree with each other.

Their notes are above, written after the fact. And there is a new test that compares the release notes against git tags, which are the one thing that script does not write.


Version 0.25.0

Date: 3 August 2026

New: a timer on issues

A stopwatch beside the spent figure on every issue. One button, because the state is unambiguous: either the clock is running on this issue or it is not.

It runs on the server, not in the tab. A timer that only exists in a browser dies with the browser — closed laptop, crashed tab, accidental reload — and takes the work it was measuring with it. The page counts elapsed seconds itself from the start time, so it survives a reload and costs no requests.

The clock sits in the sidebar on every page. A timer you cannot see is a timer you forget, and one forgotten fourteen-hour entry is enough for nobody to believe the rest of the report.

Three rules about the arithmetic

One running timer per person, enforced by a unique index rather than a check in code. Nobody works on two things at once, and a race between two tabs should be settled by the database. Starting a second stops the first, logs it, and tells you what it logged — switching tasks is normal; banking time against something else silently is not.

Rounded to the nearest minute, with a floor of one. Forty seconds is still work somebody did, and rounding it to zero would throw it away quietly.

Capped at eight hours. A clock that ran for fourteen hours did not measure fourteen hours of work; somebody went home. Recording that number puts something indefensible into a report, and discarding it loses an afternoon that really happened — so the cap is logged, and the note says it was capped. The chip turns red and says so while it is still running, rather than leaving it to be discovered in a report.

felagi:timers runs hourly and closes anything left open. Hourly rather than nightly: a clock left running at four in the afternoon should be closed the same day.

Throwing a timer away records nothing at all, which is the right answer when it was started by accident.

Over the wire

POST /issues/{id}/timer starts one, POST /timer/stop stops it, GET /timer says what is running, DELETE /timer throws it away. An editor that starts a clock when you open a file is the whole reason to want a personal token.

GET /timer answers 200 with null when nothing is running. "Nothing is running" is an answer, and a client made to treat it as an error will treat a real failure the same way.

Upgrading

php artisan migrate

Add felagi:timers to your scheduler if you are not running php artisan schedule:work. Nothing breaks without it; abandoned clocks simply stay open until somebody stops them, and are still capped when they do.

What this release does not do

  • No approval, no locked periods, no billing. A timesheet here is a record rather than a workflow, and that is a decision rather than a gap.
  • No timer on a board card, and none in the command palette.
  • No idle detection. Félagi does not watch your keyboard.

Version 0.24.0

Date: 3 August 2026

Hours over the API

POST /issues/{id}/time logs an afternoon. GET /time-entries reads them back. DELETE /time-entries/{timeEntry} takes one away.

The point is the tool already open on a developer's desk. A personal token has been issuable from Settings by anybody who works here since v0.13 — it was never gated to admins — but until now it could read issues and not record the work somebody did on them, which is the half of the loop that happens every day.

Durations read the way people say them. 2h 30m, 1d, 90m. minutes is accepted too for a tool that has already done the arithmetic, but requiring it would be a tool somebody stops using on the third day.

The hours are always the token's own. There is no actor field, and sending one does nothing: a credential that can log time against a colleague is a credential that can rewrite their timesheet, and hours are somebody's statement about their own week.

Removing follows the same two rules as the interface. An agent run's duration cannot be taken away — the run is in the timeline with its own duration, and editing the entry would let the report disagree with the history it came from. Nor can anybody else's hours.

The env block, not just the secret

Creating a token now hands back the three lines it is going to be pasted into:

FELAGI_URL=https://felagi.example.com
FELAGI_TOKEN=fat_…
FELAGI_WORKSPACE=Acme Industries

Handing over a secret and leaving somebody to guess the variable names is half a feature.

On passwords

Félagi will not store an account password for an integration to use, and this is a decision rather than an omission. A token already is the password, and a better one: scoped to one workspace, optionally read-only, optionally expiring, and revocable without touching anything else you own. An account password unlocks everything — including the ability to change the password — which is exactly what should not be sitting in a file on a laptop.

What this release does not do

  • You can read the whole workspace's hours and write only your own.
  • No timer, no start and stop; a duration is sent when the work is done.
  • No editing an entry over the API. Remove it and log it again.
  • Still no write access to projects, agents or skills.

Version 0.23.1

Date: 1 August 2026

Three features shipped between two audits of the front page and none of them was on it. This is the catch-up, and the same audit run over the documentation.

The unstructured half now has a section

Timesheets, the knowledge base and whiteboards were all missing. The first belonged in the tracker grid beside time tracking; the other two did not, and adding them as cards would have buried the interesting part.

They have their own section now — "Not everything starts as a ticket" — because the claim is different. Everything else on that page is about work that has been decided. This is about the hours before it was, and the sentence that matters is that one feeds the other: a sticky note becomes a real issue, and the note stays on the board pointing at what it became.

A page you cannot reach from ⌘K is a page half the team never finds

Cycles, Timesheets, Knowledge and Whiteboard were all missing from the command palette. They are in it now, and knowledge base articles are searchable — by title and by the words inside the document, through the plain-text copy kept beside the row.

Somebody else's draft never appears. Finding half a page through search is worse than not finding it, because whoever found it will act on it.

Documentation

Four reference pages had gone quiet rather than wrong:

  • Permissions never mentioned write-knowledge, the widest gate in the product, or the rules that live on records rather than in gates — who may delete an article, who may change who sees a board, who may edit an agent's hours (nobody).
  • Broadcasting never mentioned the whiteboard's presence channel, the one event that is sent immediately rather than queued, or the thing that is never broadcast at all: cursors, which are whispered between browsers.
  • Configuration never mentioned FELAGI_KNOWLEDGEBASE_PATH.
  • Search listed pages that no longer match the sidebar.

Concepts gained the five nouns it was missing: Timesheet, Article, Whiteboard, Label and Attachment. Getting started now says what the rest of the sidebar is for, and that a whiteboard needs Reverb to be shared.

Five new tests hold the page against the schema, the routes and the method that actually converts a note.

What this release does not do

Nothing changed in how anything works, apart from four pages and articles becoming findable from ⌘K.


Version 0.23.0

Date: 1 August 2026

New: whiteboards

An endless surface with nothing on it until you put something there. Whiteboard, in the sidebar.

Sticky notes, boxes, text, arrows, freehand drawing and images, anywhere on a plane with no edges. Pan, zoom, and seven note colours.

Everywhere else in Félagi a piece of work needs a title, a type and somewhere to live. That is the right shape for work that has been decided and the wrong shape for the two hours before it was — and a tool that insists on a form at that point does not get used. The team photographs a real whiteboard instead, and the photograph is where the thinking goes to die.

The wire back to the data model

Which is the whole reason this is here rather than in another tab.

A note becomes a real issue. First line is the title, the rest is the description, and the note stays on the board pointing at what it became — the shape of a board is the record of a conversation, and deleting a note because it graduated would take the argument with it.

An issue that already exists can be dropped on as a card, showing its live status and assignee. Read on every load, pushed on every change: a board left open overnight is not lying by morning.

Everybody at once

Cursors, labelled, in real time — whispered browser to browser and never sent to the server. Sixty positions a second per person is not worth a request, a queue job or a row; it is worth exactly as much as the moment it describes.

Changes to things do go through the server, because those are facts, and they are sent immediately rather than queued. Everything else here queues its broadcasts, which is right for news — an inbox item can arrive a second late. A whiteboard is not news; it is a shared object two people are touching, and a stopped queue would leave cursors moving while nothing anybody did ever arrived.

Nothing is applied to an element you are holding. A note that changed under your hand waits until you let go, rather than jumping backwards mid-drag.

Private boards

Shared with the workspace by default, because the argument for having a whiteboard is that other people are standing at it.

Only me makes it a notebook — the sketch before it is worth showing. Nobody else sees it in the list, opens it from a link, or can subscribe to it: hearing a surface is reading it. Only whoever made a board can change who sees it.

Voting

One vote per person on a sticky note, stored as who rather than as a number so it can be taken back. A retro where somebody's fifth vote was an accident is a retro with the wrong answer at the top.


Upgrading

php artisan migrate
npm run build

Real-time needs Reverb running (php artisan reverb:start), which composer dev already starts. Without it the board still works; it simply stops being shared.

What this release does not do

  • No undo, no multi-select, and no copy or paste.
  • Arrows do not follow. A line between two points stays where it was drawn when the notes at either end move.
  • No frames, sections or templates. No retro layout, no lanes.
  • No comments on an element, and no history of who moved what.
  • Not in the API, search, import or export.
  • No catch-up after a dropped connection. Reload.

Version 0.22.1

Date: 1 August 2026

Toasts had nowhere to land

Flux::toast() dispatches a browser event, and <flux:toast /> is what listens for it. That element was in none of the four layouts, so both calls in the codebase had been composing a message and throwing it away since the day they were written.

Nothing broke. No exception, no failing test, no error in a log — you simply never heard anything. That is the failure mode of feedback code, and it is why saving an article and saving a timesheet entry both looked like nothing had happened.

Four tests now hold it: every layout that closes a </body> must contain a toast outlet, the outlet must sit inside the body and before the scripts, and the test fails rather than passes if it finds no layouts at all.

One way to say what just happened

Two pages had grown their own version while toasts were unavailable — a session()->flash read by a callout further down the template. A callout that appears after an action and then sits there until the next full render is a worse toast, and a second mechanism is a second thing to keep in step.

The Gantt's levelling and scenario messages, and the autopilot run result, are all toasts now. Flashes that cross a redirect are a different case and stay: the message has to survive a new request, which a dispatched browser event cannot.

A test refuses any new in-component flash.


Version 0.22.0

Date: 1 August 2026

New: knowledge base

A tree of articles, one per workspace. Knowledge, in the sidebar.

A tree on the left, a document in the middle, its headings on the right. Nesting goes six levels deep — unlike an epic, which is one level on purpose. A plan with grandchildren stops being readable; documentation is the opposite, because the nesting is the organisation.

Only open branches are drawn, so a base of several hundred pages stays usable. And following a link to a page four levels down opens the branches above it — otherwise you see the article and not where it sits, which is most of what a tree is for.

The documents are files

storage/knowledgebase, one HTML file per article. The database row is metadata: title, parent, position.

A tree of four hundred articles is drawn on every page load and none of it needs the prose. In a column, every sidebar render would drag megabytes of text through the database to list some titles.

More than performance, though: the documents are legible without us. Somebody with shell access can grep them, back them up with rsync, or read one without a database client — which is worth something for the page a team writes about how to restore the database.

A filesystem is not transactional, so the code does not need one to be. Writes go through a temporary file and a rename, so a reader arriving mid-write gets the old document rather than half the new one. A missing file reads as empty rather than throwing: a restored database should render a page somebody can fix, not a 500.

Point FELAGI_KNOWLEDGEBASE_PATH somewhere else if more than one machine serves the application.

Drafts, contents, attachments

A new article starts unpublished, under Drafts at the top of the tree, visible only to whoever is writing it. Half a page is worse than no page.

The contents list on the right is derived from the document every time it is drawn, and the anchors inside the article are written by the same function. A contents list that can disagree with the article is worse than none.

Attachments work as they do on an issue: ten at a time, 25 MB each, served by the application rather than from a public folder.

Anybody who works here can write. Deliberately the widest gate in the product — a knowledge base only admins may write to is a knowledge base nobody writes to. Deleting is narrower, because a section takes its children with it.

Tables now survive being pasted

No toolbar in Flux can produce a table, so they arrive by paste — out of a wiki, a spreadsheet, or the tool a team is migrating from. The most useful page in a knowledge base is usually a table of hostnames and addresses, and flattening one into a run of text loses the only thing that made it readable.

Widened in the one shared sanitiser rather than a second one for articles, so tables now also survive in comments and issue descriptions.

Confirmations look like Félagi now

Every "are you sure" was the browser's native dialog: a grey box titled "felagi.test says", in the system font, with the buttons in the operating system's order rather than ours. It was the one piece of this product a designer never touched, and it appeared at exactly the moment somebody was about to destroy something.

All twenty-four are Flux modals, from one component. The consequence now sits in the question — "Delete this article?" above "Everything filed under it goes too" — rather than being crammed into a single line.


Upgrading

php artisan migrate

What this release does not do

  • No version history. The file is overwritten; there is no diff and no way back to yesterday's wording.
  • No drag to reorder. A new article goes last; moving one means picking a different parent.
  • No links between articles, so nothing warns you before a delete, and no way to attach an article to an issue.
  • Not in the API, and not in the import.
  • Nothing an agent reads. Skills are what an agent is given; an article is not injected into a run.

Version 0.21.1

Date: 1 August 2026

A test, not the product. One of the new report-filter tests asserted that a total was the integer 60; MySQL returns SUM() as a string and SQLite as an integer, so it passed here and failed on the release build.

Nothing in Félagi behaved differently on the two engines — only the test did. It is fixed, and worth recording because it is the second time the MySQL job in CI has caught something that every local run said was fine. That job exists for exactly this.

The tag was already published, so this is a new version rather than a moved one: two different trees under one tag is worse than a line in a changelog.


Version 0.21.0

Date: 1 August 2026

Labels and cycles arrived in the last two releases and then stayed where they were born — visible on an issue and nowhere else. A label you cannot filter by is decoration. This release takes both everywhere they should have gone.

Filter the board and the list

By label, by cycle, and by not in a cycle at all — which turns out to be the most useful question that filter answers, and one you cannot ask by picking a window.

Two labels mean the overlap, not either. Every other filter in Félagi is a single-valued column where a second selection can only mean "or". A label is not: an issue carries several at once, so bug and regression asks for the issues carrying both. Widening the result on the second click is the opposite of what a filter is for.

Filter every report

Reports differ in what they aggregate, never in what they can be narrowed by — so these landed in all five at once, and in the CSV export with them.

A saved view from before this release opens unnarrowed rather than keeping whatever happened to be on screen. A bookmark that means something different each time it is opened is worse than one that has to be set up again.

Read them over the API

An issue now carries labels, cycle and carried_over.

Labels come back as words, not ids — a label is the string, and an identifier for it would be one more thing a client has to resolve before it can do anything. ?label=bug&label=regression narrows with AND, the same as the interface. ?cycle=12 takes the number a person says out loud rather than a database key, and ?cycle=none returns everything nobody has planned.

Import them

A Labels or Tags column is picked up from Jira, Linear, YouTrack and Monday. Each exporter packs several into one cell with its own separator, so commas, semicolons and pipes are all split on.

Labels are added, never synced: a second import will not strip one somebody put on by hand in between. That is the same rule the rest of the importer already followed.

This is the one place the importer creates a record from a string, and it is right here for a reason that does not generalise — a label is a string. An unknown assignee is still reported rather than given an account.

Four documentation claims that had gone stale

The worst kind of wrong, because they were true the day they were written:

  • Import said Félagi stores no files. It has since v0.18.0.
  • Import said there are no labels to map. There have been since v0.18.0.
  • Import said there are no cycles, for the same reason. There have been since v0.19.0.
  • The API reference said nothing calls you. Signed outgoing webhooks have existed for four releases.

Each has been replaced with the reason that is actually true now, which in two cases is a different reason entirely.

What this release does not do

  • Cycles still cannot be imported. A sprint in an export is a window somebody opened on dates the file does not contain.
  • Nothing writes a label or a cycle over the API. Both are readable and filterable; setting them is done in the interface.
  • No saved filters on the board. Reports have saved views; the issue list keeps its filters in the URL and nothing else.

Version 0.20.0

Date: 1 August 2026

New: timesheets

Timesheets, in the sidebar. Your week as seven columns, with the days you recorded nothing on still there.

Time could already be logged on an issue, and that is still the right place when you are looking at one. This answers the question an issue cannot — what have I written down this week, and what is missing — which is the question somebody actually has on a Friday afternoon with four days to remember at once.

The empty days are the point

A list of what was recorded cannot show what was not, and what was not is exactly what somebody opened the page to find. So every day in the period is a column, showing what you recorded against what was expected of you:

mon 3    4h of 8h
tue 4    8h of 8h
wed 5    3h 30m of 8h
sat 8    —

"Expected" comes from the same weekly rate the Gantt and cycles already use, so somebody on twenty hours is expected to record four a day rather than eight. It is borrowed rather than restated — a second copy of that arithmetic is a second thing that can drift away from the plan it is being compared against.

The heading carries the week number after the dates — 3–9 August 2026 (week 32) — because plenty of teams schedule, invoice and talk in week numbers.

Corrections that leave the totals right

Edit or remove anything you typed. Moving an entry from one issue to another repairs both issues' totals, the one losing the hours and the one gaining them, and totals are recalculated from the rows rather than adjusted by a difference. A sum that went wrong once stays wrong forever under a difference.

Whose hours are whose

An agent's hours appear and cannot be edited. The run is in the timeline with its own duration, and editing the entry would let the time report disagree with the history it came from.

Somebody else's hours cannot be edited either, by anybody. A workspace admin can read the whole workforce's weeks — people and agents both — and correct nobody's. Another person's hours are their statement about their own week.

Notes are written in the editor

On the timesheet and on the issue. Formatting survives, HTML is sanitised on the way in, and the narrow day cell and the CSV export both show the words without the markup. The column grew from 255 characters to text: it was enough for "fixed the login bug" and not for the paragraph somebody writes when the afternoon needs explaining.


Upgrading

php artisan migrate

What this release does not do

  • No approval, submission or locked periods. A timesheet here is a record, not a workflow.
  • No timer. Nothing to start and stop; durations are typed.
  • No billing. No rates, no billable flag, no invoice.
  • No copying last week, and nothing reminds you about an empty Wednesday.
  • Not in the API. Read through the reports, written through the interface.

Version 0.19.1

Date: 1 August 2026

The front page had not caught up with the last release. Cycles existed, worked and were documented, but a visitor reading the page would not have known.

Cycles are named where the tracker is described — in the opening sentence, in the capability row, and as their own card: what they are, and why they are not sprints.

"Machine time is time" now counts forward as well as back. Reports have always shown the machine hours a team spent; a cycle shows the machine hours it has available. It is the same argument in both directions, and the second half was missing from the page.

A new question in the FAQ: why cycles instead of sprints. Anybody arriving from Jira will ask it, and the answer is a design decision rather than an omission.

One card that was always missing: import, the REST API and signed outgoing webhooks were built, documented and never mentioned on the front page.

Four new tests hold these claims against the schema, the scheduler and the report that produces the capacity figure. A landing page is the easiest place in a codebase for a sentence to quietly stop being true.

What this release does not do

Nothing changed in the application. This is the front page only.


Version 0.19.0

Date: 1 August 2026

New: cycles

A fixed window of time that arrives on a cadence. Pick one to four weeks and Félagi generates the current window and the next few to plan into. Cycles, in the sidebar.

There is nothing to open, nothing to name and nothing to close.

Why not sprints

A sprint is a container for a ceremony — somebody names it, fills it, starts it, closes it — and every one of those is a step that can be skipped, which is why so many teams have a Sprint 47 that ran for nine weeks.

Two things make that shape wrong here. Agents do not attend planning meetings; they work continuously, which is the point of them. And capacity in Félagi has always been a rate rather than a rota, so a window is a view over the timeline the Gantt already computes rather than a second plan competing with it.

What is unfinished moves on, and Félagi counts how many times

When a window's time is up, everything not done or cancelled moves into the current one — and each issue carries a count of how many closed windows it has survived.

That count is the most useful number a planning tool can produce and the one most of them hide. "Carried for four cycles" cannot be reconstructed afterwards from anything else, so it is a column on the cycle page and a line on the issue itself.

Finished work stays where it was finished. Its history is the point.

Does it fit? — with the machines counted

Planned   6d      across 5 issues
Capacity  40d     20d people · 20d machines

No other tracker can make that comparison, because an agent's weekly hours count here exactly as a person's do. A window's capacity is the whole workforce rather than the human half of it.

Three things about the arithmetic:

Issues without an estimate are counted separately, never as zero. Treating them as zero makes an under-planned cycle look comfortable, which is the opposite of useful.

Nothing is refused for not fitting. A plan that cannot be over-committed is a plan nobody believes — over-commitment is reported, along with what it means: this is what will be carried into the next window.

Working days. A fortnight from a Monday is ten, not fourteen.

The windows before this one

One cycle's numbers mean nothing alone, so the last few closed windows sit beside the current one. "You finished 14, 11 and 16" is what makes the fourth a forecast rather than a hope.


Upgrading

php artisan migrate

Add felagi:cycles to your scheduler if you are not running php artisan schedule:work. It runs daily at 00:05, generates windows ahead and closes finished ones. Nothing breaks without it — cycles simply stop arriving.

Cycles are off until a workspace admin turns them on. An issue in no cycle is the normal case, not an oversight.

What this release does not do

  • No burndown chart and no velocity forecast. The last few cycles are shown; nothing extrapolates from them.
  • No per-project cadence. One per workspace, because capacity and membership live there.
  • No filtering the board by cycle. An issue says which window it is in and the cycle page lists its work, but the board does not narrow to it yet.
  • Not in the API or the import.
  • Nothing plans for you. Capacity is compared against what somebody planned; it does not do the planning.

Version 0.18.0

Date: 1 August 2026

New: attachments

Drop a file onto a comment — a screenshot of the failing request, the log, the spreadsheet somebody sent. Images appear in the thread; everything else is a row with a download.

Up to ten files at a time, 25 MB each. Whoever attached a file can delete it, and so can a workspace admin — but nobody else, because a file is evidence and quietly removable evidence is worse than none.

Files are served by Félagi, not from a public folder. A file on a public disk is readable by anybody who guesses the URL, and these are screenshots of somebody's production incident. Every request checks that you are in the workspace the file belongs to — the workspace the file is in, not whichever one your session last selected.

Some types download rather than display. An SVG is an image and also a document that can carry script: shown inline from Félagi's own address, it would be a way to run code on a page you trust. PDFs get their own reader with its own history. Both arrive as downloads.

The file type is read from the file, not from its name. A .png that is really a script does not get in.

New: labels

Type a word in the sidebar and it is a label. No settings page to visit first — a team that has to define needs-design before they can write it down writes it in the title instead.

Bug and bug are one label, because two that differ only in capitalisation are a filter that quietly misses half its issues. The same word gets the same colour every time, from the palette rather than a colour picker, so a board stays scannable and nobody has to reassign six labels because two came out alike.

Labels already in use are offered underneath. Without that, one workspace ends up with needs-design, needs design and design needed.

Changed: the sidebar asks its questions in order

The workspace switcher is above the search box now. Which company am I in, then what am I looking for — search was sitting above the context it searches.


Upgrading

php artisan migrate

Attachments are stored on whatever FILESYSTEM_DISK points at. The default is local; an S3 bucket works and must not be public.

What this release does not do

  • No uploading over the API. Reading an attachment is there; adding one is not. A multipart endpoint needs its own quota and rate rules, and half of one is worse than none.
  • No workspace storage quota. Nothing stops a team filling a disk.
  • No image resizing or thumbnails. A 25 MB screenshot is served as a 25 MB screenshot.
  • No attachments from agents. A run can deliver a link; it cannot upload a file.
  • No label management page, and no renaming or merging. Labels are created on use and removed from an issue; tidying up a workspace's list is not there yet.
  • No filtering the board by label. They can be applied and read, not searched on — that is the next thing they need.

Version 0.17.1

Date: 1 August 2026

Fixed: a run reported a field that could never have a value

The API returned failed_at on a run. There is no such column — it was permanently null, and a client reading it would reasonably have concluded that no run ever fails.

completed_at was no better: it is set on a terminal failure too, because internally it has always meant finished. Both are now one finished_at, and status says how the run ended.

This is a rename inside v1, which the versioning rules below say should wait for v2. It is done now because the API is a day old and a field that can only ever be null is worse than a break nobody has had time to depend on.

Changed: the API reference is a reference now

It documented 14 endpoints and 7 of 53 fields. It explained the decisions well and could not be built against — somebody writing a client had to read our source or guess.

It now has every field of every resource with its type, every status code and when it is returned, the pagination envelope rather than just the per_page filter, and what the API deliberately withholds and why.

And a versioning policy, which is the part that makes the rest usable: new fields, new endpoints and new enum values can arrive inside v1, so match the values you care about and pass the rest through. A client that throws on an unknown status will break on a Tuesday.

The documentation now has tests

Documentation does not fail by being written badly. It fails by being correct on the day it was written.

So the suite asserts that every field of every API resource appears in the reference, that every endpoint does, that nothing is documented which the code does not return, and the same for the daemon protocol — its envelope fields, its headers, and its rate limits, which are compared against the values actually configured rather than the numbers somebody typed into a sentence.

That is what found failed_at, and it is what will find the next one.


Upgrading

Nothing to migrate. If you built something against completed_at or failed_at this week, read finished_at instead.


Version 0.17.0

Date: 1 August 2026

New: Claude Code

Félagi ran exactly one agent CLI, and it was the one whose output format we control. For anybody using Claude Code — which is most people — the product did not work, and "anything with a CLI" was a claim rather than a description.

Update your runtimes to daemon 0.6.0 and claude is detected alongside elyra. Create an agent, choose Claude Code, assign it an issue.

The adapter was written against real output rather than documentation: the tool calls, their results, the session id and the final summary all came from running the CLI and reading what it printed.

The CLI has to be signed in as the user the daemon runs as. Its credentials live in that user's home directory — the same rule as git and gh. If a person can run claude -p "hello" on that machine, the daemon can; if not, every run fails with Not logged in, and the timeline will say so.

Permissions stay yours. A headless run cannot answer a permission prompt, so put whatever the agent may do in its arguments--allowedTools Read Edit, or whatever your team has decided. Félagi will not widen them on your behalf: doing it in the adapter would hand every Claude Code agent in every workspace unrestricted tool access.

New: what a run cost

tokens: in 2 / out 7 / cached 0 / cache-write 20787
cost:   $0.13

Where your CLI reports it, Félagi records it — per run, on the issue beside the hours, and in the agent report with a CSV export.

Three things about the numbers:

A total says how many runs it covers. "$4.12 from 37 of 40 runs" rather than a figure that invites being multiplied out.

Nothing reported shows , never $0.00. A provider's silence is not the same as a free run, and Elyra reports nothing yet.

The pricing is your CLI's. It knows the model and the tier. Félagi would be guessing at a table that changes without telling anybody.

This also answers the question Machine time is time raises: a run's duration has been counted as spent time since v0.4.0, and now the money is beside it.


Upgrading

php artisan migrate

Rebuild your daemons at 0.6.0 for Claude Code and for cost. An older one keeps working and simply reports neither.

Nothing about the protocol requires it: result.usage is additive, and a daemon that omits it behaves exactly as before.

What this release does not do

  • No Codex adapter. It is next, and it is not here — writing one without running the CLI would mean inventing its output format, which is how a feature ships broken.
  • No cost from Elyra. It does not report usage yet.
  • No budget caps or alerts. Cost is recorded and shown; nothing stops a run for spending too much.
  • USD only, and no conversion.

Version 0.16.1

Date: 1 August 2026

Fixed: a production installation could not have a platform admin

Single sign-on shipped an hour ago, behind the platform-admin gate — and a freshly deployed Félagi has no platform admin and no way to get one. The seeders skip production deliberately, is_admin is not something a form can set, and nothing in the interface grants it.

So the release that added single sign-on also made it unreachable on exactly the installations that would want it. The same was already true of the two-factor allowlist, the user list and the analytics page.

php artisan felagi:admin ops@yourcompany.com

An account that already exists is promoted. One that does not is created, with a password printed once — generated rather than prompted for, because a password typed into a deploy script is a password in somebody's shell history. Félagi asks for a new one at first sign-in, since that one has been on a terminal.

php artisan felagi:admin someone@yourcompany.com --revoke

Revoking the last one is refused. An installation with no platform admin cannot reach its own settings, and the only way back is this command — which needs shell access somebody may not have at the moment they need it.

Why a command rather than a page. Platform administration reaches across every workspace, and anybody can create a workspace. The boundary for it is the machine, not a role that can be handed out from inside a customer's workspace.

Also: a setting that had quietly stopped working

must_change_password and its middleware have been in Félagi since before invitations existed, and once invitations landed nothing set the flag any more — dead code that looked like a security feature. It now has the job it was written for: the one password Félagi ever prints to a screen is the one it insists you replace.


Upgrading

Nothing to migrate. If you are running Félagi in production and have never been able to reach Platform, this is why, and the command above is the fix.


Version 0.16.0

Date: 1 August 2026

New: single sign-on

OpenID Connect — Okta, Entra ID, Google Workspace, Auth0, Keycloak, JumpCloud. Under Platform → Single sign-on.

Four fields: issuer, client ID, client secret, and the email domains it owns. Everything else is discovered from the issuer, so a provider that moves a URL keeps working — and discovery is attempted before the provider is saved, because a provider that cannot be reached is a login page that fails for everybody on that domain.

Identifier-first. Somebody types their work email and is sent to the right place. A login page with four sign in with buttons asks every user a question only their administrator can answer.

Refusing passwords

Tick one box and a password is rejected for those domains even when it is correct.

That is the reason an enterprise buys single sign-on: an account that can still be reached with a password has not been centralised, it has been duplicated. Everybody else keeps using a password.

It counts as the second factor

By default, signing in through your provider satisfies the two-factor requirement. Your provider did MFA; asking for a Félagi code as well is theatre.

It is a per-provider setting rather than an assumption, because a provider configured without MFA would otherwise silently satisfy a requirement it does not meet.

Accounts are matched on identity, not on email

On the provider's sub claim, which is the one thing a provider promises is stable. Matching on email would mean a rename creates a second account and recycling an address inherits one.

An existing account with that address is claimed rather than duplicated, and keeps its role — single sign-on decides who somebody is, not what they may do. A new account is created only where a workspace has been configured for that provider, and only for a domain the provider owns.

Why it is a platform setting

Not policy — the data model. An account here belongs to a person, and a person can be in several workspaces. Authentication happens before anybody has chosen one, so there is no workspace to consult when somebody arrives at the login page.


Upgrading

php artisan migrate

Nothing changes until a platform admin adds a provider. The login page only offers single sign-on once one exists — a button that always fails is worse than no button.

What this release does not do

  • No SAML. OIDC covers most of the market. SAML means XML signature verification, where signature-wrapping attacks are a real and documented hazard; that needs a maintained library rather than a weekend, and saying so is better than shipping something that looks like SAML.
  • No SCIM, and no automatic deprovisioning. Removing somebody at the provider stops them signing in, but their workspace membership stays until it is removed. For a clean offboarding, do both.
  • No group or role mapping. Everybody arrives as the configured default role.
  • One provider per domain, and no provider-initiated sign-in.

Version 0.15.0

Date: 1 August 2026

New: import from Jira, Linear, YouTrack and Monday

Export a CSV from wherever your work lives now, and bring it here.

Administration → Import. Choose the source, pick the file, and press Inspect — nothing is written yet.

Nothing is written until you have seen what it would do

What this would do
  412 created    0 updated    3 skipped    415 rows read

The dry run is the feature rather than a courtesy. Along with the counts it shows:

Which column it thinks means what, as dropdowns. Correct any of them and the counts recompute.

Every status, priority and type in the file, and what each becomes. Only the values actually present — a mapping screen listing sixty statuses nobody uses is a screen nobody reads. Anything it could not map is highlighted, because an import that silently turns Blocked into Backlog has destroyed the one thing you needed to know.

Assignees who have no account here, by name. Those issues arrive unassigned: Félagi will not create accounts from a spreadsheet, which would be a migration that silently grants people access.

The first few rows as it reads them, with the mapping applied.

Why it maps columns instead of knowing four formats

Two of the four sources do not have a format.

A Monday export is the board's own columns, named by whoever built the board. A YouTrack CSV is whatever the person exporting chose, and status and priority live in custom fields. A parser written against "the Monday format" would be correct for exactly one customer.

Jira and Linear are stable, and Félagi knows their column names from the exporters themselves. For everything else, detection plus a dropdown beats a guess plus a support conversation.

Running it twice is safe

Issues are keyed on source and issue key, as a unique index rather than a hopeful lookup. The most common way a migration goes wrong is somebody running it again because they were not sure the first one worked.

A second import updates rather than duplicating — and only the fields the file contains, so a due date you set here afterwards is not wiped by a column the export left empty.

It keeps what matters

Their created dates, so a four-year backlog does not all appear this morning. The epic links, resolved after everything exists so a row referring to an epic further down the file still works. Descriptions escaped rather than trusted — Jira wiki markup and Linear markdown are not our HTML.

One bad row is skipped and counted, never fatal. Up to a hundred notes are kept and shown under the import.


Upgrading

php artisan migrate

Add imports to your queue worker or nothing will be imported and the worker will look healthy:

php artisan queue:work redis --queue=broadcasts,mail,webhooks,imports,default

What this release does not do

  • No comments or attachments. A Jira export repeats a Comment column per comment, and nothing in Félagi stores files yet.
  • No API pull. File export only, deliberately: pulling from four vendors means storing four kinds of customer credential on a server whose whole argument is that it holds no keys.
  • No labels, sprints or cycles, because Félagi has none of them.
  • No undo. No "delete this import" button — the dry run exists so that matters less.
  • 20 MB and 20,000 rows per file. Export in batches above that.

Version 0.14.0

Date: 1 August 2026

New: outgoing webhooks

Félagi could be told things — a forge saying a pull request merged, a CI server opening an issue. It could not tell anybody anything. Everything outside had to poll, which made the API a one-way street.

Administration → Webhooks. Give it a URL, tick the events, and Félagi posts to it when they happen:

issue.created · issue.updated Opened, or edited
issue.status_changed · issue.assigned Moved, or handed over
comment.created Somebody or something commented
run.completed · run.failed An agent finished, or could not
delivery.recorded A pull request or branch was delivered, or its state changed

Only the ends of a run are sent. A task going from queued to dispatched is machinery, and an endpoint interested in the outcome should not pay for every step.

Every delivery is signed

X-Felagi-Signature is an HMAC of the timestamp and the body, not the body alone — signing only the body means a captured delivery can be replayed forever. The guide has nine lines of verification code you can paste, and explains the three things that matter in them.

Anybody can send your endpoint a request that says it is Félagi. Check the signature.

It stops when your endpoint is gone

Four attempts, backing off 10 seconds, a minute, five minutes — about the length of a deploy, so an endpoint that comes back has not missed the event.

A 4xx is not retried: the endpoint understood and refused. After twelve consecutive failures Félagi stops and says so on the page, because retrying a gone endpoint forever costs a worker on every event and tells nobody. One success forgives everything before it.

Every attempt is logged — status, duration, error, which try — and shown under the webhook, because "did you send it?" is the first question anybody asks.

Where a webhook may point

A delivery is a request the server makes on somebody's instruction, which is a server-side request forgery if nobody looks at it.

Link-local addresses are always refused, whatever the configuration says. 169.254.169.254 is the cloud metadata endpoint on every major cloud, and a webhook aimed there is somebody reading the machine's own credentials through us.

Private and loopback addresses are refused by default. The reasoning is the same as for the two-factor allowlist: anybody can create a workspace, so "only a workspace admin can set this" is not a boundary. Self-hosted installations with a genuine internal endpoint set FELAGI_WEBHOOK_ALLOW_PRIVATE=true deliberately. Redirects are not followed, since a 302 is how that check gets walked around.


Upgrading

php artisan migrate

Add webhooks to your queue worker, or nothing will ever be delivered and the worker will look perfectly healthy:

php artisan queue:work redis --queue=broadcasts,mail,webhooks,default

What this release does not do

  • No replay. A failed delivery cannot be resent from the interface.
  • No hostname resolution check. A hostname resolving to a private address gets through. Catching it needs a DNS lookup on save and another at delivery, because the answer can change in between — and a check defeated by editing a DNS record is worse than one documented as absent.
  • No per-event endpoints, and no payload filtering. One URL, a set of events, the whole shape.

Version 0.13.0

Date: 1 August 2026

New: ⌘K

Press ⌘K — or Ctrl-K — anywhere in Félagi.

It searches issues, projects, agents and skills, and it goes places: the pages are in it too, and are offered before you type anything. A palette that does nothing until you type is a search field with extra steps.

Type an issue key and it goes straight there. ACM-231, acm 231, acm231, or just 231. Somebody who types an identifier has already decided where they are going; making them read a list of near-matches first is worse than any ranking.

Arrow keys move and wrap, opens, esc closes. There is a Search button in the sidebar as well, because a shortcut nobody is told about is a shortcut nobody uses.

Until now there was no way to find anything you could not see on screen, and no keyboard shortcut anywhere in the application — which for a tool people have open all day is less a missing feature than a missing posture.

What it will not find

Anything in another workspace, and nobody else's private agent — the same visibility rule as the assignee picker, because a search that ignored it would be a way to enumerate what you cannot see.

Comment bodies, deliberately. A palette that searches every comment finds the same issue eleven times and stops being a way to get anywhere.


Upgrading

Nothing to do.

What this release does not do

  • No fuzzy matching. A typo finds nothing.
  • It does not learn. No recent or frequent items.
  • LIKE '%term%', which cannot use an index. Honest about being the first thing to change at larger sizes: a full-text index would be MySQL-only and would make the test suite and production disagree, which this project has been bitten by before.

Version 0.12.0

Date: 1 August 2026

New: a public API

Until now the only thing that could talk to Félagi was the daemon. Nine endpoints, one credential, and nothing else — a closed system being sold as an open one.

There is a REST API now, over the same data the interface shows.

curl https://felagi.example.com/api/v1/me \
  -H "Authorization: Bearer fat_…"

Create a token under Settings → API tokens. Read or read-and-write, with an optional expiry. It is shown once: Félagi stores a hash, so a lost token is replaced rather than recovered.

A token acts as you

What it can reach is decided by your membership and your role, exactly as in the browser. There is no second permission model, because two authorisation systems eventually disagree and the disagreement is always found by a customer.

It follows that a token can never do more than you can, a private agent belonging to somebody else does not appear in it, and it stops working the day you leave the workspace — which is precisely the moment nobody is thinking about tokens.

It names one workspace, too. Anybody can belong to several, and a credential that followed whatever the browser last selected would have a reach nobody could state.

Putting an agent to work in one call

Two ways, both of them what the browser does:

# Assign it — this queues a run
curl -X PATCH …/api/v1/issues/ACM-231 \
  -d '{"assignee_type": "agent", "assignee_id": 4}'

# Or mention it, which hands over the thread with a reason attached
curl -X POST …/api/v1/issues/ACM-231/comments \
  -d '{"body": "@Freya the build is failing on main"}'

The second is often what a CI server should do: it leaves a sentence explaining why, which the agent then reads.

What it will not tell you

Decisions, not omissions: an agent's instructions, its environment variables, a run's lease token or session id, and anything at all in another workspace — which answers 404 rather than 403, because telling somebody a record exists but is not theirs confirms an id and a customer in one response.

Full contract, including every field and every filter, in the API reference.

Fixed: the email preference could not be saved

The Email me setting was rendered inside the account-deletion form — a form whose route is DELETE and whose handler deletes an account. The controller that actually saves the preference is the profile one, where the field did not exist.

So anybody who changed that dropdown and pressed a button either did nothing or was asked to confirm deleting their account. It has been that way since the setting shipped. It is in the profile form now, where its handler lives.


Upgrading

php artisan migrate

Nothing else. No existing behaviour changes, and no token exists until somebody creates one.

What this release does not do

  • No webhooks out. Nothing calls you; you poll. updated_since on GET /issues exists so that polling is cheap.
  • No write access to projects, agents or skills. Creating an agent means naming a runtime and a provider, and getting that wrong over an API is harder to see than getting it wrong in a form.
  • No time entries over the API, in or out.
  • No OpenAPI document. The reference page is the specification.
  • No cursors. Page numbers, which drift if things are inserted while you page.

Version 0.11.0

Date: 1 August 2026

New: agents are confined to the work they were given

The argument for running on your own hardware is that you control it. Until now that also meant an agent having a bad afternoon had the whole machine.

Update your runtimes to daemon 0.5.0 and a run can no longer write outside its own task. The kernel enforces it — sandbox-exec on macOS, bubblewrap on Linux — along with a four-hour CPU cap, an 8 GB single-file cap, and a temporary directory of its own.

Administration → Runtimes has a Sandbox column, so one unconfined build box is visible without logging into it. A machine that cannot enforce confinement says limits rather than pretending, and an older daemon says unknown rather than being accused of off.

It is on by default. Somebody who needs it looser has to write that down, which is the right way round.

What it does not do, stated plainly because a sandbox that is oversold is worse than one that is absent: the network is untouched — an agent CLI exists to call a model — and reads are not confined, because a CLI reads its interpreter, its libraries and its certificates. This contains a careless agent, not a hostile one.

Changed: the front page says what Félagi is

It led with agents and left the project tool to be inferred. That is the wrong way round, and it read as an agent product with a task list attached.

Félagi is a project tracker — projects, epics, boards, dependencies, Gantt, time and reports — that has agents in the assignee picker. There is a new section saying so, and a new answer in the FAQ to the question anybody sensible asks first: is it useful without agents? Yes. Plenty of teams will run it that way.

And machine time is time has come out of a code comment and onto the page. An agent's run costs your team something, and a report that hides it understates both the cost and the help — so a run logs its own duration against the issue, dated and attributed, in the same report as everybody's hours.

Fixed: three more claims that were not true

The audit two days ago was too narrow, and left contradictions behind:

  • The hero's terminal showed 12.4k tokens. Nothing records tokens. The audit searched for "token cost" and "token spend" and walked straight past a number.
  • works with Claude Code · Codex · Elyra claimed three integrations. One adapter exists. It now reads works with Elyra, next Claude Code and Codex — which is the same information without the promise.
  • The terminal header said an agent was running on Claude Code.

Upgrading

php artisan migrate

Rebuild your daemons at 0.5.0 to get the sandbox. An older one keeps working exactly as before and reports unknown in the new column.

If a run fails after upgrading where it used to pass, it was writing somewhere it should not have been. "sandbox": "limits" in ~/.felagi/config.json restores the old behaviour while you find out where.

What this release does not do

  • No network confinement, no read confinement, no memory cap. Each is explained in the daemon's README rather than quietly omitted.
  • No process-count cap. RLIMIT_NPROC counts every process the user owns, so a useful cap and a working machine are mutually exclusive without cgroups.
  • On Linux, bwrap must be installed for confinement. Without it the daemon applies resource caps and says so.

Version 0.10.1

Date: 31 July 2026

Deliveries shipped an hour ago. This is the part where we finish saying what they do.

Changed: how it works is four steps now

The front page described three: connect, hire, assign. The fourth is the one with a person in it — review it — and it says plainly that Félagi never merges anything on your behalf.

Its heading moved with it. Assign work. Walk away. Read the timeline was honest when a run finished by writing a paragraph. It finishes by handing you a pull request now, so it reads Review what comes back.

Fixed: the documentation claimed cost tracking too

The front page was audited for this two days ago and the same claim was sitting in the concepts page, describing a task as carrying "token cost". It does not. Nothing anywhere records tokens or money, and the reports guide has always said so — the two pages contradicted each other, and the wrong one was winning.

A task carries how long it ran. That is the honest sentence.

New documentation

  • Delivering work — artifacts, the skill that produces them, and the webhook that closes the loop
  • The daemon protocol reference now documents result.artifacts: every field, every rule, and why a malformed one is dropped rather than refused
  • The data model explains why artifacts are a table rather than a key in tasks.result, which is the kind of decision that looks arbitrary a year later
  • Concepts has an Artifact entry

Upgrading

Nothing to do. No code changed in this release.

What this release does not do

Nothing new works that did not work in 0.10.0. This is words.


Version 0.10.0

Date: 31 July 2026

New: a run hands something over

A finished run left a summary comment. It said what happened; it did not say where anything was — and since the agent works in a checkout on your runtime, a branch full of changes simply stayed on that machine.

Runs now deliver artifacts: a pull request, a branch, a document, a preview link. They appear as chips on the issue, next to the agent's summary, exactly where a colleague would have put a link.

A pull request hands the issue to a person

When a run delivers a pull request, the issue moves to In review.

This is the only place an agent changes an issue's status, and it is narrow on purpose. A summary is not a delivery. A branch or a link is not something anybody has been asked to review. And an agent never reopens work somebody has already closed. The move is written to the timeline and attributed to the agent, so an issue that moved on its own says who moved it.

The result is a handover with a person in it: the agent delivers, you review, you merge.

And the merge closes the loop

Point your forge at the new return-path webhook and the issue finishes itself:

What happened Where the issue goes
Pull request merged Done
Pull request closed without merging Back to In progress

A one-step GitHub Actions workflow is in the guide. It takes a URL and a state and nothing else, so GitLab, Gitea, a CI server or a person with curl can post the same two fields.

Félagi still knows nothing about GitHub

There is no GitHub integration in this release, and none planned.

The agent opens the pull request itself, using gh or glab — tools already on your runtime, already authenticated — because it was told to by a skill. The guide contains one you can paste in. The whole integration is a written instruction and a tool you already had, which is why it works the same for GitLab and will work for whatever you use next.

An issue here can be about code, a document or an analysis. Building one host's idea of a pull request into the core would have made every other kind of work second-class.


Upgrading

php artisan migrate

Update your runtimes to daemon 0.4.0 if you want deliveries. An older daemon carries on working exactly as before; it simply never reports one.

To close the loop as well, set FELAGI_DELIVERY_SECRET and add the webhook. Until you set it the endpoint answers 404 to everything — it moves issues, so it is off until somebody switches it on.

What this release does not do

  • Nothing polls. If nobody posts the webhook, a merged pull request stays marked open here. Félagi will not go and look.
  • No diff in Félagi. Review happens where the code lives. A patch viewer is a code review tool and there are good ones already.
  • No automatic merge, and none planned as a default. The review is the point.
  • The skill is not installed for you. It is documentation you paste into a skill, because it belongs to your team's way of working rather than to us.
  • gh must be authenticated on the runtime. Félagi holds no token for anybody's forge, exactly as it holds no git credential and no model key.

Version 0.9.0

Date: 31 July 2026

New: agents check out your code

Until now an agent was handed an issue about a codebase and an empty directory. The list of allowed repositories was collected, stored, sent to the runtime — and then ignored. This release closes that.

Every repository on the workspace's list is checked out into the task's work directory before the agent starts, and the run's output says what happened:

Preparing repositories
acme-api — cloned
acme-web — updated

One repository means the agent starts inside it. Several means it starts in the work directory, with each checkout below it, and it is told where each one is.

Your git credentials stay on your own machine

Félagi holds no git credential and never asks for one. The checkout happens on your runtime, using whatever that machine already has — an SSH agent, a credential helper, a deploy key.

This is the same rule as the one about models: the server does not hold the keys. It is why a repository URL with a token or password in it is now refused when you save it, rather than being stored, sent to every runtime, and written to whatever logs it passes through on the way.

Changed: the repository list tells you what is wrong with it

It used to drop any line it could not read, without saying so — which is how an agent ends up without the repository its author was certain it had. Saving now fails and names the line:

Line 3 must not contain a password — the runtime supplies its own credentials.

https and ssh only. git:// is unauthenticated and unencrypted, and file:// would let a task read anything on the runtime's disk.

An existing checkout is updated, never reset

A resumed run finds its work where it left it, uncommitted changes and all. Throwing away an agent's half-finished edit because a run was resumed would be losing work to tidiness.

If the update fails — the machine is offline, the host is down — the run carries on with what is there and says so. Working from yesterday's code beats not working.

New: the daemon protocol is rate limited

Nine endpoints had nothing in front of them. They are now limited per token, at several times what a healthy runtime does: a runaway daemon is stopped, and normal work never notices.

Heartbeats have an allowance of their own, so a runtime with a polling fault is not mistaken for a dead one and its work cancelled.


Upgrading

Update your runtimes to daemon 0.3.0 before or alongside this release. The task envelope has changed shape and an older daemon cannot read it. One that is too old is turned away at the door with a message saying where to upgrade, rather than failing halfway through a run.

php artisan migrate

Then check Administration → Workspace. If a repository URL there contains a token, saving that page will now refuse it — move the credential onto the runtime where it belongs.

What this release does not do

  • No branch selection. A checkout uses the remote's default branch. There is no way to say "work on develop" yet.
  • No per-project repositories. The list is workspace-wide, so every agent in a workspace gets every repository on it.
  • Nothing is pushed. An agent can commit locally; sending that anywhere is still a person's job. No branches are created, and no pull requests opened.
  • Removing a repository from the list does not remove it from disk. A runtime that checked it out on an earlier run still has it.
  • No shared cache. Each task clones its own copy, so a large repository is cloned again for each new task rather than shared between them.

Version 0.8.0

Date: 31 July 2026

New: two-factor authentication

Félagi holds the keys to your codebase and to the machines that run it. A password on its own is one leak away from all of it, so there is now a second factor: a six-digit code from an authenticator app, on top of the password.

Setting it up lives under Settings, between the password form and the danger zone. It asks for your password in the same step — a stolen session must not be able to attach its own device — then shows a QR code for any authenticator you already use: 1Password, Bitwarden, Aegis, Google Authenticator.

You have to enter one code before it is switched on. Scanning a code you never test is how people lock themselves out of their own account.

Ten recovery codes are shown before it goes on, because afterwards is too late to be told to write them down. Each works once. You can look at them again or replace the whole set from Settings.

Turning it off asks for your password too.

New: it is required in production, and nowhere else

By default the second factor is demanded on a production installation and not on anybody's laptop. A code prompt on a developer machine protects nothing and gets in the way of every test run — which is how a team ends up switching it off in a way that quietly follows them into production.

If you want it elsewhere, or truly need it off in production, one setting decides it either way. The security page always states which of the two is in force.

New: five days before it is demanded

A new colleague can read an issue, comment and get their bearings before being marched through an authenticator app. The five days are counted from when the account begins — accepting an invitation, or registering — and Settings tells you how many are left.

Two things follow from that:

  • Somebody who has been here a year does not get five fresh days because the setting was changed this morning. They are asked on their next request.
  • The countdown in Settings is next to the button that ends it, so the warning can be acted on when it appears rather than when it expires.

New: addresses that skip the second factor

Requests from your office network can be exempted. Single addresses and ranges, IPv4 and IPv6, under Platform → Security. The page shows the address your own request came from, so you need not go and look it up.

Two things worth knowing:

  • The address is trusted, not the account. The same person from a café still proves who they are. There is no "remember this device".
  • It is a platform setting, not a workspace one. Anybody can create a workspace, so a per-workspace list would let any user switch off a security control by making a workspace of their own. A control that turns off authentication belongs with whoever is accountable for the installation.

If Félagi sits behind a proxy or load balancer, set TRUSTED_PROXIES before you use this. Without it the address Félagi sees is the proxy's, and allowing that one exempts the entire internet.

Also on the security page

Who has not set up a second factor yet, and how long each of them has left.

Fixed: the what's-new dialog showed every release ever

It rendered the whole file in one lump, under a heading announcing the newest version — so the release being announced was the last thing you reached after scrolling past everything that came before it.

Now it leads with what has happened since you last looked, and the earlier releases are below a divider, still there to scroll back to.


Upgrading

Run the migrations:

php artisan migrate

Nothing else is required. On a local or staging installation nothing changes at all.

On a production installation, everybody will be asked to set up a second factor. Accounts created in the last five days have the rest of their grace; everybody else is asked on their next request. Tell your team before you deploy, and if that is the wrong day for it, FELAGI_2FA_REQUIRED=false postpones it without postponing the rest of the release.

What this release does not do

  • No trusted devices. The code is asked for once per session, not once a month per device.
  • Passkeys are a way to sign in, not a second factor. They exist in Félagi already; they do not satisfy this requirement.
  • No administrator reset. Somebody who loses both their phone and their ten recovery codes needs somebody with database access. There is no button for it.
  • No per-workspace requirement. A workspace cannot demand more than the installation does.
  • No enforcement on the daemon. Machine credentials are tokens, and a token cannot hold a phone. Two-factor covers people.

Version 0.7.1

Date: 31 July 2026

Three small things in the furniture.

Fixed: Félagi had stopped saying its name

The workspace switcher had taken over the top of the sidebar, so the product name only appeared to somebody with no workspace — which is nobody. The name is back above the switcher, and the switcher has its own icon.

Changed: one way to change the appearance, and one way to reach settings

Dark mode was a toggle in the profile menu and a proper light/dark/system control under Settings. Only the second one remains.

Settings was in the sidebar and in the profile menu. It is now only in the profile menu, where the rest of your own preferences already were.


Version 0.7.0

Date: 31 July 2026

New: Inbox

Autopilots opened issues, agents finished runs and mentions started work — and nobody was told inside Félagi. There is an Inbox in the sidebar now, with the unread count on it.

What lands there

  • Somebody assigned you an issue
  • Somebody mentioned you in a comment
  • A comment on an issue you follow
  • Somebody moved an issue you follow
  • An agent finished a run, or could not

A run starting is deliberately not in there. That is machinery rather than news, and an inbox that reports machinery stops being read. You are also never told about your own doing.

What you'll notice

  • The unread count updates itself, without reloading the page
  • Clear takes an item out of the inbox and deletes nothing
  • Clicking an item opens the issue and marks it read on the way

New: following an issue

The inbox needed a list of who cares. You start following an issue when you open it, are assigned it, comment on it, or are mentioned in it — and there is a Follow button in the issue's properties panel for everything else.

Tip: Stop following is remembered. Your own old comment will not drag you back into a thread the next time somebody replies, which is the difference between unsubscribing and unsubscribing until further notice.

Changed: you will get less email

Every comment on an issue you were part of used to send you an email, with no way to say otherwise. Email now follows the inbox, and the default is only when I am mentioned or assigned.

What you'll notice

  • Three choices under Settings → Email me: everything, mentions and assignments only, or nothing
  • Everything still lands in the inbox whichever you pick — the setting only decides what also reaches your mailbox
  • Comment emails still carry a reply address, so answering one lands in the thread

Tip: If you liked the old behaviour, choose Everything in my inbox.

Also in this release

  • An issue read while it had no assignee could report having none for the rest of the request, including immediately after somebody assigned it

Version 0.6.0

Date: 31 July 2026

New: a dashboard that answers "what needs me"

The dashboard was four totals and a list of runs. A count of open issues is a fact about the database — it tells nobody what to do next.

What you'll notice

  • Assigned to you comes first, ordered the way you would triage it: already late, then due, then merely important
  • Active runs update live as agents work, without reloading
  • Needs attention shows the top showstoppers from the same projection the Gantt uses — what is late, what is waiting on something late, what has a date and nobody on it
  • Workforce says whether the machinery can actually work: machines responding, agents hired and busy, when the next autopilot fires
  • This week is closed against opened, and human time against agent time

Tip: If the dashboard says nothing is assigned to you and nothing needs attention, that is the whole status meeting.

Improved: it tells a setup step apart from an incident

"Runtimes online: 0" used to mean either nobody had connected a machine or every machine had gone quiet. Those are now different messages, because one needs a decision and the other needs somebody woken up:

3 machines connected, none responding — every runtime has stopped sending heartbeats. Queued work is safe and will be picked up as soon as one comes back.


Version 0.5.0

Date: 31 July 2026

A security fix, a required daemon upgrade, and reports that no longer struggle on a large workspace.

Upgrading

Update the daemon on every machine. This release requires daemon 0.2 or newer. An older one is turned away with a clear message rather than failing partway through a run, but it will not do any work until it is updated.

cd felagi-daemon && git pull && cargo build --release

The reason: the server now issues each run a token proving which machine holds it. A daemon credential says which workspace it belongs to and never which machine it is, so without the token the server could not tell two of your machines apart — and one could finish a run another was in the middle of.

Fixed: an issue could be assigned to somebody outside the workspace

The assignee list only ever offered colleagues and agents from your own workspace, but the value it sent back was taken on trust. A crafted request could assign an issue to any account on the platform, and their name would then appear on the issue.

Worse, the field accepted things that were neither a person nor an agent, and one of those resolved to another workspace's project — printing its name.

Both are refused now, and the name is resolved inside your workspace before anything is written.

Fixed: a departed colleague broke every issue they had commented on

Deleting an account left their comments with nobody attached, and the issue page stopped loading — permanently, for everyone. Those comments now read Former member.

Fixed: two agents finishing at once lost one of their times

When two runs completed on the same issue in the same instant, the issue's spent time counted only one of them. The individual entries were always right; the total on the issue was not.

Fixed: a report export could fail on an edited URL

Sharing a report link with a hand-edited date crashed the export instead of ignoring the part it could not read.

Improved: run output arrives faster and costs nothing to watch

Live output is now pushed over the websocket as the agent produces it, instead of the page asking the server for it several times a second.

What you'll notice

  • Output appears as it happens, not up to a second later
  • Ten people watching runs no longer competes with everybody else using Félagi

Tip: Run output needs the queue worker running, like everything else live. The dashboard says so when it is not.

Improved: reports work on a large workspace

Totals, counts and groupings are now done by the database rather than by loading everything into memory first. All time on a workspace with years of history is a report rather than a wait.

Time exports are streamed, so the size of the file no longer depends on how much memory the server has.

Also in this release

  • Durations refuse input they used to misread: -2h was quietly taken as two hours, and 2h30 lost the thirty
  • A retried run no longer counts the time it spent queued as work
  • Deleting an issue asks its agents to stop, instead of leaving a process running with nowhere to report
  • Faster boards, issue pages and reports: several places were making one database query per row on screen

Version 0.4.0

Date: 31 July 2026

New: Gantt

A new Gantt entry in the sidebar. Work laid out against time, grouped by project or by epic, sharing the same filters and saved views as Reports.

What you'll notice

  • Two bars per row. Blue is the plan, filled by progress; green underneath is what was actually worked, built from the time entries. Most charts draw only the first, and then nobody trusts them by the third week
  • Due dates as diamonds, project targets in yellow, today as a line
  • A bar with no start and no due date is drawn dashed, because it is a guess from the estimate rather than a decision somebody made
  • The period picks the window: in front of a Gantt, "show me September" means scroll there

Tip: Give an issue a start date as well as a due date and the bar is exactly what you planned. Without one it is worked backwards from the deadline and the estimate, which is a fine default and a poor plan.

New: working hours, and what they make possible

Everybody — person and agent — now has a working week. The default is Monday to Friday, 08:00 to 16:00, and it is editable per person from the members page and per agent from its settings.

What you'll notice

  • A person can be full time in one workspace and two days a week in another
  • Agents have their own figure: no evenings, but the machine is not always on
  • Every projection in Félagi divides by this number

New: what will stop this plan

With hours to divide by, the interesting question stops being "is this late" — a due date already answers that — and becomes "given what this person already owes, when can this actually land".

What you'll notice

  • A panel above the chart naming each problem, who owns it, and how many days it needs: Lands 5 Aug at current capacity — 3 days past 2 Aug
  • Work with nobody on it, deadlines with no estimate behind them, and issues promised after the project they belong to
  • Each person's and agent's queue measured in weeks, because weeks is the unit people argue in

Tip: The queue strip is the fastest way to see who the bottleneck is. Four weeks of work in a one-week sprint is visible at a glance.

New: dependencies and the critical chain

An issue can now wait for another. Add one from the issue's properties panel.

What you'll notice

  • Finish-to-start or start-to-start, with a lag in days for the review window
  • Lines drawn between bars, red and dashed when a successor starts before its predecessor ends — the picture of a plan that cannot happen
  • The panel names the cause rather than the symptom: cannot start until ACM-2 is done
  • Each late issue reports how much is stuck behind it
  • The critical chain — the issues with no slack — marked in yellow, with the date everything lands

Loops are refused. The candidate list never even offers one.

Tip: Our critical chain counts a busy person as a constraint, not only a dependency. Two issues on one person's queue with no link between them still cannot both move.

New: what-if scenarios

A scenario is a copy of the plan you can move around. Nothing is written to a real issue until you apply it.

What you'll notice

  • A plan selector at the top of the chart: live plan, or any scenario
  • Drag a bar, push a row a week, reassign it, or take the projected date straight off a warning
  • Move dates onto capacity puts everything on dates the work can actually happen
  • The bar shows whether it helped: critical 10 → 8
  • Applying skips any change whose underlying value moved since you proposed it, and says so

Tip: Scenarios are private until you share them. Try three, keep one.


Version 0.3.0

Date: 31 July 2026

New: Reports

Five reports, reached from Reports in the sidebar. They share one filter — period, projects, types, statuses, priorities, specific people and agents, and whether to count humans, agents or both — and differ only in what they add up.

Report The question it answers
Status report Where are we, what moved, what is stuck, what is due
Time report Where the hours went, and whose they were
Throughput Are we keeping up, and how long does work take
Workload Who is carrying what, right now
Agent performance Which agents are working, and which are failing

What you'll notice

  • Filters apply as you set them — no Run button, and the results dim while they catch up so you can see it happening
  • Every report exports to CSV with the filters you have on screen
  • The status report leads with what is overdue and on hold, because that is what a status report gets read for

Tip: The status report is built for somebody who is not in Félagi. Set the period, pick the project, export, and paste it into whatever your Monday meeting runs on.

New: saved views

Narrowed a report the way you like it? Save it, and it appears on the reports index and in the picker on that report.

What you'll notice

  • Saved views are private unless you tick Share with the workspace
  • A shared view can be opened by anyone and changed only by whoever made it
  • A view somebody deleted stops applying rather than breaking your bookmark

Tip: "Last month, design project, agents only" is a question you will ask again. Saving it costs one click and removes six.

New: logging time

Time is now recorded against an issue as individual entries — who, how long, and which day — rather than as one number you overwrite.

What you'll notice

  • Log time on an issue takes a duration, a date and an optional note
  • Entries are listed underneath, and you can remove your own
  • Agent runs write their own entries when they finish, so machine time shows up beside human time in every report
  • The spent-time total is now shown rather than typed: it is the sum of the entries

Tip: Log an hour against the day you worked it, not the day you remembered to enter it. Every report counts by the date the work happened.

Also in this release

  • The report tiles on the index are clickable. They previously looked like links and did nothing.
  • The workload report no longer offers a period filter it ignores.

Version 0.2.0

Date: 31 July 2026

New: a developer role

Running the agent workforce and deciding who works here are two different kinds of trust, and until now you had to grant both to give somebody either.

What you'll notice

  • A fourth role between admin and member, in the invitation and role pickers
  • Developers hire agents, connect machines, write skills and build autopilots
  • They cannot add, remove or re-role people, and cannot change workspace settings
  • Nobody is shown a menu item they would only get turned away from

Tip: This is the role for a contractor or an agency. They get the machines without getting your member list.

Changed: people are invited, not created

You no longer create an account for a colleague. You send an invitation to their email address, and nothing happens to any account until they accept it.

What you'll notice

  • Invite person replaces Add person on the members page
  • Pending invitations are listed with who sent them and when they expire, and can be resent or revoked
  • Somebody who already has an account joins with one click; a new one is created on the way in
  • Accepting confirms the email address, so there is no second email to open

Tip: Invitations last 14 days, and inviting the same address again replaces the open one. Nobody ends up with two working links in their inbox.

The reason for the change is simple: creating an account for somebody means choosing their first password, which means knowing it. That is not a thing a colleague should have to trust.

Improved: emails that sound like a workplace

The invitation, sign-in and password emails were rewritten. The defaults explained the mechanism; these explain the point.

What you'll notice

  • An invitation says who invited you, to what, as what, and what happens when you click — including that nothing happens until you do
  • Password and confirmation emails are shorter and say what they are for
  • All of it in the Félagi design rather than a framework default

Also in this release

  • The landing page now explains the name. félagi is Old Norse for one who lays their wealth in with yours — the root of English fellow. Not mythology: an ordinary word from law and trade, which is the better story.
  • Installation instructions no longer promise a Homebrew formula that does not exist. The daemon is built from source, and the documentation says so.
  • An outdated daemon is now pointed at a page that exists.
  • The MIT licence file the README has been claiming since day one.
  • New installations default to UTC rather than to the author's timezone.

Version 0.1.0

Date: 31 July 2026

The first release. Félagi is a place where humans and coding agents work from the same board: you assign an issue, an agent claims it, does the work on your own hardware, and reports back in the thread.

New: agents that work like colleagues

Hire an agent, give it instructions, and hand it an issue the same way you would hand it to a person — by setting the assignee, or by mentioning it in a comment.

What you'll notice

  • Agents appear in the assignee picker beside people, not in a separate "AI" section
  • Mentioning @Freya in a comment queues a run immediately
  • An agent comments with its summary when it finishes, and its time is added to the issue's spent time
  • The run's output streams into the issue page while it happens

Tip: An agent picks up its next run in about three seconds and resumes its previous session on the same issue, so a follow-up comment continues the conversation rather than starting cold.

New: your own machines do the work

Félagi never calls a model. Every agent runs as a CLI on hardware you control, driven by a small daemon that polls for work.

What you'll notice

  • Connect a machine from Admin → Runtimes and get a one-time token
  • One runtime per agent CLI, so a laptop with both Claude Code and Codex shows up as two
  • A machine that goes offline requeues its work automatically — a closed lid loses nothing

Tip: Credentials, code and context never leave your machine. The server only ever sees what the agent chooses to report.

New: skills, so knowledge compounds

Write down how your team does something once, attach it to an agent, and it is written into the work directory before the CLI starts.

What you'll notice

  • Skills are ordinary markdown, discovered by the agent's own CLI without being told
  • Attach a skill to several agents, or switch it off without detaching it
  • Extra files travel with a skill, keeping their folder structure

Tip: Good skills are specific and say what not to do. "Run the test suite before finishing" changes behaviour; "write clean code" costs tokens and changes nothing.

New: autopilots — work that starts itself

An autopilot opens an issue and hands it to an agent on a schedule, on a webhook, or when you press the button.

What you'll notice

  • A nightly dependency review, a weekday triage pass, an issue opened whenever a build fails
  • Placeholders fill themselves in: {{ date }}, {{ payload.branch }}
  • History shows the firings that produced nothing, and why

Tip: A scheduled autopilot will not fire again while its previous issue is still open, so a five-minute schedule cannot bury an agent under twelve copies of the same job.

New: projects, epics and time

What you'll notice

  • Projects with a lead who may be a person or an agent, and progress measured from closed issues rather than reported by hand
  • Epics that contain issues, with progress of their own
  • Estimates written the way you speak — 2h 30m, 1d — and agent run time counted as spent time
  • Issue types chosen per project, so a picker only offers what your team uses

New: replies by email

Comment notifications carry a reply address, and replying to one lands in the thread.

What you'll notice

  • Answering from your phone works, including mentioning an agent to start a run
  • Quoted history is trimmed away
  • Out-of-office replies are ignored rather than posted as comments

Tip: Replies are off until an administrator points a domain at Resend. Everything is configured in .env, so each installation uses its own domain.

New: everything updates itself

What you'll notice

  • Boards move as issues change, without reloading
  • Run output appears line by line as the agent works
  • Drag a card between columns to change its status

Nothing here has binaries yet — the daemon is built from source. There is no in-app inbox, no repository checkout, and no bounce handling. Those come next.