Files
roam/azostodos.org
2026-10-14 09:41:44 +03:00

439 lines
24 KiB
Org Mode

:PROPERTIES:
:ID: 939a9fa3-ddc8-4cc1-8130-771cc5de000f
:END:
#+title: azos/todos
#+filetags: :project: :todo:
Backlink: [[id:bf4dde56-0967-4374-94ed-771301e47c66][azos]]
Ordered plan for the new Fedora/EXWM work machine effort. Work top to
bottom; check items off with the todo skill as they land. Motivation
throughout: a new work computer (Fedora + nix) wants the =exwm= suite,
which depends on the (renamed-from-station) =desktop= suite, and many
=azos/features/*= modules should become reusable azos-core suites for
it — except genuinely personal-only ones, which stay in azos/features
per the discord precedent.
* DONE Rename azos-core/features to azos-core/suites; enforce suite grouping
Done 2026-10-13. Renamed =azos-core/features/= -> =azos-core/suites/=,
updated both =(inputs.import-tree ./suites)= references in
=azos-core/flake.nix=. Folded =claude-memory= and =claude-skills= into
=azos-core/suites/base/= (both now register
=config.flake.modules.homeManager.base=, same as git/gpg-agent/python) —
dropped their now-redundant standalone =suiteModules.homeManager.*=
entries from =home-manager/home.nix=. Updated AGENTS.md throughout
(directory tree, "where to put a feature", Claude integration table, key
files table) plus two stray in-repo path comments. Validated with
=nix eval ...nixosConfigurations.lauretta.config.system.build.toplevel=
— full derivation path, no errors. Convention going forward: no bare
top-level file directly under =suites/=, every azos-core feature belongs
inside a suite directory.
* DONE Decouple the exwm/station coupling — ended up as a station rename, not a split
Done 2026-10-13. Re-discussed scope with the user: station's apps
(browser, gimp, libreoffice, spotify, whatsapp, zoom) turned out to be
generically useful on *any* GUI machine, work included — so instead of
splitting station into desktop-plumbing + apps + a separate
personal-apps suite, =station= was simply **renamed to =desktop=**, kept
intact. The one genuinely personal-only app, =discord=, was dropped
entirely from azos-core and now lives as its own standalone
=azos/features/discord= (own =azos.discord.enable=, default true) — the
reasoning being any machine that's actually "personal" already uses the
=azos= project rather than a different downstream repo, so there's no
need to carry personal-only apps inside the shared azos-core suite at
all. =steam= needed no change — it was already sitting correctly as a
standalone =azos/features/steam= feature, never part of station.
Also fixed the vestigial Emacs-level coupling found earlier (full
investigation notes in
[[id:c52036a9-25ec-4b8b-8b72-90fcd9ea65dd][azos/exwm]]): removed
=pkgs.azos-emacs-station= from exwm's =_pkg.nix= packageRequires and the
=(require 'azos-emacs-station)= line from exwm's config.org — grepped and
confirmed exwm's own config never used any station symbols
(mu4e/elfeed/wttrin/kubernetes-/pass). exwm still defaults to depending
on the renamed =desktop= suite itself (=azos.suites.desktop.enable =
lib.mkDefault true=, unchanged in kind from the old station dependency)
— renamed, not removed, since the chromium/qutebrowser/gimp quick-launch
keybindings in exwm's config.org need those binaries from desktop's
browser.nix/gimp.nix and desktop is wanted on the work machine anyway.
Also renamed the paired elisp package =azos-emacs-station= ->
=azos-emacs-desktop= everywhere (desktop suite itself, and the real
consumer at =azos/features/lauretta/emacs=, which legitimately wants
mu4e/elfeed/kubernetes/wttrin). Dropped a duplicate bare =reaper= package
from desktop's plumbing list — =azos/features/reaper= already provides it
with plugins, so desktop's copy was a vestigial duplicate.
Per-app enable toggles (mirroring =browser.nix='s pattern) turned out
unnecessary for gimp/libreoffice/spotify/whatsapp/zoom — they're staying
in desktop unconditionally now since the user wants them everywhere, work
machine included. Validated the whole thing with targeted =nix eval=
checks against lauretta (confirmed =azos.suites.desktop.enable=,
=azos.discord.enable=, and =discord= present in the resolved
=home.packages= list, plus a full successful toplevel build) — no
regressions.
* DONE Audit azos/features/* for portability into existing azos-core suites
Standing principle established by the discord move above: a feature only
belongs in azos-core if it's something you'd want on *any* machine
including a work one. Personal-taste/consumer apps stay in azos/features
even if they're technically generic/reusable code-wise — e.g. =steam= and
=reaper= (audio production) are left out of azos-core for this reason,
not because they're hard to move.
DONE so far (2026-10-13), moved into azos-core with behavior preserved
(own toggle kept, no new suite-enable AND-gate added, validated via
targeted =nix eval= against lauretta — all resolved identically to
before the move):
- =audio=, =binfmt=, =bluetooth= -> =suites/desktop/= (home-manager +
new =nixos.desktop= module)
- =headphones= -> =suites/exwm/= (not desktop — user's call)
- =chess= -> =suites/editor/chess/= (brought its =emacs/= subdir along
too — =_pkg.nix=/=config.org=/=default.nix= defining the
=azos-emacs-chess= package; almost missed this on the first pass since
it wasn't visible in a maxdepth-1 =default.nix= read, only in the
directory listing)
- =kubernetes= -> =suites/dev/kubernetes/= (home-manager)
- =virtualization= -> =suites/dev/virtualization/=, the **first**
=nixos.dev= module (dev suite previously had no NixOS side)
- =hfsprogs= -> =suites/desktop/hfsprogs/=, as a *NixOS* module
(=environment.systemPackages=, not =home.packages= — deliberately
system-wide now, per request, unlike exfatprogs/e2fsprogs which stayed
home-manager-side in desktop's plumbing)
Decided to stay in azos, NOT moved (personal-taste/unused, even though
technically generic — see standing principle above): =leetcode=,
=qutebrowser= (currently broken/unused anyway), =ytdl=, =opencode=
(unused currently), =mpris-proxy= (narrowly tied to one university VPN
connection), =nextcloud= (keep personal, not genericizing).
DONE 2026-10-13, genericization batch — all four moved into
=suites/base/=, personal/secret values turned into mandatory options (no
=default=, so enabling the feature without supplying them is a hard eval
error by design, per explicit instruction: no real default values ever,
ever since azos-core shouldn't ship anyone's personal data even as a
fallback). Validated end-to-end via targeted =nix eval= against lauretta
— every resolved value matches what was hardcoded before:
- =git-config= -> =suites/base/git-config/=: =userName=/=userEmail=
mandatory; signing and the pass-git-helper credential helper
(=passCredentialHelper=) each independently toggleable, mapping content
also now an option instead of a committed =.ini= file.
- =encryption= -> =suites/base/encryption/=: split into independently
toggleable pieces — base gpg-agent setup (always on when
=encryption.enable=), =passwordStore= (pass + GPG key id),
=yubikey.enable=, =browserpass.enable=, =gpgSsh= (expose a GPG subkey
as an SSH key — keygrip/pubkey both mandatory options now, previously
committed files). Reason for all the independent toggles: unsure a work
machine will have GPG keys at all, let alone a YubiKey.
- =homey-alert= -> renamed to =azos-send-alert= (the command — "homey"
was personal selfhosted-infra naming), landing in
=suites/base/azos-send-alert/=, next to claude-memory/claude-skills.
The remote-notification half (previously a hardcoded ntfy curl call)
is now =azos.send-alert.remoteCommand=, an arbitrary shell command run
with TITLE/PRIORITY/TAGS/MESSAGE in its env; local notify-send fallback
unchanged. =--print-remote= still works: sets
=AZOS_SEND_ALERT_PRINT_REMOTE=1= and captures the command's stdout, by
convention the command resolves local secrets and prints a
self-contained invocation. =azos/features/homey-alert= now just hosts
the concrete ntfy-backed =azos-homey-ntfy-alert= overlay package,
plugged in via =azos.send-alert.remoteCommand= in home.nix.
- =printing= -> =suites/base/printing/= (nixos side): hardcoded "aner"
username is now =azos.printing.username= (mandatory).
Decided NOT to genericize, stay in azos as personal: =snx-rs= (BGU VPN
client — might drop it soon anyway), =hardware-auth= (any machine this
gets used on is personal anyway, per user).
Stays in azos, not generalized: attic (also flagged separately: it
embeds a plaintext JWT token directly in a git-tracked nix file — worth
a dedicated look regardless of this migration), lauretta (machine-specific
by definition), roam-backup (trivial personal workflow, low value to
move), steam, reaper (personal-taste apps, see standing principle above),
nextcloud (keep personal).
Noticed mid-audit but unrelated to this task, left untouched: a HORI
fight-stick udev rule uncommitted in =features/steam/default.nix= — the
user is actively editing it elsewhere, hands off.
hyprland suite move explicitly deferred — not used currently, a possible
future project in a few months, not part of this effort.
* DONE Make mail accounts a configurable attrset option
Done 2026-10-13 — turned out to be less work than expected. Discussed
first, without touching anything: =accounts.email.accounts= is already
home-manager's own generic attrset-of-accounts option, so there was no
need to invent a new =azos.mail.accounts= wrapper around it — azos-core
just had to stop pre-populating that option with real addresses.
- =suites/desktop/mail/= (destination: desktop, not base — user's
call) now owns only the mechanical glue: mu/mbsync/msmtp toggles, the
=mu init --my-address=...= list *derived* from whatever
=accounts.email.accounts= actually contains (via
=lib.mapAttrsToList=) instead of hardcoded, and =mbsyncSafe= (new-mail
count diff + notify-send, already fully generic).
- The hardcoded =azos-card-present= smartcard gate inside =mbsyncSafe=
became =azos.mail.presenceCheckCommand= -- an optional pluggable shell
command (null by default = always sync unconditionally), since a
given machine might not back mail passwords with a GPG smartcard at
all. =azos/features/mail= wires it to
=${pkgs.azos-card-present}/bin/azos-card-present= but only
conditionally, when =azos.encryption.yubikey.enable= is true --
directly addresses the concern that whether this gate makes sense at
all depends on whether a YubiKey/smartcard is even in the picture.
- =azos/features/mail/default.nix= kept the =default_account_params=/
=default_gmail_params=/=default_smtp= convenience merge-helpers and
the three real accounts (personal boilerplate, not mechanism) and
just sets =accounts.email.accounts= + =azos.mail.enable= + the
presence-check wiring directly.
- Validated via targeted =nix eval= against lauretta: all three
addresses present, =mu init= has the derived =--my-address= flags,
presence check resolves to the real =azos-card-present= binary path.
* DONE (stretch) Make claude-memory/claude-skills backend-pluggable
Done 2026-10-13 — went further than "LLM-agnostic" framing suggested:
the real axis is the *agent harness* (Claude Code CLI vs. agent-shell
vs. others), not the model.
** Findings (grounded by cloning and reading actual source, not docs)
- =agent-shell= (xenodium, Emacs ACP client) supports ~19 backends
(Claude, Gemini CLI, Codex, Goose, OpenCode, etc.) via per-backend
"agent configs". =agent-shell-mcp-servers= (plain Elisp defcustom) is
passed to *every* session's ACP =session/new= regardless of backend —
genuinely the "memory regardless of model" mechanism.
- =agent-shell-subscribe-to= exposes a =file-write= event (=:path=,
=:content=) — a cleaner, backend-agnostic equivalent to Claude Code's
=PostToolUse= hook for the org-roam-resync-on-edit behavior.
- Initially proposed isolating Claude-via-agent-shell with
=CLAUDE_CONFIG_DIR= pointed at an empty dir to avoid double-firing —
user correctly pushed back: slash commands
(=~/.claude/commands/*.md=) already work today inside agent-shell
sessions, and isolating the whole config dir would have broken that.
- Cloned =claude-agent-acp= (zed-industries, the ACP shim
=azos/features/claude= installs) and confirmed directly in source:
it is built on the real Claude Agent SDK and merges
=~/.claude/settings.json='s hooks into every session it drives
(=resolveSettings=, same merge engine the CLI itself uses; has a
dedicated =session-hooks.test.ts= proving this is intentional) — so
the double-fire risk for the *two memory-specific hooks* was real.
But it **never reads =~/.claude.json=\= at all** (zero references in
source) — so the MCP-server registration was never actually at risk
of colliding; nothing needed fixing there.
** What shipped
- =azos.claude-memory.enable=/=azos.claude-skills.enable= renamed to
=azos.agent-memory=/=azos.agent-skills=, both reshaped as
=backends.<name>.enable= attrsets instead of a single flag.
- =agent-skills= has one backend (=claude=) — no cross-backend
mechanism exists for slash-commands/CLAUDE.md, so nothing to add
alongside it yet.
- =agent-memory= gets a second backend, =agent-shell=: drops
=~/.emacs.d/extra/azos-agent-memory.el= (auto-loaded via the existing
=load-dir= setup in =suites/base/emacs=, not a compiled
=azos-emacs-*= package, since it needs real Nix-interpolated paths)
registering =org-roam-mcp= in =agent-shell-mcp-servers= and
subscribing to =file-write= for resync.
- Double-fire fix: the =agent-shell= backend appends
=AZOS_AGENT_SHELL_SESSION=1= to
=agent-shell-anthropic-claude-environment=; the =claude= backend's
two memory-specific hooks (=PostToolUse= resync, =SessionStart=
context injection) check for that var and no-op if present. Nothing
else needed touching (permissions, the unrelated =azos-send-alert=
=PreToolUse= hook) since those have no agent-shell-side equivalent to
conflict with.
- Both backends bundled where their tool gets installed, not separately
remembered: =azos.claude.enable= (=azos/features/claude=) turns on
the claude backend for both memory and skills; installing
=agent-shell= (=suites/editor/emacs=, via =azos-emacs-editor=) turns
on the agent-shell memory backend.
- Validated via =nix eval= against lauretta: both backends resolve
=true=, generated =.el= file has real paths substituted correctly,
generated Claude hook scripts have the guard clause in the right
place.
** Not verified — needs real interactive testing, not just nix eval
The Emacs Lisp itself (=agent-shell-mode-hook=, =agent-shell-subscribe-to=,
=map-elt=/=string-prefix-p= usage, whether =AZOS_AGENT_SHELL_SESSION=
actually propagates from agent-shell's subprocess env down into the
hook-command's own env) is a best-effort first pass written from reading
=agent-shell='s source, not from running it. =nix eval= can prove the
Nix side is wired correctly (confirmed) but can't prove the Elisp
actually behaves as designed once Emacs loads it. Needs a real rebuild +
interactive check: open an agent-shell session (any backend) and an
agent-shell Claude session specifically, confirm org-roam tools show up
in both, confirm editing a roam file resyncs without double-firing.
* DONE Live-test today's azos-core changes after rebuild
Everything below was validated with =nix eval= against lauretta (option
values resolve, derivations build, no eval errors) but =nix eval= can't
prove runtime behavior -- actual service startup, Emacs loading real
Elisp, hooks firing at the right time. Rebuild first
(=azos-lauretta-update=, run by the user -- needs sudo), then work
through this top to bottom. Mark each TODO/DONE as it's checked; where
something fails, reopen the matching azostodos item that produced it
rather than re-diagnosing from scratch (each one is linked below).
** DONE agent-shell / agent-memory backends (highest risk, least testable from Nix alone)
Live-tested 2026-10-13 against a *real* agent-shell Claude session: this
very Claude Code conversation turned out to be running *through*
agent-shell the whole time (confirmed via env — CLAUDECODE=1,
CLAUDE_AGENT_SDK_VERSION set, CLAUDE_CODE_ENTRYPOINT=sdk-ts), which made
this an unusually direct test rather than a separate exercise.
Found and fixed a real bug in the process (see the =tool-call-update=
fix commit): subscribed to *every* agent-shell event on this session's
buffer and made a real Write-tool edit. =file-write= never fired —
Claude's native Write/Edit tools write directly via the SDK, never
through agent-shell's client-side filesystem round-trip, which is the
only thing that event reports on. =tool-call-update= fired repeatedly
instead (pending -> pending-with-diff -> completed) and *is*
backend-agnostic by construction (every ACP agent must report tool
calls for the client's diff/permission UI). Switched the subscription to
that, filtered to completed edit calls, verified live it fires exactly
once for a real write under =~/roam=.
Confirmed, with evidence:
- =agent-shell-mcp-servers= populated correctly (real =/nix/store/...=
paths, correct =ORG_ROAM_DIR=/=ORG_ROAM_DB_PATH=).
- =AZOS_AGENT_SHELL_SESSION=1= genuinely present in this session's own
process environment — not just configured in Emacs, but actually
propagated through the full chain (agent-shell -> claude-agent-acp
subprocess -> SDK -> this session -> its own Bash tool calls).
- org-roam MCP tools (=search_nodes= etc.) work in this session — and
since =claude-agent-acp= never reads =~/.claude.json= (confirmed
earlier via source), this is proof the =agent-shell-mcp-servers= path
is what's actually serving them, not the Claude-specific registration.
- The corrected =tool-call-update= handler fires exactly once per
completed edit under =~/roam= (verified via live-loaded test handler
before writing the permanent fix).
- No =*Warnings*= buffer, clean =load-dir= pickup of
=~/.emacs.d/extra/azos-agent-memory.el=.
Not independently tested, noted as residual gaps rather than assumed:
- Session-start context injection specifically for a *non-Claude*
agent-shell session (no non-Claude equivalent exists yet, so nothing
to test there one way or the other).
- A second ACP backend end-to-end: =opencode= is actually installed
(=which opencode= resolves, =agent-shell-opencode= feature loads) and
would be the natural candidate, but starting a second interactive
agent-shell session non-interactively via =emacsclient= wasn't
attempted — diminishing returns given =agent-shell-mcp-servers= is
confirmed generic by construction (same function serves every backend,
verified in source) and already proven live for one backend.
- Plain =claude= standalone in a terminal (outside agent-shell) wasn't
separately re-run live in this pass, since this whole session *was*
the agent-shell path. Lower risk anyway: =~/.claude.json= mcpServers
and =~/.claude/settings.json= hooks (checked earlier, both present
and correct) are read identically regardless of agent-shell.
- Run plain =claude= standalone in a terminal (NOT via agent-shell).
Confirm: slash commands still work (=/todo=, =/project-brain=,
=/workplate=, =/work-history=, =/notify=), org-roam MCP tools are
available, session-start context injection still happens -- all
should be completely unaffected by any of today's changes.
Source: [[id:c52036a9-25ec-4b8b-8b72-90fcd9ea65dd][azos/exwm]] has the
EXWM-specific keybinding/architecture notes if agent-shell's own
keybindings seem off after the suite rename.
** DONE Notification system (azos-send-alert, renamed from homey-alert)
Confirmed 2026-10-13 live: =which azos-send-alert= resolves,
=which homey-alert= doesn't; =azos-bash-notify-hook='s script content
calls =azos-send-alert= (not =homey-alert=); =azos-send-alert -t "Test"
"..."= produced both a local desktop notification and a remote ntfy
push (first attempt failed on a PIN entry mistake, not a regression --
retry worked clean); =-r/--print-remote= printed a well-formed,
already-token-resolved =curl= command. Long-command-threshold live fire
not separately tested but the underlying mechanism is confirmed working
end to end.
- =which azos-send-alert= should resolve; =which homey-alert= should
**not** (confirms the rename is clean, nothing still refers to the
old binary name).
- =azos-send-alert -t "Test" "rebuild test"= -- should produce both a
local desktop notification (notify-send) and a remote ntfy push
(check the ntfy app/web client for ~alerts~).
- =azos-send-alert -r -t "Test" "remote test"= -- should print a
well-formed, already-token-resolved =curl= command (not an error, not
an unresolved =$(pass ...)= call).
- Check =~/.claude/settings.json='s =PreToolUse= hook (the long-command
notifier) references =azos-send-alert=, not =homey-alert=, in its
wrapped command text.
- Let a Claude Code Bash command run past the threshold (default 300s,
=azos.send-alert.longCommandThresholdSeconds=) and confirm the
long-command notification still fires correctly with the new name.
** DONE Broad regression sweep of everything else moved/genericized today
Live-tested 2026-10-13, directly on lauretta post-rebuild. Two of the
test plan's own binary-name guesses were wrong (not real gaps, just my
mistake writing the plan) -- corrected below for next time.
- *desktop suite (renamed from station)*: =which chromium qutebrowser
gimp libreoffice spotify zapzap zoom-us flameshot discord= -- all
present (package is =gimp3=, binary is =gimp=, not =gimp3= -- the
test plan had the wrong binary name). Discord confirmed present via
its new standalone =azos/features/discord=, not the desktop suite.
- *audio/binfmt/bluetooth -> desktop (new =nixos.desktop= module)*:
=bluetooth.service= active; pipewire is a *user* service
(=systemctl --user status pipewire/pipewire.socket=, both active) --
the system-level unit shows masked/inactive by design, not a
regression.
- *headphones -> exwm suite*: =azos-connect-headphones --list= shows
all 3 devices correctly.
- *chess -> editor suite*: =(featurep 'azos-emacs-chess)= -> t.
- *kubernetes/virtualization -> dev suite*: =libvirtd.service= active,
=virt-manager= present, =kubectl= correctly absent (default-false,
unchanged by the move).
- *hfsprogs -> desktop, NixOS-level/system-wide*: =mkfs.hfsplus=/
=fsck.hfsplus= resolve from =/run/current-system/sw/bin= -- system-wide
as intended, not just the user profile.
- *git-config genericized*: =git config user.name/user.email/
user.signingkey/credential.helper= all show the real values.
Stronger proof than planned: two real commits made *during this very
testing session* (the =tool-call-update= fix, both azos-core and
outer repo) were actually GPG-signed successfully end to end --
pinentry-emacs needed a restart + the user re-entering their PIN once
(unrelated pre-existing hiccup, not caused by today's changes), but
signing itself worked correctly with the genericized config.
- *encryption genericized*: =~/.ssh/gpg-as-ssh.pub= and
=~/.gnupg/sshcontrol= both show the real configured values;
=yubikey-personalization='s actual binaries (=ykinfo=/=ykpersonalize=
-- not =ykman=, that's a different package, =yubikey-manager= --
another test-plan naming mistake) resolve correctly.
- *printing genericized*: =aner= correctly in the =lp= group; =cups= and
=avahi-daemon= both active.
- *mail genericized*: =mbsync-timer.service= shows a real recent
successful run (=status=0/SUCCESS=) -- the presence-check-gated sync
(tied to =azos.encryption.yubikey.enable=) is working end to end on a
live timer fire, not just evaluating correctly.
Not independently exercised: browserpass actually autofilling in a live
chromium session (declarative config confirmed correct, GUI behavior
not opened and checked); a real print job (no printer currently
reachable to test against).
- *encryption genericized*: =pass <some-entry>= decrypts fine via
pinentry-emacs; =cat ~/.ssh/gpg-as-ssh.pub ~/.gnupg/sshcontrol= show
the real key; browserpass still autofills in chromium; =which
ykman= (or whatever yubikey-personalization provides) resolves.
- *printing genericized*: =groups aner | grep lp=; =systemctl status
cups avahi-daemon=; print something if a printer's actually reachable.
- *mail genericized*: =mu info= lists all three addresses; =systemctl
--user status mbsync-timer= is active; pull the YubiKey and confirm
=journalctl --user -u mbsync-timer= shows a silent skip (presence
check now tied to =azos.encryption.yubikey.enable= rather than
unconditional); replug and confirm sync resumes.
* TODO DAV calendar sync + workplate reading today's calendar
Extend the DAV setup ([[id:1196aa79-76ba-4090-a896-6e4938338bd1][azos/dav]]) to sync calendars (events) too — same vdirsyncer account machinery, khal and/or =ical2orgpy= for reading and the org view — and teach the =workplate= skill to take today's schedule into account when planning.