: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..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 = 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.