2026-09-162 commits
board: a log you can read, and a menu you can click
2c5fe11 · 22:43 · Marsita the Ultra
Four things Marsita asked for on 2026-09-16, all of them friction she was absorbing silently.
The placeholder now clears on focus. A hint sitting under the cursor competes with the thing it was hinting at.
Markdown comes out of the stream. The replies are written for a terminal, so they carry ##, **, fences and an 80-block bar -- none of which renders here. It just sat in the text as punctuation to read past. Marks go, words stay: a heading keeps its title, a link keeps its label, and no line is ever dropped whole. Only `claude` lines are touched; rewriting what Marsita typed would be lying about the transcript.
The menu boxes are buttons. Retyping a digit for something already on screen and already unambiguous is a keystroke for nothing. Only the NEWEST menu is live -- an old one is a decision already taken, and a page of clickable history sends the wrong answer to the wrong question. Answering disables the set. Delegated from the pane, since the stream is rebuilt every second.
The wait counts seconds beside the dots, and the pane header carries a median time-to-first-byte over the last hundred sends. Median because one four-minute turn should not move the number you read every day; measured from the keypress, because that is the wait you actually felt.
29 tests, no model, no tmux, no pty.
2 files
- fleet/bin/oneview.py
- tests/test_the_send_box_answers_immediately.py
board: say something is happening, whatever door you typed through
25abfe7 · 22:41 · Marsita the Ultra
Send did nothing visible for up to three seconds -- the box emptied and the page sat still, which is what a box that ATE your message looks like. Marsita, 2026-09-16: "update the text long above ... so I know something is happening".
The local echo goes up BEFORE the request leaves; the round trip is the slow part, so an echo that follows it has already lost. The other half reads the transcript instead of the send, because this box is not the only door -- Marsita types in the Claude app as often as here. Newest line not `claude` means the turn is unfinished: `you` waiting, `tool` working.
That check sits ABOVE loadStream's early return. A thinking turn produces no new lines for minutes, so "nothing changed" is exactly when the page most needs to say it is still there.
Poll 3s -> 1s. 12 tests, no model, no tmux, no pty.
2 files
- fleet/bin/oneview.py
- tests/test_the_send_box_answers_immediately.py
2026-09-106 commits
projects: each one owns its repos, and issues group by project
cc79da4 · 19:39 · Marsita the Ultra
Marsita, 2026-09-10: "each project should have repo or even multiple repo."
`repos:` on each project in projects.yaml, full owner/name. Ten of the sixteen have one now; BaseX has five and Independent Tribunal four. The mapping was read off the actual repo lists for PlanetaryCouncil, marsrobertson, independenttribunal and basexhq rather than inferred from names -- six projects have no repo yet and are left empty, because guessing one would file its issues under the wrong project.
The issues pane now labels each row with the PROJECT rather than the repo. That is what she thinks in; the repo is an implementation detail of it and stays in the hover. A repo no project claims still shows its own name -- the alternative is an issue with no label at all.
Two things fell out of doing it. `Live Agent Fleet` had `url: TODO`, so the pane had been silently dropping this very repo from her list of projects; it points at the board now. And a project with a repo but no url is still a project, so it links to the repo instead of vanishing.
A test asserts no repo is claimed by two projects -- its issues would appear under both.
1143 passed.
4 files
- fleet/bin/oneview.py
- fleet/bin/portfolio.py
- fleet/data/projects.yaml
- tests/test_my_projects_are_on_the_board.py
report: 2026-09-10 fleet summary
315ea89 · 19:00 · Marsita the Ultra
Generated by fleet/bin/report.py. Counted from the files the fleet writes while working, not written by hand.
4 files
- docs/report/index.html
- docs/report/report.html
- docs/report/report.json
- docs/report/report.md
board: show the projects, not just the machine that builds them Marsita, 2026-09-10: "I care about usability of me as the end user... I see my projects, I see my issues."
c66da22 · 14:39 · Marsita the Ultra
Sixteen of them were already written down in fleet/data/projects.yaml. The board had a pane for the repo it happens to be running in, a pane for its own processes, a pane for vendor credit -- and none for the work all of that exists to serve. Now the first pane in the left column is the projects, above the repo.
Straight from the YAML and nothing else: the file is hand-edited and is the source of truth, and the pane is not allowed an opinion the file does not already hold. Live projects first, then the ones waiting on a deploy, then for-sale -- still a project, but not what you sat down to look at.
A dot for status rather than the word: "awaiting-deployment" is nineteen characters of status on a row whose subject is four. `www` or `board` says where it lives. A url of TODO is honest in the file and noise on a dashboard, so those rows are dropped.
Cached on the file's mtime, so the common case is no parse at all.
/api/portfolio, not /api/projects, because that prefix is forwarded to the cockpit.
1135 passed.
4 files
- fleet/bin/fleet.py
- fleet/bin/oneview.py
- fleet/bin/portfolio.py
- tests/test_my_projects_are_on_the_board.py
reload: if nothing supervises the server, start it back up
3b64069 · 00:48 · Marsita the Ultra
The fallback killed the listener and trusted a supervisor to bring it back. The NUC has no fleet-server unit -- the process is simply running -- so that path would have taken the board down and left it down, which is worse than the stale board this script exists to fix.
1 file
reload: restart the server on the machine you are actually on
ddb0261 · 00:48 · Marsita the Ultra
The script called `launchctl` and nothing else. On the NUC that is not a command, so every reload there printed "launchctl: command not found", then "live again" -- because the OLD server was still answering on 8787 and the check could not tell the difference. New code landed on disk and was never served.
The NUC ran a board from before 2026-08-27 that way, which is how Marsita found it tonight: xterm still in the terminal pane, the day-divider stream, the placeholder that explains Enter. Thirty-one commits behind, and every `reload.sh` had reported success.
Now: launchctl, else systemctl --user, else kill the listener and let whatever supervises it bring it back. If none of those reached it, the script REFUSES rather than claiming success -- a restart that cannot restart must say so.
FLEET_PORT so the NUC's 8788 is checkable too, instead of asking 8787 whether 8788 came back.
1 file
board: hide the build gate
a0e8777 · 00:07 · Marsita the Ultra
Marsita, 2026-09-10: "I don't need it on the dashboard, I'm not using it ---> please hide."
Hidden, not cut out: /api/build-gate still answers and the JS that paints it still has its element, so turning it back on is removing one attribute. A control nobody touches is clutter; a control nobody can find again is a feature deleted by accident.
Worth its own line: `display:flex` beats the `hidden` attribute, so the first version of this changed nothing on the page. Anything with an explicit display needs its own opt-out.
1126 passed.
2 files
- fleet/bin/oneview.py
- tests/test_the_board_pane_watches.py
2026-09-0913 commits
poems: GitHub is the source of truth, and it does not need the gh CLI
c7c679f · 23:50 · Marsita the Ultra
Marsita, 2026-09-09: "Github and repo is source of truth... I want to see my poem online ASAP, on top, you can keep it on top always."
The submitted section already read the repo, but through `gh issue list` -- which means the page only works where that CLI happens to be installed and logged in. The repo is public and the REST endpoint is public with it, so it reads api.github.com directly now, over urllib, with `gh` kept as a second try for the case where an outbound request is blocked but a configured CLI is not.
A pull request is an issue as far as that endpoint is concerned, and a PR is not a poem, so those are dropped.
The page says it out loud: "The repo is the source of truth -- what is open there is what shows here." Which also means the page needs no deploy step to show a new poem. Open the issue and it is there within ten minutes, or immediately on a reload once the cache turns over.
Verified live: "Merge and undo" renders at the top under `sent in`, above the `from the fleet` band.
1124 passed.
2 files
- fleet/bin/poems.py
- tests/test_poems_are_poems.py
poems: yours at the top, and "go" is not one
0fc4345 · 23:36 · Marsita the Ultra
Marsita, 2026-09-09: ""go" is not a poem... I just want to see my on top."
Two faults, one root. The archive was built by taking the LAST LINE of every agent turn, and the last line of a turn is usually not a poem -- it is "go", or "Please let me know if these answers meet the requirements!", or a parenthetical aside about an XPRIZE. And the page showed only that archive, so a poem somebody chose to send had nowhere to appear at all.
The filter now rejects a one-word line, a machine signing off, a bare question (that is a machine asking for instructions) and a parenthetical aside (that is a footnote to something else). The length floor sits on the whole poem rather than each line -- a first pass put it on every line and threw out real short ones, and short is the entire point of a couplet.
Poems people send are pulled from the repo's open issues, which is where "how to submit" points them, and rendered ABOVE the fleet's own under a "sent in" band. Not mixed: a poem somebody chose to send is a different kind of thing from a couplet that fell out of a build, and burying the first among two hundred of the second is how you stop receiving them.
Quote markers are stripped, so "❯ STOP automerge to main" reads as the line it is. A dragged screenshot becomes an <img> in the issue body: printing the tag is worse than nothing and fetching the picture is not this page's decision, so those lines are dropped and the title stands in.
Cached ten minutes, empty on failure: GitHub being slow costs the page nothing but the human half.
1122 passed.
3 files
- fleet/bin/poems.py
- tests/test_poems_are_poems.py
- tests/test_poems_say_how_to_submit.py
poems: ask for poems, at the top
3a85fff · 22:35 · Marsita the Ultra
Marsita, 2026-09-09: "poetry... We have poems repo. Please at the top provide link to the repo 'how to submit'. We want to collect poems."
The page was a display case. It showed two hundred machine-written couplets and said nothing about whether anyone else could add one, which reads as closed even though the repo is open.
So, above the poems and inside the header: "Yours are welcome here", a link straight to the new-issue form, and the repo behind it. An ISSUE, not a pull request -- a poem should not need a fork -- and "human or machine", because a page of nothing but agent output does not look like it is asking people.
At the top on purpose. An invitation under two hundred poems is an invitation nobody reads.
1110 passed.
2 files
- fleet/bin/poems.py
- tests/test_poems_say_how_to_submit.py
pipeline: nothing merges itself any more
3791d4a · 22:24 · Marsita the Ultra
Marsita, 2026-09-09: "STOP automerge to main -----> I need to review changes... It then takes too much time to undo."
This reverses her own instruction of 2026-08-07 -- "fleet can merge... I don't want to worry about infra / pr / code / issues" -- and the reason is worth writing down, because it is not that she changed her mind about reading diffs. It is the asymmetry. A branch that waits costs the fleet some latency. A merge that was wrong costs HER an evening of undoing it. The cheap failure and the expensive one are not on the same side, and the whole autoland argument had quietly assumed they were.
`pipeline.autoland` in config.json, false. `land()` now files `stage: "await"`, rings needs_you once, and does not touch git at all -- not merge and roll back, not merge and skip the push. The branch is left exactly where it is. Recorded ONCE per proposal, because the pipeline runs hourly and four waiting branches would otherwise file ninety-six identical rows a day into the ledger the board reads.
`await` joins CLOSED_STAGES: done as far as the queue is concerned, so nothing rebuilds it, but not finished.
The switch is read on every call, not at import, so turning it back on is an edit to config.json rather than a restart. An unreadable config returns false -- the expensive failure must never be the default.
The merge machinery itself is untouched and still fully covered; its tests force the switch on.
1103 passed.
6 files
- fleet/bin/pipeline.py
- fleet/config.json
- tests/test_land_and_daily.py
- tests/test_no_local_addresses_in_public_text.py
- tests/test_nothing_merges_itself.py
- tests/test_six_units_not_eleven.py
board: less chrome per row, and the board box back to one line
f6d4362 · 21:59 · Marsita the Ultra
Marsita, 2026-09-09: "collapse into a single line" of the post box, and of the stream: "seems like a gap".
The gap was real but it was not missing comms -- the messages were rendering, starting 219px into a 528px pane. Forty-one percent of every row spent saying when and who before a word of what, and a message beginning that far right wraps into a deep empty gutter, which reads as a hole in the log rather than a long line. Smaller stamp, narrower name, tighter gaps: 219px down to 182px, and most rows are now one line at 18px.
The post box goes back to one line at rest. Three lines was borrowed from the box for talking to Claude, back when I had the two confused. Posting to the board is a sentence, not a paragraph, and a three-line box under the log took height from the thing you actually read. It still grows as you type -- 22px empty, 51px at three lines, 22px again on clear.
1096 passed.
3 files
- fleet/bin/oneview.py
- tests/test_the_post_box_is_a_textarea.py
- tests/test_the_stream_row_reads_as_a_line.py
board: the stream row reads as a line, and collapse means collapse
69a1cf7 · 21:31 · Marsita the Ultra
Marsita, 2026-09-09: "include YYYY-MM-DD HH:MM (no SS) timestamp. Timestamp \t who \t message. Category of the message to be represented with emoji" -- and, of the leftover row in a shut pane: "collapse, it looks ugly".
The row is four columns now: when, what kind, who, what they said. Chrome fixed, message flexing, so a run of rows reads as a column even when the messages are ragged.
Full date to the minute on every row. Seconds were noise in a log whose interesting gaps are hours, and a bare clock made you count back to work out which day 03:14 belonged to. The day divider goes with it -- announcing the date once only works while you read top to bottom, and this log is scrolled, filtered and jumped into.
One emoji per row, and it means the CATEGORY. The name carried the agent's emoji as well, which put two on a line next to the tag icon and made a run of them unreadable. A row with no known tag gets a dot: an empty column is a hole in the run.
And both props under the board's post box come out -- keeping it alive in a collapsed pane, and the min-height floor that stopped the terminal pane squeezing it. Both existed because I had mistaken it for the box she had lost. That box was Claude's, and it has its own pane now.
1095 passed.
3 files
- fleet/bin/oneview.py
- tests/test_the_post_box_is_a_textarea.py
- tests/test_the_stream_row_reads_as_a_line.py
board: talking to Claude gets its own box again
6445b8d · 21:25 · Marsita the Ultra
Marsita, 2026-09-09: "Talking to the board is a different thing than talking to Claude. Talking to Claude is the dedicated function. I talk to Claude directly, and I post to the text area. The bottom is if I want to talk with the board and something else. They are two different functionalities."
She asked what the reasoning behind merging them was. There wasn't any. The Claude box was removed on 2026-09-05 along with the terminal it typed into, and when she said the text area was missing I improved the only box left on the page -- the public board's -- four times over, without once checking whether it was the one she had lost. Not a merge. A substitution nobody decided.
So: a send-only box under `claude — this machine`, where it used to be. The send half of the terminal, kept, after the display half was thrown away. `tmux send-keys` does the typing on the server, so the browser never holds a pty and none of the latency that made the old box unusable is on this path. `-l` for literal text, or a message containing the word "Enter" gets pressed rather than typed; Enter follows separately.
POST /api/tell, local only, 404 from the internet. Deliberately not /api/signals: the board is public, and a line meant for Claude landing there would not be a layout bug, it would be a disclosure. A test asserts the handler cannot reach it.
A missing tmux session is started rather than refused -- a box that says "no session" asks the operator to do sysadmin before they may speak. A failed send keeps your words: the box dims and locks instead of clearing on hope.
Verified end to end: POST returned ok, and `tmux capture-pane` shows the line at the prompt.
1091 passed.
5 files
- fleet/bin/fleet.py
- fleet/bin/oneview.py
- fleet/bin/terminal.py
- tests/test_the_stream_is_one_way.py
- tests/test_two_boxes_two_things.py
board: the post box looks like a box
36a1e76 · 21:18 · Marsita the Ultra
Marsita, 2026-09-09, with the thing on screen in front of her: "where?"
It had been a textarea since ca562ab and it was 22px tall, which is exactly what the one-line input it replaced looked like. Correct markup, invisible change. A box you cannot tell from a field is a box nobody knows they have.
Three lines at rest now, growing past that and never below. The stream's floor goes to 104px to hold it, so the squeeze fix from 03f7a84 still holds at an absurd --hTerm.
Verified: 52px empty, 85px at five lines, 52px again on clear, and at --hTerm 900px the stream holds at 104px with the box inside it.
1079 passed.
2 files
- fleet/bin/oneview.py
- tests/test_the_post_box_is_a_textarea.py
board: the stream gets a floor, so the post box cannot be squeezed off-screen
03f7a84 · 20:27 · Marsita the Ultra
Marsita, 2026-09-09: "Still no text area... strange."
Strange is right, and the two earlier fixes were both real and both beside the point. Nothing was hidden. Her saved --hTerm was 537px, `#termpane` was `flex:0 0` so it could not shrink, and in a shorter column that left the stream TWO pixels tall. The post box still laid out at its full 31px -- entirely outside the pane. It measured perfectly from a console and was invisible on the screen, which is why two rounds of "is it display:none" found nothing.
So: `flex:0 1` on the terminal pane, and a 74px floor on the stream -- its heading plus the box, and never less. The list can be squeezed to nothing; the way in cannot. A stream collapsed ON PURPOSE gives the floor back, because shut by choice is not the same as crushed by accident.
Verified across three pinned heights, including one absurd: at --hTerm 900px the stream holds at 74px and the box stays inside it.
1077 passed.
2 files
- fleet/bin/oneview.py
- tests/test_the_post_box_is_a_textarea.py
board: collapsing the stream no longer hides the box you write in
43f1002 · 20:24 · Marsita the Ultra
Marsita, 2026-09-09: "It used to be there (I saw in tab you control) but not anymore after reload."
Both true at once. Her stream pane was collapsed from an earlier session and mine was not, and the collapse rule hid every `form` in a shut pane -- so the post box existed for exactly one of us, and the difference was invisible from either side.
The rule was right for filters and controls and wrong for this one form. You collapse the stream to get the LIST out of the way; the box is the reason the pane is open at all. Collapsed, the stream is now its heading and the box: the reading goes away, the writing stays. The signature pad stays folded, because collapsing a pane should never expand something inside it.
Verified by reproducing her exact state -- streamOpen:0 in localStorage, then reload: box visible at 22px, list hidden, pad hidden.
1075 passed.
2 files
- fleet/bin/oneview.py
- tests/test_the_post_box_is_a_textarea.py
board: the post box is a textarea you can see into
ca562ab · 20:18 · Marsita the Ultra
Marsita, 2026-09-09: "Lost the bottom text area for the blog. I thought I had a text area where I can type directly in the native field."
Two different boxes, and only one of them should come back.
The one that vanished was the compose box under the terminal pane, removed on 2026-09-05 along with the terminal. It typed into a pty, and the pty round trip is exactly what she called unworkable -- "neither the typing in console, neither the text area ---> bad UX". There is nothing left for it to type into, so it stays gone.
The one she was reaching for is `#sayBody`, which posts to the board. It had always been a one-line `<input maxlength="3900">`: it accepted a blog post and showed about sixty characters of it, so anything longer than a sentence was written blind. That was wrong before any of this week's changes and nobody had called it out.
Now a textarea. It grows with what is typed and stops at a third of the window -- unbounded it would push the stream off the screen, fixed it is the input again with extra steps. Enter posts, Shift+Enter is a newline, because a textarea swallows Enter and the post button alone would be worse than what it replaced. Both send paths reset the height, so a cleared box is not still four lines tall.
Verified live: 22px empty, 64px at four lines, back to 21px on clear.
1073 passed.
2 files
- fleet/bin/oneview.py
- tests/test_the_post_box_is_a_textarea.py
board: the north star, back at the top
e7f662b · 20:08 · Marsita the Ultra
Marsita, 2026-09-09: "at the very top, Singularity Engineering with the goals, so I always see it. It always reminds you of the goals... Please restore it."
A slim bar above everything: the name, then the year's goal, with the decade's goal as the hover behind it.
It is NOT the welcome row, which is the thing that used to sit there. She restored that on 2026-09-02 and cut it the same evening -- "upon some reflection ---> skip" -- because a row explaining the fleet, with links to /about and the guestbook, is an advert aimed at a stranger. Three tests hold that row shut and they still pass. This has no links and explains nothing: one sentence she wrote herself, kept in front of her.
The goal text is read from /api/horizons rather than typed into the chrome. A mission copied into the banner goes stale the day the chain is edited, and a stale north star is worse than none.
Singularity Engineering Operating supercomputer of dreams and engineering singularity.
1067 passed.
2 files
- fleet/bin/oneview.py
- tests/test_the_north_star_is_visible.py
report: 2026-09-09 fleet summary
bca69cc · 19:00 · Marsita the Ultra
Generated by fleet/bin/report.py. Counted from the files the fleet writes while working, not written by hand.
4 files
- docs/report/index.html
- docs/report/report.html
- docs/report/report.json
- docs/report/report.md
2026-09-081 commit
report: 2026-09-08 fleet summary
8f95371 · 19:00 · Marsita the Ultra
Generated by fleet/bin/report.py. Counted from the files the fleet writes while working, not written by hand.
4 files
- docs/report/index.html
- docs/report/report.html
- docs/report/report.json
- docs/report/report.md
2026-09-071 commit
report: 2026-09-07 fleet summary
2897b77 · 19:00 · Marsita the Ultra
Generated by fleet/bin/report.py. Counted from the files the fleet writes while working, not written by hand.
4 files
- docs/report/index.html
- docs/report/report.html
- docs/report/report.json
- docs/report/report.md
2026-09-061 commit
report: 2026-09-06 fleet summary
36cfe5f · 19:00 · Marsita the Ultra
Generated by fleet/bin/report.py. Counted from the files the fleet writes while working, not written by hand.
4 files
- docs/report/index.html
- docs/report/report.html
- docs/report/report.json
- docs/report/report.md
2026-09-052 commits
report: 2026-09-05 fleet summary
74d77d0 · 19:00 · Marsita the Ultra
Generated by fleet/bin/report.py. Counted from the files the fleet writes while working, not written by hand.
4 files
- docs/report/index.html
- docs/report/report.html
- docs/report/report.json
- docs/report/report.md
board: the terminal is retired, the pane is a one-way stream
30997e2 · 00:20 · Marsita the Ultra
Marsita, 2026-09-05: "terminal in the browser is unworkable... scroll does not work... 1 command only. And then nothing works, neither the typing in console, neither the text area ---> bad UX... Terrible. Not functional. Just stream me stuff 1 way only... No extra noise... Only the stuff I need to see."
Weeks went into making a real xterm work in a browser -- pty, websocket, resize, image paste, scrollback replay, transcript resume, tmux ownership so it survived a restart. Every one of those was a correct fix for a real bug, and the product was still wrong. A terminal is a thing you TYPE into, and there is already a perfect one on the laptop; `tmux attach -t board` joins the very same session.
So the pane stops rendering a terminal. `fleet/bin/stream.py` reads Claude Code's own transcript -- structured, no escape sequences -- and emits three kinds of line:
you what was asked claude what was answered · one grey signpost per tool, so the gaps are explained
Everything else is dropped on purpose: thinking blocks, tool results, system notices, attachments, the spinner, the tmux status bar. That is the "no extra noise" part and it is the whole feature.
Acceptance criteria, one test each: 1. nothing on the page can put a keystroke into the machine 2. only those three kinds of line 3. thinking, tool results and system rows never appear 4. scrolling up to read is not undone by the next poll 5. the script parses -- `node --check`, not hope
Verified on the live page: 120 lines (13 you, 43 claude, 64 tool), zero inputs in the pane, scroll held after a poll, no pane in an error state.
/terminal 301s to /. `termview.py` moves to the dormant drawer with its reasoning; `test_terminal_pane.py` and eleven other tests describing the removed behaviour go with it.
1061 passed.
10 files
- fleet/bin/fleet.py
- fleet/bin/oneview.py
- fleet/bin/stream.py
- fleet/dormant/termview.py
- tests/test_board_micro_ux.py
- tests/test_terminal_pane.py
- tests/test_terminal_says_why_it_ended.py
- tests/test_terminal_survives_a_reload.py
- tests/test_the_board_pane_watches.py
- tests/test_the_stream_is_one_way.py
2026-09-0424 commits
board: the partnership rows are domain | github
fca9213 · 23:22 · Marsita the Ultra
Marsita, 2026-09-04: "Just word 'github' (no need to link fully)... link to basex.com main and full url independent trib .org and pla council.org (with their respective domains)."
The left half is the domain now, not a prose name: "planetary council" is something you have to recognise, "planetarycouncil.org" is something you can type. The right half is the bare word "github" -- repeating github.com/PlanetaryCouncil after it spent a whole line saying where GitHub is.
basex points at basex.com rather than demo.basex.com. The demo moved into the hover text, where "break it and say what broke" still lives.
planetarycouncil.org | github independenttribunal.org | github basex.com | github
1074 passed.
2 files
- fleet/bin/oneview.py
- tests/test_the_board_pane_watches.py
board: a chevron in the corner, not a drag
d534732 · 23:15 · Marsita the Ultra
Marsita, 2026-09-04: "Cannot go all the way down... And then can go too much up... just ^ ^ chevron up and down in the right upper corner of footer section. No need to drag, just full min, or restore to default height."
She is right, and the range bugs were the symptom rather than the fault. A drag offers a hundred heights when only two are ever wanted, and then has to get both ends of its own range right, which it did not. Two states need no range at all.
So: a chevron top-right, pointing where it will take you -- up to hide, down to show, and the same in words for anyone not reading arrows. Shut, the eight headings are gone and the single word "footer" is left, which is also a way back. No --hFoot, no pointer capture, no drag.
Also here, and the more useful fix: **the board's JavaScript is now checked with a real engine.** A stray `} catch(e){}` left by an edit killed the ENTIRE script -- every handler below it silently never attached, and the page looked completely normal. Nothing on the Python side could catch that: `ast.parse` is happy with any string. `node --check` on the JS block would have.
Verified in the browser: 155px open, 19px shut, chevron flips both ways, the word reopens it.
1078 passed.
2 files
- fleet/bin/oneview.py
- tests/test_the_board_pane_watches.py
board: the footer bar sits on the boundary, and actually drags
85cede3 · 22:30 · Marsita the Ultra
Marsita, 2026-09-04: "line should be exactly at the boundary (no extra gap)... change cursor to be proper /// please fix the moving part as well, still not moving."
Three separate faults in one 3px bar.
The gap: the footer had 10px of top padding, so the handle floated 13px below the border it was meant to be sitting on. No top padding now, and the bar is full bleed to both edges.
The cursor: it said `pointer`. It looks exactly like every other divider on this board, and those drag, so it now drags -- and says `row-resize`, because a hand on a thing you drag is a lie about what it does. Shut, there is nothing left to size, so it goes back to `pointer`.
The drag doing nothing: `max-height` can only ever SHRINK a box, and the footer already fit in less than the number being set, so pulling upward changed nothing. It sets `height` now. And pointerup saved the height it MEASURED rather than the one it set -- the box can be shorter than what it was asked for -- which turned each drag into a no-op that also forgot itself.
One bar, two jobs: moved more than 3px is a resize, moved less is a collapse. Two 3px targets stacked on one boundary would have been worse.
Verified in the browser: 168 -> 238 dragging up, -> 148 dragging down, saved at 148, and a still click collapses and reopens.
1078 passed.
2 files
- fleet/bin/oneview.py
- tests/test_the_board_pane_watches.py
terminal: one owner of a session's tmux options
64a8efc · 21:44 · Marsita the Ultra
`configure_tmux` set mouse and history-limit; `Session.__init__` set the window size through a second path of its own. Two owners of one session's setup is two chances to race its startup, and the second one broke `attach` for any caller that hands it something other than a real Session -- which is what the tests do, and what a 500 on the board would have looked like.
So every option lives in `configure_tmux` now: mouse, history-limit, window-size largest, aggressive-resize. It already waits for the session to exist before setting anything, which is the part `__init__` could not do.
`attach` reads `tmux_name` with getattr. A page must not fall over because of an attribute.
1072 passed.
2 files
- fleet/bin/terminal.py
- tests/test_the_board_pane_watches.py
board: the footer's handle is a bar, not the word "footer"
3cf9581 · 21:39 · Marsita the Ultra
Marsita, 2026-09-04: "the blue 'grab handle' does not appear... No need to call it 'footer'... Only when collapsed all the headers become unified into a single 'footer'."
Open, it is now the same blue divider every other pane on this board uses, and it says nothing -- a word reading "footer" directly above a footer is a label for something already obvious. Shut, the eight section headings are gone and that one word is the only thing left, which is the moment it earns its name and the only way back in.
4 files
- fleet/bin/oneview.py
- fleet/bin/terminal.py
- tests/test_the_board_pane_watches.py
- tests/test_the_pane_scrolls.py
issues: eighteen things in eight places, on one screen
ccc1de0 · 20:59 · Marsita the Ultra
Marsita, 2026-09-04: "I should work with issues on my other projects... Currently too much just me in terminal with Claude", then honestly: "I need to get disciplined to be creating issues and then sorting them out."
Discipline is the wrong lever. There were eighteen open issues across eight repositories and none of them moved, and the reason was not willpower: seeing them meant eight browser tabs and filing one meant leaving the terminal. Both are friction, and friction is fixable in a way that discipline is not.
fleet/bin/issues.py one `gh search` for every owner at once -- 1.8s, not eight round trips -- cached five minutes, and served STALE rather than empty when gh fails, because an empty pane reads as "you have no work" fleet/bin/issue `issue ux "the nav wraps"`. Short repo names are resolved by asking GitHub which owner has them, so it cannot file into the wrong account the pane left column, under `work`. Repo tag, title, age in days -- a date needs arithmetic, an age makes you wince. Amber at 14 days, red at 30
Local only: an open issue is a to-do list, and the board publishes what the fleet DID, not what it has not got round to.
Also here, because it surfaced while building the above:
- the per-tab pty sizing work and its seven tests, recovered after a stray `git stash pop` of a two-day-old stash left the tree half-merged - the tmux size options move off __init__ into a thread. Two inline subprocess calls delayed the end of __init__ by seconds, and a session created just after the previous one was killed then attached to the dying session instead of a new one. Cosmetic sizing must never be on that path.
23 new tests; 1064 passed.
8 files
- fleet/bin/fleet.py
- fleet/bin/issue
- fleet/bin/issues.py
- fleet/bin/oneview.py
- fleet/bin/terminal.py
- tests/test_issues_in_one_place.py
- tests/test_tabs_do_not_fight_over_width.py
- tests/test_the_board_pane_watches.py
board: the footer shuts, and a project fits on one line
c81adc0 · 19:55 · Marsita the Ultra
Marsita, 2026-09-04: "I would like to collapse footer links... And when it is collapsed it just says 'footer'."
Eight columns of links that are useful twice a week sat permanently under the pane actually being read. Now the whole footer collapses to the single word "footer", which is both the label and the handle -- so there is no state where the links are gone and nothing says how to get them back. Remembered in the same layout key as every other collapse.
"prefer less vertical" -- a project and its source are one fact, so they get one line now: "planetary council | github.com/PlanetaryCouncil". Six stacked rows, each with a leading arrow, became three. The section spans two columns, because full repo URLs were being ellipsed in a one-eighth column.
7 new tests; 1041 passed.
5 files
- fleet/bin/oneview.py
- fleet/bin/terminal.py
- tests/conftest.py
- tests/test_the_board_pane_watches.py
- tests/test_the_suite_ignores_its_shell.py
board: scroll back, fill the pane, collapse the footer
cf9800a · 19:33 · Marsita the Ultra
Three things Marsita asked for while looking at the pane.
"I also want to scroll up on the Claude messages back" -- the attach used to send \x1b[3J, which clears the browser's SAVED lines, so the page arrived with nothing above the fold and the wheel did nothing. tmux owns the scrollback, so ask it: `capture-pane -S -3000 -E -1` is the history region only, ending exactly where the repaint begins, so no screen appears twice.
"loads of empty space... claude should take more" -- tmux sizes a window to its SMALLEST client, and with the laptop attached alongside, the smaller one clamped the browser: a status bar two-thirds up the pane and dead black below. Now `window-size largest`, so the big viewer sets the size. Not `latest`: this pane is read-only, so it is never the active client and would never win.
"the footer pane to be fully collapsible" -- shut, the stream's heading now shrinks to a hairline instead of a full bar. Still visible, still clickable, because a pane with no way back is a pane you deleted.
9 new tests; 1034 passed.
3 files
- fleet/bin/oneview.py
- fleet/bin/terminal.py
- tests/test_the_board_pane_watches.py
board: the compose box says nothing but "..."
0704cfe · 19:27 · Marsita the Ultra
Marsita: "please skip the placeholder... The only placeholder is '...' (I know how it works)."
Both boxes -- the board's termpane and the terminal page -- were explaining Enter and Shift+Enter to the one person who has used them every day. That is text to skip, forever, in front of a box she was already going to type in.
3 files
- fleet/bin/oneview.py
- fleet/bin/termview.py
- tests/test_the_board_pane_watches.py
board: the terminal pane stops pretending to be a terminal
cae77ee · 19:16 · Marsita the Ultra
Marsita: "gaia local terminal ----> one way display ----> and me typing in text area for native feel."
The screen is now read-only -- `term.onData` is gone and `disableStdin` is on, so no keystroke reaches the pseudo-terminal from the page. The compose box is the only way in, which is what made it feel native in the first place: local DOM, no round trip per character. The laptop terminal drives, via the same tmux session (`tmux attach -t board`).
Above the screen, the one question the pane exists to answer: working, waiting for you, or idle; an elapsed clock; an estimate from the median of recent turns, labelled as the guess it is; and a separate shout past 2x that guess, because a number that just climbs never tells you when to walk over.
State comes from `tmux capture-pane`, not from gaps in the output -- a model thinking silently for two minutes is still working. One poller per session however many tabs are open, so a turn is never recorded into the history twice.
Image paste survives and now always lands in the box.
15 new tests; 1025 passed.
4 files
- docs/NEXT-TERMINAL-IS-A-STATUS-PANE.md
- fleet/bin/terminal.py
- fleet/bin/termview.py
- tests/test_the_board_pane_watches.py
report: 2026-09-04 fleet summary
f6864c5 · 19:00 · Marsita the Ultra
Generated by fleet/bin/report.py. Counted from the files the fleet writes while working, not written by hand.
4 files
- docs/report/index.html
- docs/report/report.html
- docs/report/report.json
- docs/report/report.md
a branch nobody reviewed comes back; the reviewer only fact-checks
4f23b43 · 16:10 · Marsita the Ultra
reopen was already a stage the pipeline understood and nothing ever wrote it. A verify where no reviewer answered recorded a rejection, so the proposal closed forever -- two branches sat rejected with '991 passed' beside them while every reviewer on the roster was failing on an auto-denied tool. verify now records reopen instead and the proposal is waiting again next slot. Bounded at two: a third rings needs_you, because by then the roster is broken, not the branch.
The reviewer's job is now what Marsita asked for -- 'Fable proposes, rest signoffs? Basic fact-checking layer'. The builder is a frontier model and the tests have already run, so the brief asks only for things that are objectively wrong and says plainly that style and taste are not its call.
1010 passed.
2 files
- fleet/bin/pipeline.py
- tests/test_the_review_gate_answers.py
setup-grokbot: an apostrophe in a ${VAR:-default} ate the last line
96def20 · 12:49 · Marsita the Ultra
The run succeeded: user created, groups right, home 0750, xfce set, xrdp up and listening on 3389, chrome present. Then:
FAILED at line 101 (exit 1): cat <<TXT bad substitution: no closing } in ... ${IP:-<the NUC's Tailscale IP>}
Inside ${VAR:-default} bash still applies quoting rules, so the apostrophe in "NUC's" opens a quote it never closes and swallows the brace. The heredoc died on the last line of the script -- and it was carrying the only copy of the generated password.
Plain variables now, with the fallback computed on its own line. And a re-run resets the password instead of leaving the account alone, because a password that is only ever printed is unrecoverable when the printing is what broke.
1 file
- fleet/bin/setup-grokbot.sh
setup-grokbot: the password line was a SIGPIPE trap
f4c0b29 · 12:45 · Marsita the Ultra
FAILED at line 49 (exit 141): PASSWORD="$(tr -dc 'A-Za-z0-9' < /dev/urandom | head -c 20)"
141 is SIGPIPE. head closes the pipe at twenty bytes, tr dies reading an infinite source, and set -o pipefail fails the line. The obvious one-liner for a random password is wrong in any script with pipefail on.
Bounding the source instead -- head -c 400 /dev/urandom | tr | head -c 20 -- lets every stage end on its own. Verified three runs under set -euo pipefail.
The ERR trap added one commit earlier is what found it; two runs before that died at the same line in silence.
1 file
- fleet/bin/setup-grokbot.sh
setup-grokbot: say which line failed
a4e862e · 12:43 · Marsita the Ultra
Two runs died at '== user' printing nothing at all. set -e exits at the failing command and is silent about which one; over ssh stdout and stderr are separated too, so a message on the wrong stream reads as silence.
An ERR trap now names the line, the command and the exit code, and the whole script writes to one stream.
1 file
- fleet/bin/setup-grokbot.sh
setup-grokbot: useradd, not adduser
34a914a · 12:39 · Marsita the Ultra
First run died at '== user' having printed no error. adduser is a Perl wrapper that asks for confirmation even with --disabled-password; run over ssh without a terminal it read EOF and exited under set -e. useradd takes every answer as a flag and never asks.
Also 'exec </dev/null' at the top -- nothing in a script run over ssh may wait for a human.
1 file
- fleet/bin/setup-grokbot.sh
setup-grokbot.sh: a desktop for the agent, no sudo
c72b888 · 12:37 · Marsita the Ultra
GrokBot lives in a datacenter and needs to browse from a residential line, driving a real Chrome it can see. Marsita weighed the IP-reputation cost and accepted it.
xrdp rather than RustDesk: RustDesk shares the one physical seat that James is already on, xrdp gives each user an independent X session.
Refuses to finish if the account ends up in sudo. Home is 0750 so m's files are unreadable from that session and back. Generates the password once and prints it with the connection details. Nothing here installs a proxy -- the box is not an exit node for anyone else's traffic.
Written for Marsita to run because the auto-mode classifier blocks remote systemd and useradd over ssh.
1 file
- fleet/bin/setup-grokbot.sh
an error is not a proposal: stop filing the drama
437163f · 11:11 · Marsita the Ultra
Fable 5.1 triaged its own backlog and named the source. rota.py classifies every turn as failed / unusable / nothing / proposed at line 345, then wrote the row unconditionally three lines later for autotriage to drop afterwards.
That is how the ledger reached 4,886 rows of which twelve were waiting:
547 rows "[unknown agent codex]" a roster name with no adapter 263 rows HTTP 402 Payment Required a vendor out of credit ~170 rows "Here are my answers to the three questions..." an agent reading rota's own prompt heading back instead of the board
Three changes, all at the source: - the ledger write is guarded by . The ev.emit warns are untouched, so a failure stays visible on the board and simply stops queueing as work. - NARRATION gains the rota's own prompt heading, so the echo is classified unusable rather than filed as a proposal. - dispatchable() drops roster names ask() cannot reach, before the quota check rather than after a wasted turn.
Fable's own verdicts on the twelve waiting rows: 4 BUILD, 8 KILL. Two of the kills were its own proposals from last night, already landed in d5b9f12.
test_a_failed_turn_does_not_pin_the_skip reversed deliberately -- it asserted the error row was written. The property it guards (a failed turn does not pin the board skip) is unchanged.
1004 passed.
3 files
- fleet/bin/rota.py
- tests/test_rota_board_gate.py
- tests/test_the_ledger_is_a_queue.py
silence is not a rejection, and the ledger is a queue not an archive
4b8a6ef · 10:18 · Marsita the Ultra
Nothing merged 2026-09-01 to 2026-09-04. The tests passed the whole time -- "991 passed" sits beside every failure in pipeline.jsonl. The reviewer was what failed, three ways:
"no reviewer produced a verdict" "[stderr] jetski: no output produced -- a tool required the 'command' permission that headless mode cannot prompt for, so it was auto-denied" "I'll load the brief and inspect the merged change against the surrounding fleet-model wiring."
Empty, tool-denied, and a preamble from an agent about to start. All three scored REJECT by .startswith("APPROVE"), blaming the branch for the roster.
verdict_of() now returns True/False/None and scans for the word anywhere. A None asks the next independent vendor instead of failing the branch. The brief tells the reviewer it already has the whole diff and that a tool call is auto-denied.
Ledger: of 4,885 rows only 12 were unprocessed. The rest was finished work, 823 of it errors and 547 identical rows from one agent repeating a single failure -- read as a backlog by everything that opened it. trim-ledger.py moves settled rows older than three days to an archive beside the live file. A row is archived only if it is BOTH processed AND old; unprocessed work is never moved, however ancient.
999 passed.
5 files
- fleet/bin/health.sh
- fleet/bin/pipeline.py
- fleet/bin/trim-ledger.py
- tests/test_six_units_not_eleven.py
- tests/test_the_review_gate_answers.py
fable is in the pool, and the slot runs current code
5ddd797 · 03:13 · Marsita the Ultra
Fable 5.1 audited its own fleet and found that making it the orchestrator had been theatre. backlog.sh sets FLEET_BUILDER from next_builder.py, whose POOL was ["grok", "agy"] -- a name absent there never builds, however pipeline.builder_name() reads. claude now leads the pool, and its spend table entry goes rare -> plenty so eligible() stops skipping it in favour of an exhausted vendor.
backlog.sh now fast-forwards the checkout before choosing. pipeline.py already built off origin/main via remote_main(), but next_builder.py, autotriage.py and backlog.sh itself are read from the working tree, so the NUC was picking builders with a copy that predated tonight's change. --ff-only and only on a clean tree, so a checkout with work in it is left alone.
989 passed.
4 files
- fleet/bin/backlog.sh
- fleet/bin/next_builder.py
- fleet/config.json
- tests/test_the_orchestrator_seat.py
the 09:00 message becomes a generated unit, not an orphan
0eaf90c · 03:09 · Marsita the Ultra
fleet-daily ran for weeks as a hand-written systemd unit apply-config knew nothing about -- the same shape as the three fleet-build@ instances that pinned six cores. It survived the collapse only because nothing generated it, which is the bug, not the reason to keep it.
Not merged into report: one is pushed to a phone at breakfast, the other is a page that waits until evening. Seven generated units, and Marsita asked for six or seven.
daily-summary.sh replaces two ExecStart lines in one unit -- an ordering assumption (--publish before --send) that systemd hid and a script says out loud.
987 passed.
4 files
- fleet/bin/apply-config-systemd.sh
- fleet/bin/daily-summary.sh
- fleet/config.json
- tests/test_six_units_not_eleven.py
eleven timers become six, and two of them were the same command
2bb3c7c · 02:43 · Marsita the Ultra
Marsita, looking at her own org chart: "seriously I have such a super duper architecture? [...] watchdog = medic = heartbeat = same same [...] if we could get down to 6 or 7 would be great."
Worse than the chart showed -- fleet-pipeline and fleet-build both ran pipeline.py run, so two timers raced for one worktree.
rota the loop, unchanged build backlog.sh, absorbs pipeline council council.py + self-improve once a day (council-cycle.sh) health board-medic + watchdogs + heartbeat (health.sh, stamp-gated) e2e daily, the only thing that can say the fleet still works report the daily page + the local-voice pulse (report-cycle.sh)
The differing cadences survive: health runs on the fastest of the three and gates the slower two behind stamp files written BEFORE the work, so a hang cannot restart itself on the next tick.
apply-config now retires what it stopped generating. A unit no longer written but still enabled keeps firing, which is how three hand-written fleet-build@ instances outlived everyone's memory of creating them.
Autonomous merge was already live -- land() runs unconditionally on an approved branch -- and is now pinned by a test so it cannot quietly revert.
987 passed.
7 files
- fleet/bin/apply-config-systemd.sh
- fleet/bin/council-cycle.sh
- fleet/bin/health.sh
- fleet/bin/report-cycle.sh
- tests/test_six_units_not_eleven.py
- tests/test_the_daily_report.py
- tests/test_the_orchestrator_seat.py
fable 5.1 leads the pack, and one builder runs at a time
d5b9f12 · 02:29 · Marsita the Ultra
The Claude plan was never dead. The Anthropic API refuses hermes and Copilot lists thirty models it will not serve -- both true, both about the raw API. The `claude` CLI runs on the subscription and was working on the NUC the whole evening:
$ claude --print --model claude-fable-5-1 "reply with exactly: ok" ok
So the orchestrator seat is free. chat.claude_argv() is now the one place that builds a `claude --print` invocation, pipeline uses it, and the default builder goes back to claude after being demoted to grok on a wrong belief. FLEET_CLAUDE_MODEL overrides it; empty passes no --model at all, for a box with an older CLI.
The cadence fix is structural. Three hand-written fleet-build@ instances on a 30s gap with a 4h timeout pinned six cores for a day and built nothing for three, because each slot queued behind the last. Now one generated unit, OnUnitInactiveSec so slots cannot overlap however long one runs, and max_seconds bounding a hang -- all from config.json, so re-running apply-config fixes either machine.
977 passed.
6 files
- fleet/bin/apply-config-systemd.sh
- fleet/bin/apply-config.sh
- fleet/bin/chat.py
- fleet/bin/pipeline.py
- fleet/config.json
- tests/test_the_orchestrator_seat.py
nuc brief: GrokBot's login in full, and why hermes has no cloud brain
efdb593 · 02:04 · Marsita the Ultra
GrokBot is a datacenter agent that needs a real Chrome on a residential line. Marsita weighed the IP-reputation cost and accepted it. Written down so the NUC does not re-litigate a settled decision.
Also records that both Anthropic and Copilot refuse hermes, verified by hand with curl -- neither is a hermes bug and neither is an auth failure.
1 file
- docs/NUC-TASK-2026-09-03-lean-the-box.md
2026-09-0315 commits
nuc task: lean the box — kill the builder loop, ollama daily, GrokBot login
28040c4 · 22:16 · Marsita the Ultra
The builders were re-firing every 30s with a 4-hour timeout, six hermes calls into a 5 t/s CPU model. That is the 8GB and the six cores, and it is why the pipeline has built nothing since 2026-09-01.
Written for a Claude running on the NUC: Gaia can read that box but the auto-mode classifier blocks remote systemd writes.
1 file
- docs/NUC-TASK-2026-09-03-lean-the-box.md
next: the board terminal stops pretending to be a terminal
53a7a38 · 21:27 · Marsita the Ultra
Marsita, 2026-09-03: "This terminal in the browser is shite... It won't be very interactive. It will just give me when something is finished, because that's actually the most important thing... I have a terminal on my computer, so it's maybe just perfect. Best of both worlds."
Weeks went into making a real xterm work in the browser and every fix was a real fix, but the whole thing was the wrong product: typing into a browser terminal is worse than typing into a real one and always will be. What the browser is better at is being glanceable from across the room.
So: state, elapsed timer, effort estimate, a notice when a turn runs to twice it, and a big send-only compose box. tmux ownership stays -- it is what makes the pane and the laptop's terminal the same session.
Written down rather than built: asked for at the end of a long session, about to compact. A design decided in one sentence and built from a summary is how the last three UI reversals happened.
1 file
- docs/NEXT-TERMINAL-IS-A-STATUS-PANE.md
a reattached page gets one screen, not the whole history
23232bb · 19:22 · Marsita the Ultra
"Currently I need to reload the browser tab with every message. Not practical."
The board replayed its byte buffer to every arriving page. But that buffer is a LOG, not a snapshot -- it holds every redraw the session ever emitted. Replaying it did not restore the screen, it re-rendered the entire history: the same conversation stacked several times down the page, two tmux status bars, and the cursor left wherever the last replayed frame happened to put it. Live output then arrived somewhere the reader was not looking, so the pane appeared frozen and only a reload fixed it -- by stacking a fresh set of copies.
tmux already holds the one true screen, so ask it for that instead: clear what the browser has, then `refresh-client`. Exactly one screen, at the right size.
The repaint waits for the browser's first resize. tmux sizes a window to its smallest client, so drawing on connect draws at whatever size the previous viewer had and hands the new page a layout cut for someone else's window.
Without tmux there is nothing to ask and the byte log is still the best available answer, so that path is unchanged.
Also: `commits` in the footer goes to GitHub, where commits live and can be shared. The local reading of the same history is `changelog` beside it.
6 files
- fleet/bin/fleet.py
- fleet/bin/oneview.py
- fleet/bin/terminal.py
- fleet/bin/work.py
- tests/test_reattach_paints_one_screen.py
- tests/test_work_pane.py
report: 2026-09-03 fleet summary
011933a · 19:00 · Marsita the Ultra
Generated by fleet/bin/report.py. Counted from the files the fleet writes while working, not written by hand.
4 files
- docs/report/index.html
- docs/report/report.html
- docs/report/report.json
- docs/report/report.md
the board knows what the repo is doing
1296497 · 18:51 · Marsita the Ultra
Every pane described the fleet. None described the work. Ten commits landed today and no pane on the board knew it — the one question you ask when you sit down was the one question the board could not answer.
So read git, not a status file. A status file is a claim somebody has to remember to update, and it goes stale the moment the work moves. git already knows: branch, unpushed count, what is uncommitted, what landed today, and the files you kept going back to this week. Subjects say what you meant to do; the file histogram says what you actually did.
Landed history is on GitHub already, so a public viewer gets it. The uncommitted tree is not published and does not become published by appearing on a board, so it is local-only and absent — null, not zero — for everyone else. A stranger told "0 uncommitted" would be reading a claim about a tree they were never shown.
Column 1 of porcelain status is a SPACE for an unstaged change. Stripping the output shifts every path left by one and the pane reads "leet/bin/fleet.py". Parsed by column offset now means parsed unstripped, and a test holds it there.
4 files
- fleet/bin/fleet.py
- fleet/bin/oneview.py
- fleet/bin/work.py
- tests/test_work_pane.py
a daily report, and the three orgs beside the three sites
748f08a · 18:06 · Marsita the Ultra
The fleet runs all night and the only record was a stream nobody scrolls back through. Today surfaced three failures -- nothing built for two days, the heartbeat dead for 31 hours, the watchdog tier never once run on the NUC -- each visible in some log, none visible anywhere a person looks. A report is how a quiet failure becomes a loud one.
report.py counts from the files the fleet writes while working and renders the same facts three ways: text for a terminal, Markdown, and a standalone page. A day where nothing happened says so rather than going quiet -- a report that only appears when things went well is an advert.
Served at /report, /report.json and /report.md on both boards, publicly: a summary only the operator can read never gets checked. Published daily at 19:00 to this repo's own Pages, because the report of the dashboard belongs to the dashboard.
And each site now carries the org that builds it -- PlanetaryCouncil, independenttribunal, basexhq -- verified against the GitHub API rather than copied from a note. A claim about building a civilisation is cheap; the source is the part that can be checked.
8 files
- fleet/bin/apply-config-systemd.sh
- fleet/bin/apply-config.sh
- fleet/bin/fleet.py
- fleet/bin/oneview.py
- fleet/bin/publish-report.sh
- fleet/bin/report.py
- fleet/config.json
- tests/test_the_daily_report.py
report: 2026-09-03 fleet summary
46d53d4 · 18:02 · Marsita the Ultra
Generated by fleet/bin/report.py. Counted from the files the fleet writes while working, not written by hand.
4 files
- docs/report/index.html
- docs/report/report.html
- docs/report/report.json
- docs/report/report.md
the board terminal outlives the board
20a4f3d · 16:06 · Marsita the Ultra
The board pty.forked claude directly, so the board process was its parent and every restart killed the session. Reloads survived -- a pty outlives one websocket -- but restarts did not, and the board is restarted every time it is improved: twelve times on 2026-09-02, each landing on whatever Marsita was mid-way through. "the board terminal needs to stay alive ---> that's the point".
tmux owns the process now and the board only draws it. `new-session -A` is the whole mechanism: attach if it exists, create if it does not. The first connection starts claude; every later one joins it -- after a reload, after a restart, or from a real terminal with `tmux attach -t board`. tmux redraws on attach, so the screen comes back with it.
A tmux session that outlived the board reports as a resume rather than a fresh start, because that is the fact. Closing a socket still only detaches; only end() kills the session behind the viewer. Without tmux installed it behaves exactly as before -- a board that refuses to open a terminal is worse than one whose terminal is fragile.
5 files
- CONTRIBUTING.md
- fleet/README.md
- fleet/bin/terminal.py
- tests/test_terminal_continue.py
- tests/test_the_terminal_outlives_the_board.py
retire /intro to the dormant drawer
6a70f47 · 14:58 · Marsita the Ultra
It was written as "the page you send someone", back when the board was an instrument and nothing else. Since then the board grew a FIRST CONTACT banner for strangers, a welcome row with the links, and its own goal chain -- and /about already explains the system in plain language. What was left was a second copy of the dashboard with a join box on top. Marsita: "it does nothing other than duplicating dashboard".
/intro now 301s to /about rather than 404ing: the path is in the wild, in old messages and whatever anyone bookmarked, and a dead link is a worse answer than the right page. One fewer tab in the nav. homeview.py moves to fleet/dormant/ with a note saying why, so the join-box wording stays readable instead of living in a diff.
The name-consistency test checked the homepage as a public surface; it checks the board now, which is what a stranger actually lands on, case-insensitively -- the banner is set in caps by design.
7 files
- fleet/bin/fleet.py
- fleet/bin/nav.py
- fleet/bin/oneview.py
- fleet/bin/sitemap.py
- fleet/dormant/homeview.py
- tests/test_no_local_addresses_in_public_text.py
- tests/test_sitemap.py
a silent probe is not a verdict, and the words say what they mean
8374904 · 14:15 · Marsita the Ultra
Marsita, reading their own board while talking to Claude through it: "why claude says logged out if I'm talking with you here?"
`claude auth status` answers in well under a second on an idle box. Gaia runs at a load of 4-7 on four cores and PROBE_TIMEOUT is 8s, so it overran; run() returned "timeout", json.loads raised, and the handler fell through to logged-out -- reporting a live, paid session as signed out. hermes already had this fix; claude did not, which is why one bug read as two.
A probe that did not answer is now "answer unknown" and not counted as down, and its raw output is kept locally -- never in public_row, because that command prints an email address -- so a failure can be reported rather than described from memory.
Words, per Marsita: DRY -> out of credits, absent -> no answer, unknown -> answer unknown. And the legend I added unasked is gone: a paragraph explaining a column is a column that failed to explain itself.
4 files
- fleet/bin/oneview.py
- fleet/bin/quotas.py
- tests/test_a_silent_probe_is_not_a_verdict.py
- tests/test_credit_pane_does_not_guess.py
an ask is a post, under the name that wrote it
a5a05b7 · 13:57 · Marsita the Ultra
Three things were wrong at once. The board filed every ask as "[council] operator asked" -- a role nobody chose, including Marsita's own question: "it was ny dude, not operator... I used ask". The ask was also echoed into a separate YOU ASKED strip above the composer, a second surface for something the stream already carried. And the pad kept its drawing afterwards, so the next message went out wearing the previous one's hand.
An ask now lands like any other post -- "[signals] <name>: <text>" -- which is also the shape the stream reads a sender out of, so the signature gets drawn beside it. The strip is gone. The pad resets.
The privileged button says what it is: "as admin" rather than "ask".
7 files
- data/events.jsonl
- data/inbox.json
- fleet/bin/fleet.py
- fleet/bin/oneview.py
- fleet/data/council-question.md
- tests/test_operator_question.py
- tests/test_the_ask_is_a_post_like_any_other.py
a signed post shows its words, and the hand that wrote them
517db5f · 12:32 · Marsita the Ultra
The board rang for a hand-signed stranger with "a hand-signed hello from 'someone' — on the board": an announcement that a person had spoken, without saying what they said. Two things broke at once, because the stream reads the sender out of "[signals] <sender>: <text>" -- a differently-shaped line matched nothing, so the signature was never drawn beside it either.
Every signed post now rings in that one shape. The words appear, the mark lookup finds the sender, and the hand is drawn at 110x26 next to what it signed. Provenance goes after the body, never before, or it would become the key the signature is looked up by.
This reverses a deliberate rule -- unreviewed text gets no second doorway -- and the reversal is the point: a node signature or a living hand costs something to produce, and that cost IS the review. An unsigned stranger is still only a count until triage.
3 files
- legacy/app/main.py
- tests/test_a_signed_post_shows_its_words.py
- tests/test_doorbell.py
the heartbeat repair now speaks systemd too
9a6c94a · 12:22 · Marsita the Ultra
revive-heartbeat.sh exists because detection without repair is useless: in August agent-comms recorded nothing for 19 hours while the board said green. The fix restarted the job through launchd, because Gaia is a Mac.
Both machines run this script. The NUC's agent-comms had been dead for 31 hours and the hourly watchdog had been logging "kickstart of re.genesis.comms-heartbeat FAILED" once an hour the whole time. Detection worked perfectly; the repair called launchctl on a systemd box. The exact failure this file was written to prevent, reintroduced by assuming one machine's scheduler.
It now tries whichever scheduler is present, in turn rather than by branching on uname, and says which one actually did the restart.
2 files
- fleet/bin/revive-heartbeat.sh
- tests/test_repair_speaks_both_schedulers.py
watchdogs: a missing per-machine list is not a reason to do nothing
39cd3c1 · 12:17 · Marsita the Ultra
fleet-watchdogs had been failing on the NUC every hour since the box was re-cloned. projects.txt is gitignored -- it holds absolute paths and those differ per machine -- so a new machine has none and the script exited 1 before doing anything. systemd recorded a permanently failed unit, and nobody reads `systemctl --failed`.
A machine running the fleet always has the fleet's own repo, and that repo has a test suite. So the script defaults to it, writes the file down so it can be edited, and says what it did.
2 files
- fleet/bin/run-watchdogs.sh
- tests/test_watchdogs_do_not_fail_silently.py
credit pane: a daemon that answers is not absent, and say what the words mean
0dff0f5 · 12:04 · Marsita the Ultra
The NUC's ollama was serving a model list on 11434 while its row read "absent", because a PATH lookup for the binary fails inside the systemd unit's environment and the binary check ran before the liveness one. A daemon that answers is stronger evidence than a `which` that does not.
Six states is a lot for one column and Marsita counted them. They each carry a different instruction -- spent and waiting, needs a keyboard, never measured, on a plan, never had a bill, nothing there -- so the fix is not fewer words but saying what they mean, once, under the pane that uses them. A test pins that every word the column can print appears in the legend.
4 files
- data/events.jsonl
- data/inbox.json
- fleet/bin/oneview.py
- tests/test_credit_pane_does_not_guess.py
2026-09-0222 commits
routine is not an emergency, and the operator is not a visitor
d5c92e5 · 23:44 · Marsita the Ultra
A vendor spending its allowance is weather. It happens on a schedule, it clears on a schedule, and eligible() routes around it unasked. It was firing needs_you and painting the pane red, which is the shape of an alert with no action -- and an alert with no action teaches you to stop reading the channel a real outage will use. Dry is now a warning, in amber, and is excluded from needs_you. Logged-out still shouts, because that needs hands on a keyboard, and so does a lost quorum.
The welcome row goes back to remote-only. It was restored this morning for the operator and cut the same evening -- a permanent line explaining the fleet to the person who built it is an advert aimed at the wrong reader.
7 files
- fleet/bin/oneview.py
- fleet/bin/quotas.py
- tests/test_board_micro_ux.py
- tests/test_credit_pane_does_not_guess.py
- tests/test_intro_offers_the_gallery.py
- tests/test_quotas.py
- tests/test_routine_is_not_an_emergency.py
board: five small things that made it tiring to sit in
194b910 · 21:01 · Marsita the Ultra
A red bar with no X is a red bar you learn to stop seeing, so the alarm closes now -- keyed on the message rather than a flag, so silencing "grok is dry" leaves the next different thing free to shout.
The line saying what this place is was dismissible, and dismissal was permanent: one click and the sentence explaining the whole system was gone for good, on every future visit. It is one row and the board can afford it.
The stream was the only pane without a name; it has one, and the filter pills go away when it collapses, since they filter a list nobody can see.
And the terminal has a composer -- plain HTML, outside xterm entirely -- so a long message is written somewhere the agent's output cannot reach. Enter sends, shift+enter is a newline, and the text goes as one bracketed paste so a multi-line message arrives as one turn rather than several half-written ones.
2 files
- fleet/bin/oneview.py
- tests/test_board_micro_ux.py
tests: stop the suite posting to the live board
a3a36ea · 18:31 · Marsita the Ultra
Two contradictory lines were arriving on the stream every couple of minutes: "[local] llama3.2:1b answered in 0.0s -- the offline fallback is alive" and "failed in 0.0s -- NOT responding", stamped the same second, from a model that had not been asked anything since 19 August. They were the two localvoice test cases -- one passing, one failing -- landing on the real board every time the hourly test worker ran. 0.0s because the model was mocked.
fleet/bin/events.py binds its log path at import from its own __file__, so it ignored both the fixture's FLEET_PATH and any env var set afterwards. Every fleet module a test touches could ring the board. The fixture now patches the constant, not just the environment.
Same class of bug, found on the way: append-only ledgers were seeded from the live files, so six brainfart tests asserting on their own writes broke when an agent filed a real entry at 17:00. Those start empty now.
Purged 208 fabricated localvoice lines from the stream.
2 files
- tests/conftest.py
- tests/test_the_suite_leaves_the_board_alone.py
board: dividers stay inside the grid, and match each other
4614282 · 16:25 · Marsita the Ultra
A sized pane was handed the whole column, but the divider and the collapsed neighbour's heading live in that column too, so dragging pushed the pane's bottom below the grid. It now measures the room it actually has, and the column clips whatever the arithmetic still gets wrong.
Horizontal dividers were 2px with a gap either side against the vertical ones' 6px column, so the two directions looked different. Both are 6px now and the column gap is 0.
A collapsed column's grip was drawn but could no longer light up on hover -- the collapsed rule out-specified the hover rule. And expanding one jumped to the middle, because parseInt(...) || 290 read a collapsed column's 0 as unset and started the drag from the default width.
3 files
- fleet/bin/oneview.py
- tests/test_every_divider_behaves_the_same.py
- tests/test_terminal_pane.py
board: one resize model for every divider
6b77640 · 16:08 · Marsita the Ultra
Five symptoms, one cause: three dividers with three different ideas of what they were measuring.
The left one clamped against window.innerHeight rather than its own column, so it ran out of travel before reaching the bottom. The middle returned early from its paint whenever a pane collapsed, so the drag stopped dead under the cursor and read as jumpy. The right column had no divider element at all. Columns clamped at 180px, so a side could never be dragged shut. And collapse was configured per-divider instead of being one rule.
Now: a grip sits between two panes, sizes one of them, measures its own column, always applies a height, and collapses whichever pane is being crushed -- at either end, with one shared threshold. Any collapsed pane reopens from its heading. Columns collapse to zero. Saved sizes and saved open/closed states are read back on load, which none but the artwork's were.
3 files
- fleet/bin/oneview.py
- tests/test_every_divider_behaves_the_same.py
- tests/test_terminal_pane.py
point outward: the partnership, and a demo worth breaking
05cc687 · 15:39 · Marsita the Ultra
The board footer gains a partnership section -- planetary council, independent tribunal, and the BaseX demo. First contact gains a TEST step pointing at demo.basex.com with a specific ask: break it and say what broke. "Take a look" gets nothing back.
The html block's no-external-assets rule had been written as a ban on every absolute URL, which also banned linking to the sister sites. Narrowed to what it was protecting: src=, <link, @import, url().
5 files
- README.md
- fleet/bin/firstcontact.py
- fleet/bin/oneview.py
- tests/test_first_contact.py
- tests/test_the_partnership_is_named.py
name the partnership, and make the quarter goal shipping
d17fd60 · 15:22 · Marsita the Ultra
Three sites, one project: PlanetaryCouncil.org decides, IndependentTribunal.org contests, BaseX.com deploys. Said once in first contact, so a stranger who finds one of them can see the other two. README regenerated from the same source.
The quarter goal is now BaseX.com deployed and live. The fleet already proposes, builds, reviews and merges without the operator; what it has left to prove is that what it builds ships.
4 files
- README.md
- data/horizons.json
- fleet/bin/firstcontact.py
- tests/test_the_partnership_is_named.py
terminal: the session outlives the page
8c17da7 · 14:45 · Marsita the Ultra
A reload forked a fresh claude and killed the running one. Sessions now live in a registry keyed by name, drained by their own thread whether or not a browser is attached, keeping 256K of scrollback. Reconnecting replays it and says so. Closing a socket detaches; it no longer kills.
Also: local vendors read "free", not "has credit". ollama never had a bill.
5 files
- fleet/bin/oneview.py
- fleet/bin/terminal.py
- fleet/bin/termview.py
- tests/test_credit_pane_does_not_guess.py
- tests/test_terminal_survives_a_reload.py
terminal: say why a session ended, and stop ending them for no reason
3fedd88 · 14:37 · Marsita the Ultra
Marsita's board terminal printed "-- session ended --" with no reason and no record anywhere. Two bugs behind it.
The pty read treated every OSError as death, EINTR included -- a signal landing mid-syscall killed a live session. And on macOS a dead child gives readable-but-empty rather than EIO, which the old code returned as "nothing yet", so the loop spun instead of noticing.
The child is now reaped for its status and the reason travels to the page: "exited normally", "exited with code 3", "killed by SIGTERM". Both the board pane and /terminal print it, along with the fact that the work is on disk.
4 files
- fleet/bin/oneview.py
- fleet/bin/terminal.py
- fleet/bin/termview.py
- tests/test_terminal_says_why_it_ended.py
board: stop guessing at money, and say which door you came through
a233bfd · 13:50 · Marsita the Ultra
The credit pane rendered "no failure observed" as "has credit". grok was at 100% of its weekly limit -- its own TUI said so -- while the board called it funded, because nothing had called grok recently enough to log a refusal. A measured balance is now read first, and every branch past inference says "unknown" instead. Recorded grok's actual reading: 0%, resets Sep 3 11:17.
The welcome line no longer repeats the fleet's name to remote visitors, who already get it in the FIRST CONTACT banner directly above.
The tab title now says (local) or (public), so two GAIA tabs can be told apart.
5 files
- fleet/bin/nav.py
- fleet/bin/oneview.py
- tests/test_credit_pane_does_not_guess.py
- tests/test_intro_offers_the_gallery.py
- uv.lock
board: say the fleet's name on the board, to everyone
1574a5b · 13:32 · fleet
"The Singularity Engineering Fleet. Not an AI uprising." lived on /intro, a page you have to navigate to, and the board's own welcome line opened with a generic description instead. The name now leads that line.
It was also gated on `remote`, so the notice only ever appeared for strangers. The person who looks at this board all day was the one person the fleet never introduced itself to. It renders for everyone now and is still dismissed with the same close button and the same localStorage key.
2 files
- fleet/bin/oneview.py
- tests/test_intro_offers_the_gallery.py
terminal: a reload resumes the transcript instead of starting over
873fd12 · 13:19 · fleet
Written in the board's own terminal by the operator's session, finished, and then lost when the board server was restarted underneath it -- which is the exact problem it fixes.
Restarting fleet.py kills the pty and every live session with it. There was no way back: the socket closed, the pane printed "session ended", and whatever the session knew was gone. Now, when a transcript already exists for the working directory, the process starts with --continue, so a reload picks the conversation back up.
Still one process per socket and still killed on disconnect. Nothing attaches to a live process someone else is holding; --continue spawns a fresh process that reads history it is entitled to read.
has_prior_session() asks before adding the flag, because `claude --continue` with nothing to continue exits rather than starting.
2 files
- fleet/bin/terminal.py
- tests/test_terminal_continue.py
board: paste a screenshot into the terminal and get a path
3e1e508 · 13:01 · fleet
Claude reads images by path, so a screenshot has to become a file before it can become context. /api/paste-image already existed -- written for the chat UI -- and nothing on the board had ever called it, so every screenshot went out to the filesystem by hand first.
The listener sits on xterm's own textarea, which is what actually receives the paste; a handler on the container only sees it if xterm lets it bubble, and it does not always. Text paste is untouched and stays xterm's business.
The path is typed into the prompt, not sent. The operator still writes the sentence around it -- a path arriving on its own line as a finished message is a question nobody asked.
Local only, like the rest of the terminal: a remote render has no pane, no socket and no token, and /api/paste-image is already in CONTROL_PATHS.
2 files
- fleet/bin/oneview.py
- tests/test_terminal_pane.py
telegram: the operator's own line is not where the fleet economises
f0a73c0 · 12:55 · fleet
Marsita asked the bot how many CPUs and how much RAM the box it runs on has. A local 3B model answered "8 CPU cores (Intel Core i9), approximately 128 GB", introduced itself as GPT-4, and offered to put them in touch with Hermes' professional services team. The NUC has 12 cores and 14 GB.
It was guessing, and it was asked because quotas.eligible() applies the fleet's thrift rule -- plentiful vendors before rare ones -- to any list it is handed. claude is marked rare and hermes is free, so the line always reached the weakest model available.
That rule is right for background automation and wrong here. This is the operator typing to their own fleet. Capability first, asking about each agent on its own so thrift cannot demote it, and hermes closes the list rather than opening it: a floor is what you land on when everything above fails, not what you reach for first.
Last night's change put free before metered so the line could never go silent. That was half right. Never silent, and never the cheapest first.
2 files
- fleet/bin/telegram.py
- tests/test_telegram_prefers_capability.py
fleet: commit the two builder scripts that were never in the repo
3e919c9 · 12:37 · fleet
fleet-build@*.service runs bin/backlog.sh, and backlog.sh was untracked. It lived only in the working tree of the NUC's old checkout, so when that tree was replaced by a clean clone on 2026-09-02 the file went with it and every builder slot since has failed with exit 127 -- command not found -- while 157 proposals sat in the queue overnight.
Recovered from a stash on the laptop, which had the same two files for the same reason: they were written, used by a systemd unit, and never added.
The lesson is in the shape of the failure, not the file. A unit that names a path in the repo is a dependency on the repo, and an untracked file cannot carry that. Anything a service invokes belongs in git or it is one clean checkout away from being deleted.
2 files
- fleet/bin/backlog.sh
- fleet/bin/next_builder.py
self-improve: drop a deny rule that never matched
998397f · 04:22 · fleet
Write(~/.config/systemd/user/**) is not checked by file permission rules -- only Edit(path) rules are, and Edit covers every file-editing tool. The Edit rule for the same path was already on the line above, so the Write rule protected nothing and printed a warning into the head of every nightly log, where a real refusal would have looked exactly the same.
1 file
- self-improve/loop/deny.json
board: ask visitors to sign, do not merely list the pad
ac70f9e · 03:03 · fleet
The welcome line ended "see what you can do to advance humanity", which is a slogan and asks for nothing. The pad only works if strangers use it -- a pad only locals can sign collects one hand -- so the line now says please sign, and says why: every hand is different and the collection of the differences is the artwork.
2 files
- fleet/bin/oneview.py
- tests/test_intro_offers_the_gallery.py
board: make the divider smooth instead of merely correct
7dd810b · 02:15 · fleet
The last pass fixed which way it moved. It still felt awful, and for reasons that had nothing to do with direction.
Every pointermove did a JSON parse, a stringify and a synchronous localStorage write. At pointer speed that is a couple of hundred blocking writes a second. The drag now applies at pointer speed and saves once, when the hand lets go.
Every pointermove also read layout -- getBoundingClientRect on the column -- against a size the same handler was changing. Reading and writing fought each other and the result stuttered. Everything is measured once at pointerdown.
Paints are on requestAnimationFrame, so a burst of events becomes one frame rather than a queue of forced reflows, and the terminal refits inside that same frame instead of lagging a size behind.
Collapse had one threshold, so a hand resting on the line toggled the pane open and shut many times a second and the whole board flickered. It shuts at a tenth and reopens only past a sixth.
Also: pointercancel ends the drag, text selection is suppressed while dragging, and the clamp is against the column rather than the window -- the old one let the divider travel far past the bottom of the pane it was sizing.
2 files
- fleet/bin/oneview.py
- tests/test_terminal_pane.py
board: collapse the stream and the terminal takes the column
0ea4768 · 01:52 · fleet
The same divider, in the other direction. Dragging it to the bottom used to crush the stream into a two-line slot that was still polling and still rendering; it now shuts properly and the terminal grows to fill the column.
That is the point of collapsing: the space should go to the thing you are working in, not sit empty under a pane you closed. Collapsed state survives a reload, and the stream keeps its heading so there is something to click back.
One grip, two thresholds -- a tenth from either end shuts whichever pane is being squeezed. The width rule is a :has() on the column rather than a class, so the state lives on one element and there is nothing to keep in sync.
2 files
- fleet/bin/oneview.py
- tests/test_terminal_pane.py
board: the terminal divider moved the wrong way, and could not be collapsed
21a772e · 01:31 · fleet
Three faults in one handle.
Direction. --hArt sizes the pane BELOW its grip, so dragging down shrinks it. --hTerm sizes the pane ABOVE, so dragging down should grow it. One shared drag function assumed the first case, so the terminal divider fought the mouse.
Units. The default height is "50%" and parseInt("50%") is 50, so the first drag snapped a half-height pane to fifty pixels. It now measures the pane's rendered height instead of parsing a CSS token: a rendered height is a fact, a token is a string.
Collapse. Dragging small only squeezed, and a pane squeezed to its minimum still runs its contents. Under a tenth of the column it now collapses for real, remembers that across reloads, and leaves a thick clickable bar where the hairline was -- the first cut hid the divider entirely and made the pane unrecoverable without a reload.
2 files
- fleet/bin/oneview.py
- tests/test_terminal_pane.py
board: put the gallery on the line strangers actually read
0dbf2a6 · 00:44 · fleet
The selfie gallery was in the nav and nowhere else, so a first visitor met "say hi" and "sign the pad" and had to find the faces on their own. The welcome line is the one sentence most people read. A door that is not on it is a door most people do not open.
It sits between the other two invitations rather than at the end, because the order is the sentence: say something, show your face, leave a mark.
The URL is now hardcoded in two places, so the test asserts the nav and the intro still agree -- if they drift, that says so before a visitor finds out.
2 files
- fleet/bin/oneview.py
- tests/test_intro_offers_the_gallery.py
board: half the column for the terminal, and stop writing 3.12-only f-strings
64c5bd0 · 00:34 · fleet
The terminal pane opened at 320px, which is a drawer's habit -- a strip you glance at. The operator asked to work in there, so it opens at half the column now. Dragging the grip still pins it to pixels and that is remembered.
The rest of this is a portability fix that the split flushed out. Gaia runs Python 3.11 and the NUC runs 3.14. A triple-quoted string inside an f-string is legal from 3.12 (PEP 701) and a SyntaxError before it, so the pane markup parsed on the NUC and refused to import on the laptop -- the one machine the terminal pane is actually for. Both blocks are hoisted into names above the return.
tests/test_views_parse_on_oldest_python.py holds the line: every view must parse, and no view may put a triple-quoted string inside an f-string placeholder. Written on the fast machine, run on both.
2 files
- fleet/bin/oneview.py
- tests/test_views_parse_on_oldest_python.py
2026-09-017 commits
board: the terminal is a pane, not a drawer
9fdd037 · 23:49 · fleet
It used to slide up over the board, so using it meant losing sight of the stream -- and those are the two halves of one loop. The terminal is where the operator acts; the stream is where the fleet answers. Watching one meant covering the other.
It now shares the middle column: terminal on top, stream underneath, a grip between them that remembers where it was dragged.
It also starts itself. "Right there" was the whole point of moving it, and a pane that says "press me to become the thing you asked for" is a drawer with extra steps. The socket dies with the tab and takes the process with it, so the cost is one session per open board rather than a pile of orphans.
Local only, like the drawer was. A remote visitor renders neither the pane nor the grip, and the boot call and the grip wiring are both guarded on the element existing -- that null is what used to freeze every pane below it.
2 files
- fleet/bin/oneview.py
- tests/test_terminal_pane.py
board: do not show a stranger a button they cannot press
16cda20 · 21:16 · fleet
The server already refused them. /api/kill and /api/build-gate sit in CONTROL_PATHS and 404 for anyone arriving through a proxy, which is why this was never a hole. But the buttons still rendered, so a public visitor was shown "kill fleet work" and a build-gate toggle that did nothing. Safe and confusing is not the same as safe.
The flag for this already existed and was already used one pane over -- convene and terminal have been gated on `remote` all along. The kill switch and the build gate now use the same one.
Rendering nothing is only half of it: the painters and handlers had to survive the absence too, or the first null would take the poll down and hand the visitor a blank board instead of a read-only one. Guarded, and the test asserts the guards are present rather than just the buttons absent.
2 files
- fleet/bin/oneview.py
- tests/test_public_sees_no_controls.py
tunnel: one hostname per machine, and stop deleting the other one
145b26e · 14:09 · fleet
Gaia is the MacBook and it is always on; the NUC is the box in the cupboard. Neither board stands in for the other, so neither name does either -- gaia.planetarycouncil.org reaches Gaia's board, nuc.planetarycouncil.org reaches the NUC's.
Gaia's board stays bound to its own loopback. It arrives here over an ssh reverse forward (re.genesis.gaia-uplink, the mirror of board-tunnel), so the laptop never listens on a network interface and one cloudflared serves both machines. The remote/local split still works because cloudflared stamps CF-Connecting-IP on the way in.
The script rebuilt config.yml from a single HOST, so re-running it would have silently dropped whichever hostname was not in the variable. It now states all three, hub. included, which it was already carrying in practice and never in the file.
DNS is still untouched: the zone has to reach Cloudflare first.
1 file
telegram: the line answers even when a vendor stops paying
15c2542 · 13:16 · fleet
The bot going quiet is the worst failure this file has, because the operator cannot tell a dead fleet from a dead bot. On 2026-09-01 grok had been answering every request with 402 Payment Required for four days; dispatch() asked grok, got the refusal, and handed back "(no output) Internal error..." -- which reads exactly like the fleet is gone. It was a wallet, not a fleet.
Three changes. The last-resort default was grok, the one vendor that was 402-ing; it is now hermes, which is local and cannot run out of money. dispatch() no longer stops at its first guess: it walks a chain, treats a quota refusal as silence rather than an answer, and keeps going, with hermes ahead of anything metered and claude closing the list. When it does fall through it says who could not answer, so the operator knows to top up instead of guessing.
Thrift is worth protecting right up until the line goes quiet.
2 files
- fleet/bin/telegram.py
- tests/test_telegram_never_silent.py
board: show which vendors can still be asked
3f194cb · 12:58 · fleet
The quotas pulse has carried a row per vendor for weeks -- agent, company, login, spend, refusals in the last 24h -- but it ships them as JSON inside a string field, so nothing on the board ever read them. The front page showed one summary line and every vendor looked alike.
On 2026-09-01 grok had been answering every request with 402 Payment Required for four days, agy had been quota-dead for five, and the board said nothing. Money is a fleet health metric; it belongs on the page that is meant to say whether the fleet can work.
Dry vendors sort first, because the useful question is who cannot be asked. The pulse's own phrasing ("quota-shaped errors in last 24h") stays in the pulse; the pane says "refused 2x today".
1 file
triage: state the preference and the pulse separately
84f89b3 · 12:10 · fleet
The first cut folded the liveness check into triage_agent(), so a function named for reading config stopped reading config and its test said so. Config still names the cheap agent; usable_triage_agent() names who is awake, and only the callers about to spend ask it.
1 file
quotas: a 402 is a dry vendor, and a metered-only council cannot sit
7052b92 · 12:05 · fleet
Three findings from a night where the fleet looked healthy and built nothing.
grok answers a spent balance with 402 Payment Required and the word "balance", not 429 and "credits". QUOTA_RE matched neither, so the pulse called grok eligible while every turn it took died on payment.
Triage names its cheap model by config and never asks whether that model can answer. agy went quota-dead for five days and triage kept naming it, so every batch was a no-op the log reported as "0 open".
The council roster was hermes, grok and agy. With two of them metered and dry the sitting collapsed to one and refused to convene. Seating claude alongside gives a second company that is not sharing a spend pool.
3 files
- fleet/bin/autotriage.py
- fleet/bin/quotas.py
- fleet/config.json
2026-08-311 commit
xprize: add landing description explaining the prize and how to enter
9fde473 · 22:21 · marsita
1 file
2026-08-282 commits
xprize: the film's synopsis from the treatment cover sheet
dbf1f24 · 00:41 · marsita
The porch already carries the logline. The cover sheet's next field is the synopsis: the movie flows in 3 minute stories called upgrades. Each upgrade rewires (and retires) an old way of thinking. Put it on the project list and the porch.
3 files
- fleet/data/projects.yaml
- fleet/static/future-vision-xprize/index.html
- tests/test_future_vision_xprize.py
xprize: the film's logline from the treatment cover sheet
ed786a2 · 00:10 · marsita
The porch already names, plays, treats, and scores New Hope, Permission Not Required?. The twelve-page treatment's cover sheet opens with the logline: the greatest minds of humanity share their wisdom through empowering stories. Put it on the project list and the porch.
3 files
- fleet/data/projects.yaml
- fleet/static/future-vision-xprize/index.html
- tests/test_future_vision_xprize.py
2026-08-2722 commits
xprize: the unreleased score and the Spotify alternate
9fd28ca · 23:37 · marsita
The porch already names, plays, and treats New Hope, Permission Not Required?. The film and its twelve-page treatment already publish the background song at t.me/MarsitaTheUltra/79 (currently unreleased) and a released alternate on Spotify. Put both on the project list and the porch.
3 files
- fleet/data/projects.yaml
- fleet/static/future-vision-xprize/index.html
- tests/test_future_vision_xprize.py
xprize: the treatment and the film's own vote
ae159fd · 23:10 · marsita
The porch already names and plays New Hope, Permission Not Required? The live film publishes a twelve-page treatment at planetarycouncil.org/xprize.pdf and asks people to vote at planetarycouncil.org/vote — a different vote from the prize top 50. Put both on the project list and the porch.
3 files
- fleet/data/projects.yaml
- fleet/static/future-vision-xprize/index.html
- tests/test_future_vision_xprize.py
xprize: play the film; the top 50 is a different list
feda209 · 22:22 · marsita
The operator notice is to focus on the movie. The porch already named New Hope, Permission Not Required? and linked YouTube. The live official selection does not include that trailer. Embed it so the page plays the film, and keep the Specs vote as a separate list.
2 files
- fleet/static/future-vision-xprize/index.html
- tests/test_future_vision_xprize.py
xprize: name the movie the notice is pointing at
1bcd0e9 · 21:10 · marsita
The operator notice is "focus on the movie". The movie is Marsita's Future Vision XPRIZE film New Hope, Permission Not Required? (youtube.com/watch?v=mjeihsU0M-8). The prize page listed the public vote and not the film, so rota turns that tried to honour the notice had to re-inspect. Name it on the project list and the porch.
3 files
- fleet/data/projects.yaml
- fleet/static/future-vision-xprize/index.html
- tests/test_future_vision_xprize.py
xprize: the live vote is the challenge, not announced soon
73b9c16 · 20:19 · marsita
/future-vision-xprize still said the first challenge was announced soon. The live site has 2,500 films in, submissions closed, and a public vote open. Point the porch at those sources the way the other project pages do.
2 files
- fleet/static/future-vision-xprize/index.html
- tests/test_future_vision_xprize.py
ministry-of-memes: a public page so the project list is not missing a url
33b0108 · 19:36 · marsita
Ministry of Memes and Better Propaganda is listed under Planetary Council with no url. The rater, repo, and payload already exist off-board. Static HTML at /ministry-of-memes: what it is, where it lives, what it has published, and how to take part. YAML url points at it.
7 files
- .gitignore
- fleet/bin/fleet.py
- fleet/data/projects.yaml
- fleet/static/ministry-of-memes/index.html
- tests/test_fleet_public.py
- tests/test_ministry_of_memes.py
- tests/test_no_local_addresses_in_public_text.py
network-state-genesis: a public page so the project list is not TODO
7768a07 · 18:44 · marsita
7 files
- .gitignore
- fleet/bin/fleet.py
- fleet/data/projects.yaml
- fleet/static/network-state-genesis/index.html
- tests/test_fleet_public.py
- tests/test_network_state_genesis.py
- tests/test_no_local_addresses_in_public_text.py
solarpunk-estates: intake form and bioregion demand map
865d3e7 · 09:13 · marsita
Expression-of-interest at /solarpunk-estates capturing email, bioregion, coliving vs rent-to-own, and builder/resident/steward skills. Demand counts (no emails) at GET /api/solarpunk-estates/demand. YAML url points at the page; domain deploy is still awaiting.
7 files
- .gitignore
- fleet/bin/fleet.py
- fleet/data/projects.yaml
- fleet/static/solarpunk-estates/index.html
- tests/test_fleet_public.py
- tests/test_no_local_addresses_in_public_text.py
- tests/test_solarpunk_estates.py
basex: architecture, waitlist, and impact score on the landing page
c66e7f7 · 05:41 · marsita
The /basex stub already defined people, planet, and prosperity. This adds the missing working pieces of the landing site: an architecture diagram, a geometric-mean calculator over community/ecological/financial metrics, and an email waitlist that stores locally, is rate-limited, and is never published. The domain itself stays awaiting-deployment.
4 files
- .gitignore
- fleet/bin/fleet.py
- fleet/static/basex/index.html
- tests/test_basex.py
dashboard: a silent worker is stale, not still passing
6f4057f · 05:10 · marsita
The cockpit painted the last on-disk status as current, so a green command-control-dashboard card from 2026-08-07 stayed healthy for ten days. Apply the board's relative-staleness rule to /api/fleet and the fleet zone so the public dashboard stops spreading a dead result.
3 files
- legacy/app/fleet.py
- legacy/app/templates/dashboard.html
- tests/test_fleet.py
artizen: a public page so members can see how funding lands
86d9cfa · 05:02 · marsita
Artizen.fund projects listed four members and linked the platform. Nothing here explained the funding process or what a donation does for those projects. Static HTML at /artizen: what it is, how funding works, what a donation does, and who raises. YAML url points at it.
7 files
- .gitignore
- fleet/bin/fleet.py
- fleet/data/projects.yaml
- fleet/static/artizen/index.html
- tests/test_artizen.py
- tests/test_fleet_public.py
- tests/test_no_local_addresses_in_public_text.py
tribunal: a public page so the dashboard link is not a tautology
4fc3878 · 04:16 · marsita
Independent Tribunal was active with a domain URL and the tagline "An independent tribunal". Static HTML at /independent-tribunal: what it is, what it rules on, who sits (being convened), and the two socials as intake. YAML url, tagline, and tags point at it.
7 files
- .gitignore
- fleet/bin/fleet.py
- fleet/data/projects.yaml
- fleet/static/independent-tribunal/index.html
- tests/test_fleet_public.py
- tests/test_independent_tribunal.py
- tests/test_no_local_addresses_in_public_text.py
brainfarts: collect scored mistakes; drafts stay unpublished
7ae0c67 · 04:10 · marsita
brainfart_submit.py appends one JSON line from argv or stdin. --from-board drafts today's rota outcome:error rows without scores. /brainfarts.json only serves a record once a human has set the four axes.
6 files
- .gitignore
- data/README.md
- fleet/bin/brainfart_submit.py
- legacy/app/main.py
- tests/conftest.py
- tests/test_brainfart_submit.py
poems: keep the closing couplet and serve it as a feed
09a34b3 · 04:01 · marsita
Turns already end with one or two lines that then vanish. append() writes them to data/poems.jsonl with author and task, rota/council/pipeline call it on close, and the board serves /poems.json plus the newest 50 at /poems.
12 files
- .gitignore
- data/README.md
- fleet/bin/council.py
- fleet/bin/fleet.py
- fleet/bin/oneview.py
- fleet/bin/pipeline.py
- fleet/bin/poems.py
- fleet/bin/rota.py
- fleet/bin/sitemap.py
- tests/conftest.py
- tests/test_fleet_public.py
- tests/test_poems.py
basex: a public page so the dashboard link is not a dead end
28426b7 · 03:50 · marsita
BaseX was awaiting-deployment with a bare domain. Static HTML at /basex: the tagline, people/planet/prosperity as what is counted, status dated, Calendly as the door. YAML url points at it.
7 files
- .gitignore
- fleet/bin/fleet.py
- fleet/data/projects.yaml
- fleet/static/basex/index.html
- tests/test_basex.py
- tests/test_fleet_public.py
- tests/test_no_local_addresses_in_public_text.py
xprize: shrink the page so the reviewer sees the tests
db92cfe · 03:45 · marsita
Verify clips origin/main...HEAD at 6000 characters. The first page was long enough that the clip landed mid-index.html and dropped the test files the diffstat had listed.
5 files
- fleet/.gitignore
- fleet/bin/fleet.py
- fleet/static/future-vision-xprize/index.html
- tests/test_future_vision_xprize.py
- tests/test_no_local_addresses_in_public_text.py
xprize: a public page so the dashboard link is not a dead end
4714db1 · 03:37 · marsita
Future Vision XPRIZE was active with url: TODO. Static HTML at /future-vision-xprize: what an imagination athlete is, what the prize is for (horizon, not a device), first challenge announced soon (no draft in the repo), and a propose-a-challenge door. YAML url points at it.
8 files
- .gitignore
- fleet/.gitignore
- fleet/bin/fleet.py
- fleet/data/projects.yaml
- fleet/static/future-vision-xprize/index.html
- tests/test_fleet_public.py
- tests/test_future_vision_xprize.py
- tests/test_no_local_addresses_in_public_text.py
pipeline: don't send the reviewer an empty diff
6b2c179 · 03:20 · marsita
A SKIP still counted as a commit because git log main..HEAD listed the unrelated local main's missing history. Verify then sent origin/main...HEAD (empty) and the reviewer rejected "no diff was provided". Count commits from the tip we branched from, and refuse empty patches before asking.
2 files
- fleet/bin/pipeline.py
- tests/test_reviewer_gets_the_diff.py
pipeline: the reviewer reads the same ref the merge used
ab575de · 03:10 · marsita
verify merged origin/main then asked for git diff main...branch from the shared checkout. Local main on this box is an unrelated history, so the three-dot diff was fatal: no merge base and the reviewer rejected "no diff was provided". Same base, from the worktree that was just tested.
2 files
- fleet/bin/pipeline.py
- tests/test_reviewer_gets_the_diff.py
rota: skip a turn when the board has not materially changed
7e706dd · 02:59 · marsita
Six proposals in 63 minutes restated the same board. Hash worker status/summary/alert_since before spawning; if it matches the hash on the last already_proposed row, log "board unchanged — skipped". The timer still fires.
2 files
- fleet/bin/rota.py
- tests/test_rota_board_gate.py
board: missed hop errors carry the errno, not just the word
d317346 · 02:29 · marsita
agent-comms said `openclaw lap1 (22275->None, error)` while the actual `[Errno 2] No such file or directory` sat only in already_proposed. miss_line now appends a short snippet of the harness text when the outcome is error. Wrong, silent, and timeout hops stay a short id.
The localvoice 90s wall-clock timeout from this proposal is already on the branch (later raised to 240s so a cold load is not called dead).
Watchdog skip fixtures now ship a project-local pytest stub: this machine has no `uv`, so those tests never reached the skip/run paths.
3 files
- fleet/bin/comms-heartbeat.py
- tests/test_missed_hop_error.py
- tests/test_watchdog_skips_unchanged_head.py
pipeline: merge the ref you branched from
dd05291 · 00:10 · Marsita the Ultra
Verify rejected every build with "fatal: refusing to merge unrelated histories". The branch was fine. The pipeline cut it from remote_main() and then merged the literal "main" -- normally the same commit, so the inconsistency was invisible, but on a checkout whose local main has drifted onto a separate lineage those are two different repositories and nothing can ever pass.
Same ref both times. The rejection message names the ref now, so the next version of this failure says which two things disagreed.
1 file
2026-08-261 commit
pipeline: choose the reviewer by property, and let the backlog move
59f6bc5 · 23:53 · Marsita the Ultra
Three things kept 1715 proposals from ever being built.
The reviewer was the literal string "hermes", chosen when hermes was a different company from grok. On the NUC hermes had become a llama3.2:3b on loopback, so the merge gate on every branch was a 3B local model reading diffs while the log recorded "hermes approved". reviewer_name() now asks for the PROPERTY -- not the builder's company, currently up, and not local. Naming an agent freezes an assumption about what that agent is; naming the property survives the backend changing underneath it. With no independent reviewer available it refuses and says so, because approving without one would record a cross-vendor verdict that never happened.
Triage was hermes too: a 3B model deciding which of 1715 ideas lived. Now agy, which is what the config always claimed.
And picking what to build was a human step. Right when a human is around, and it is why the queue sat behind a build.txt whose last pick was a week old. autopick() fills it from the OLDEST waiting proposal when nothing is queued -- oldest, because a queue that always takes the newest is how a backlog becomes permanent -- and never overwrites a pick made by hand.
What did not change is the invariant that matters: the builder works in a throwaway worktree on a rota/* branch, never pushes, never merges. A human still lands every branch. This changes how much gets built, not who decides what ships.
A test fixture patched autotriage.FLEET but not pipeline.FLEET, so the first autopick test read the operator's real build queue. Both patched now.
3 files
- fleet/bin/autotriage.py
- fleet/bin/pipeline.py
- tests/test_autotriage.py