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

24 KiB
Raw Blame History

azos/todos

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 + 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: 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 (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.