24 KiB
azos/todos
- Rename azos-core/features to azos-core/suites; enforce suite grouping
- Decouple the exwm/station coupling — ended up as a station rename, not a split
- Audit azos/features/* for portability into existing azos-core suites
- Make mail accounts a configurable attrset option
- (stretch) Make claude-memory/claude-skills backend-pluggable
- Live-test today's azos-core changes after rebuild
- DAV calendar sync + workplate reading today's calendar
Backlink: 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
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 + newnixos.desktopmodule)headphones->suites/exwm/(not desktop — user's call)chess->suites/editor/chess/(brought itsemacs/subdir along too —_pkg.nix=/=config.org=/=default.nixdefining theazos-emacs-chesspackage; almost missed this on the first pass since it wasn't visible in a maxdepth-1default.nixread, only in the directory listing)kubernetes->suites/dev/kubernetes/(home-manager)virtualization->suites/dev/virtualization/, the firstnixos.devmodule (dev suite previously had no NixOS side)hfsprogs->suites/desktop/hfsprogs/, as a NixOS module (environment.systemPackages, nothome.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=/=userEmailmandatory; signing and the pass-git-helper credential helper (passCredentialHelper) each independently toggleable, mapping content also now an option instead of a committed.inifile.encryption->suites/base/encryption/: split into independently toggleable pieces — base gpg-agent setup (always on whenencryption.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 toazos-send-alert(the command — "homey" was personal selfhosted-infra naming), landing insuites/base/azos-send-alert/, next to claude-memory/claude-skills. The remote-notification half (previously a hardcoded ntfy curl call) is nowazos.send-alert.remoteCommand, an arbitrary shell command run with TITLE/PRIORITY/TAGS/MESSAGE in its env; local notify-send fallback unchanged.--print-remotestill works: setsAZOS_SEND_ALERT_PRINT_REMOTE=1and captures the command's stdout, by convention the command resolves local secrets and prints a self-contained invocation.azos/features/homey-alertnow just hosts the concrete ntfy-backedazos-homey-ntfy-alertoverlay package, plugged in viaazos.send-alert.remoteCommandin home.nix.printing->suites/base/printing/(nixos side): hardcoded "aner" username is nowazos.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, themu init --my-address…= list derived from whateveraccounts.email.accountsactually contains (vialib.mapAttrsToList) instead of hardcoded, andmbsyncSafe(new-mail count diff + notify-send, already fully generic).- The hardcoded
azos-card-presentsmartcard gate insidembsyncSafebecameazos.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/mailwires it to${pkgs.azos-card-present}/bin/azos-card-presentbut only conditionally, whenazos.encryption.yubikey.enableis 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.nixkept thedefault_account_params=/ =default_gmail_params=/=default_smtpconvenience merge-helpers and the three real accounts (personal boilerplate, not mechanism) and just setsaccounts.email.accounts+azos.mail.enable+ the presence-check wiring directly.- Validated via targeted
nix evalagainst lauretta: all three addresses present,mu inithas the derived--my-addressflags, presence check resolves to the realazos-card-presentbinary 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 ACPsession/newregardless of backend — genuinely the "memory regardless of model" mechanism.agent-shell-subscribe-toexposes afile-writeevent (:path,:content) — a cleaner, backend-agnostic equivalent to Claude Code'sPostToolUsehook for the org-roam-resync-on-edit behavior.- Initially proposed isolating Claude-via-agent-shell with
CLAUDE_CONFIG_DIRpointed 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 shimazos/features/claudeinstalls) 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 dedicatedsession-hooks.test.tsproving 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.enablerenamed toazos.agent-memory=/=azos.agent-skills, both reshaped asbackends.<name>.enableattrsets instead of a single flag.agent-skillshas one backend (claude) — no cross-backend mechanism exists for slash-commands/CLAUDE.md, so nothing to add alongside it yet.agent-memorygets a second backend,agent-shell: drops~/.emacs.d/extra/azos-agent-memory.el(auto-loaded via the existingload-dirsetup insuites/base/emacs, not a compiledazos-emacs-*package, since it needs real Nix-interpolated paths) registeringorg-roam-mcpinagent-shell-mcp-serversand subscribing tofile-writefor resync.- Double-fire fix: the
agent-shellbackend appendsAZOS_AGENT_SHELL_SESSION=1toagent-shell-anthropic-claude-environment; theclaudebackend's two memory-specific hooks (PostToolUseresync,SessionStartcontext injection) check for that var and no-op if present. Nothing else needed touching (permissions, the unrelatedazos-send-alertPreToolUsehook) 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; installingagent-shell(suites/editor/emacs, viaazos-emacs-editor) turns on the agent-shell memory backend. - Validated via
nix evalagainst lauretta: both backends resolvetrue, generated.elfile 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-serverspopulated correctly (real/nix/store/...paths, correctORG_ROAM_DIR=/=ORG_ROAM_DB_PATH).AZOS_AGENT_SHELL_SESSION=1genuinely 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_nodesetc.) work in this session — and sinceclaude-agent-acpnever reads~/.claude.json(confirmed earlier via source), this is proof theagent-shell-mcp-serverspath is what's actually serving them, not the Claude-specific registration. - The corrected
tool-call-updatehandler fires exactly once per completed edit under~/roam(verified via live-loaded test handler before writing the permanent fix). - No
*Warnings*buffer, cleanload-dirpickup 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:
opencodeis actually installed (which opencoderesolves,agent-shell-opencodefeature loads) and would be the natural candidate, but starting a second interactive agent-shell session non-interactively viaemacsclientwasn't attempted — diminishing returns givenagent-shell-mcp-serversis confirmed generic by construction (same function serves every backend, verified in source) and already proven live for one backend. - Plain
claudestandalone 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.jsonmcpServers and~/.claude/settings.jsonhooks (checked earlier, both present and correct) are read identically regardless of agent-shell. - Run plain
claudestandalone 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: 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-alertshould resolve;which homey-alertshould 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 foralerts).azos-send-alert -r -t "Test" "remote test"– should print a well-formed, already-token-resolvedcurlcommand (not an error, not an unresolved$(pass ...)call).- Check
~/.claude/settings.json'sPreToolUsehook (the long-command notifier) referencesazos-send-alert, nothomey-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 isgimp3, binary isgimp, notgimp3– the test plan had the wrong binary name). Discord confirmed present via its new standaloneazos/features/discord, not the desktop suite. - audio/binfmt/bluetooth -> desktop (new
nixos.desktopmodule):bluetooth.serviceactive; 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 --listshows all 3 devices correctly. - chess -> editor suite:
(featurep 'azos-emacs-chess)-> t. - kubernetes/virtualization -> dev suite:
libvirtd.serviceactive,virt-managerpresent,kubectlcorrectly absent (default-false, unchanged by the move). - hfsprogs -> desktop, NixOS-level/system-wide:
mkfs.hfsplus=/ =fsck.hfsplusresolve 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.helperall show the real values. Stronger proof than planned: two real commits made during this very testing session (thetool-call-updatefix, 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.puband~/.gnupg/sshcontrolboth show the real configured values;yubikey-personalization's actual binaries (ykinfo=/=ykpersonalize– notykman, that's a different package,yubikey-manager– another test-plan naming mistake) resolve correctly. - printing genericized:
anercorrectly in thelpgroup;cupsandavahi-daemonboth active. - mail genericized:
mbsync-timer.serviceshows a real recent successful run (status=0/SUCCESS) – the presence-check-gated sync (tied toazos.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/sshcontrolshow 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 infolists all three addresses;systemctl --user status mbsync-timeris active; pull the YubiKey and confirmjournalctl --user -u mbsync-timershows a silent skip (presence check now tied toazos.encryption.yubikey.enablerather than unconditional); replug and confirm sync resumes.
TODO DAV calendar sync + workplate reading today's calendar
Extend the DAV setup (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.