todo: one file per item, grouped by theme, with a generated index

TODO.md had reached 1050 lines and 72 open items across 28 sections, read mostly
on a phone. It is now a directory: todo/<theme>/<item>.md, one card per file, with
frontmatter carrying id, status, created date and order, and the prose body copied
across verbatim. TODO.md becomes a generated index (bun run todo:index).

Dates: where an item's prose stated one it is used; otherwise its old section's;
otherwise 2026-08-20, floored at the day before the split rather than invented.
date_source records which.

Shaped for a two-way Kanban board, filed as todo/tooling — a card is a file, id is
the identity, frontmatter is the machine's half and the body is the owner's.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-21 05:42:39 +00:00
co-authored by Claude Fable 5
parent 8fa560df1b
commit 6071ded175
124 changed files with 2240 additions and 1043 deletions
+5
View File
@@ -0,0 +1,5 @@
---
theme: infra
heading: "Infra (alpha)"
kind: theme
---
@@ -0,0 +1,18 @@
---
id: infra/delete-etc-systemd-system-officer-vnc-service
title: "Delete `/etc/systemd/system/officer-vnc.service`"
theme: infra
status: open
created: 2026-07-15
date_source: stated
order: 1
---
**Delete `/etc/systemd/system/officer-vnc.service`.** Hand-installed unit (not in this repo)
that ran `vncserver :1 -geometry 1920x1080 -localhost yes -fg` with `Restart=on-failure`,
enabled at boot — it kept a whole parallel XFCE session alive on `:1` (283 MB, 166 tasks)
independently of the platform, and silently respawned it whenever the display was killed.
Obsolete now that the Desktop panel mirrors `:0` via x11vnc. Disabled 2026-07-15
(`systemctl disable --now`), but the unit file is still on disk. Nothing in the codebase
recreates it and nothing documents it, so delete the file rather than leave a mystery service
one `systemctl enable` away from returning.
@@ -0,0 +1,16 @@
---
id: infra/move-the-remaining-env-credentials-into-the
title: "Move the remaining env credentials into the secret store"
theme: infra
status: open
created: 2026-08-19
date_source: stated
order: 3
---
**Move the remaining env credentials into the secret store.** `putSecret`/`getSecret` and
`bun run secret:set` landed 2026-08-19, and notify now reads `apns-p8`, `apns-key-id`,
`apns-team-id`, `fcm-service-account` and `discord-webhook` from the store with env as a
fallback. Every other plugin credential is still env-only — same argument applies: bun
auto-loads `.env` into every PM2 process, so each one is readable from `/proc/<pid>/environ`
of twenty processes that have no business with it.
@@ -0,0 +1,33 @@
---
id: infra/nudger-a-control-in-the-chat-composer-beside
title: "Nudger — a control in the chat composer, beside the background-job button"
theme: infra
status: open
created: 2026-08-20
date_source: stated
order: 4
---
**Nudger — a control in the chat composer, beside the background-job button.** Wanted
2026-08-20 while leaving an agent to work overnight: "continue every hour if you have stalled".
`scripts/nudge-agent.ts` does it from outside tonight — it polls `GET /api/chat/sessions?all=1`
and speaks over the chat websocket, exactly as a browser does, so the platform needed no changes.
That is the workaround, not the feature.
**Both things this said the platform lacked have since been built** (verified 2026-08-21): the
recurring trigger is `startNudger()` at `server.tsx:182`, ticking once a minute over a `nudges`
table; the door into a session is `startClaudeTurn({ ws: null })` at `nudger.ts:102`. What is left
is the UI — a control in the chat composer, beside the background-job button — plus generalising
delivery beyond the nudger's own schedule. See "Speaking into a session from the backend".
Design notes worth keeping:
- **Never interrupt work.** Check `isGenerating` immediately before speaking. The point is to
restart a stopped agent, not to hurry a busy one — the script already gets this right.
- **It must outlive what it nudges.** A chat background job is owned by its session, so a nudge
job launched from the session being nudged dies with it — the 30-minute idle GC, a clear, or a
restart takes the watchdog with the agent. Whatever owns the schedule cannot be the session.
- **Bound it.** The script stops after `--max` (12). A loop nobody is watching should end.
- The same trigger would serve `COMMS/command-hq/watcher-loop-proposal.md`, which wants
infrastructure to own a loop rather than a model holding a protocol across turns.
@@ -0,0 +1,26 @@
---
id: infra/revisit-the-frozen-lockfile-rule-in-claude-md
title: "Revisit the frozen-lockfile rule in `CLAUDE.md`, now that plugins resolve at install time"
theme: infra
status: open
created: 2026-08-20
date_source: stated
order: 5
---
**Revisit the frozen-lockfile rule in `CLAUDE.md`, now that plugins resolve at install time.**
Raised by the owner 2026-08-20, to be discussed rather than decided alone.
The rule as written is absolute: `bunfig.toml` sets `frozenLockfile = true`, `bun install` resolves
from `bun.lock` and nothing else, and an unexplained lockfile change in a diff is the signal the
whole arrangement exists to produce. The reasoning is the 2026-08-04 compromise, and it is good.
`npmDependencies` does not follow it. Plugin packages are declared as RANGES and installed with a
plain `bun install` into `plugins/node_modules/`, so they resolve per machine and per install. That
was chosen deliberately — ranges are what let two plugins share one copy of `sharp` — but it means
the codebase now holds two different postures on the same risk, and only one of them is written down.
Worth settling: whether the rule should say "the platform's own dependencies" rather than reading as
universal; whether the merged `plugins/bun.lock` should be committed after all (it would pin the
tree, at the cost of a per-machine file in git); and whether `bun install --frozen-lockfile` in
`plugins/` is achievable once that lockfile exists.
@@ -0,0 +1,14 @@
---
id: infra/ufw-blocks-port-9010-on-the-lan
title: "`ufw` blocks port 9010 on the LAN"
theme: infra
status: open
created: 2026-08-20
date_source: assumed
order: 2
---
**`ufw` blocks port 9010 on the LAN.** Default deny incoming; only `22/tcp`, `80/tcp`,
`443/tcp`, and everything on `tailscale0` are allowed — so `http://192.168.47.196:9010` is
unreachable from the LAN while localhost and Tailscale work.
Fix: `sudo ufw allow from 192.168.47.0/24 to any port 9010 proto tcp`