Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 0 additions & 10 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,3 @@ plaintext/
/result
.wrangler/

# cliamp's runtime files. Since 2026-09-13 ~/.config/cliamp is a real directory
# with only config.toml and themes/ linked in (home/home.nix), so these no
# longer land here; the lines stay so a stale checkout or a revert cannot
# commit them again. resume.json carries a Navidrome stream URL with a Subsonic
# token — never track it.
home/dot_config/cliamp/cliamp.log
home/dot_config/cliamp/cliamp.sock
home/dot_config/cliamp/cliamp.sock.pid
home/dot_config/cliamp/history.toml
home/dot_config/cliamp/resume.json
13 changes: 13 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,16 @@
**Claude seats (2026-09-22).** Two Claude Code logins live on the coordinator
and nowhere else: `cc` is `~/.claude` (the personal Claude Max login, config file
`~/.claude.json`), `cc2` is `~/.claude-work` (the leger.run Claude Max login,
config file `~/.claude-work/.claude.json`), selected only by `CLAUDE_CONFIG_DIR`
in the fish launchers (`home/dot_config/fish/config.fish`). Both share the same
skills and settings links; login, history, sessions and trust are per seat, so a
session id resumes only on the seat and from the cwd that created it
(`claude-sessions` fans out over the seats). Claude never saves workspace trust
for `$HOME`, so the launchers move into `$CLAUDE_ENVELOPE` (default `~/today`)
when typed from `~`; never start a seat in the home directory. Logins are hand
`/login`s, never a delivered secret (the old claude-credentials seed was removed
this day). The seat meters are `~/.local/state/tally-rewrite/meters/<seat>.json`.

**Physical seats (2026-09-16, supersedes older headless/client-only wording below).**
Tom is returning the coordinator to primary-desktop duty with two upright LG
5K displays side by side at scale 2. Both coordinator and client have
Expand Down
32 changes: 32 additions & 0 deletions DECISIONS.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,37 @@
# DECISIONS

2026-09-23 the SessionEnd harvest hook is REMOVED; `harvest` stays a manual
verb.

MEM-2 (dotfiles#339) wired SessionEnd -> `harvest` so a closing session
distilled itself. The script was careful — it always exited 0, it always
logged one line, it timed itself out under Claude Code's own timeout — and
none of that was the problem. The problem is structural: the session process
WAITS for a SessionEnd hook before it exits. On the runs that actually
harvested, that wait reached 57 s, and Claude Code aborted the hook and
printed `SessionEnd hook [...] failed: Hook cancelled` at every close. The
ledger it kept is the argument against it: of the last 101 runs, 46 created a
note, 1 updated one, and 54 skipped — most of those `one cleaned
user/assistant turn exceeds the declared utility context`. Every session end
paid the latency; fewer than half bought anything with it.

Removed: the hook script, its home.nix link, the SessionEnd block in
home/dot_claude/settings.json, `checks.ai-memory-harvest-hook`,
tests/ai-memory-hook/, tools/mem-2-hook-oracle.sh, tools/mem-2-eval-probe.sh.
docs/local-ai/harvest-on-close.md is kept, banner-marked as removed.

Kept: `ai_memory.py`, the `harvest` verb, its `--enqueue` leg (FIX-E08,
dotfiles#348) and MEM-3's probe. The `drain` skill still runs the verb on
demand, so the capability is intact — only the automatic leg is gone.

The 2026-09-13 entry below noted that the ai-memory-harvest-hook check
asserted there is no SessionStart block, so a hook naming an unshipped script
could not return by accident. That guard is not lost: it is replaced by
`checks.no-claude-code-hooks`, which is strictly broader — it fails on ANY
hook block in settings.json, and on anything delivered into ~/.claude/hooks.
~/.claude/hooks itself stays a real, writable directory owned by no link.
Re-adding a hook of any kind is now a deliberate edit to that check.

2026-09-13 flake checkouts no longer ride into host closures, chrome-stream
is installed, and a switch refuses a stale raw-dotfiles checkout.

Expand Down
26 changes: 25 additions & 1 deletion docs/local-ai/harvest-on-close.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,28 @@
# Harvest on close — the SessionEnd hook
# Harvest on close — the SessionEnd hook (REMOVED 2026-09-23)

> **This mechanism no longer exists.** The SessionEnd hook, its script, its
> contract test, its flake check and its oracle were all removed on 2026-09-23.
> The document is kept as the record of what MEM-2 built and why it was undone;
> everything below describes the mechanism in the present tense as it stood
> until that date. Nothing below is live.
>
> **Why it went.** The session process WAITS for a SessionEnd hook before it
> exits. The runs that actually harvested took up to 57 s, so Claude Code
> aborted the hook and printed `SessionEnd hook [...] failed: Hook cancelled`
> on every close. Of the last 101 logged runs, 54 were skips — most of them
> `one cleaned user/assistant turn exceeds the declared utility context`. The
> cost was paid on every session end; the benefit landed on fewer than half.
>
> **What survives.** The `harvest` verb and `ai_memory.py` are untouched, and
> the `drain` skill still invokes them on demand. Only the automatic
> close-triggered leg is gone. MEM-3's probe (`tools/mem-3-eval-probe.sh`)
> still covers the verb.
>
> **The guard.** `flake.nix` now carries `checks.no-claude-code-hooks`, which
> fails if any hook block returns to `home/dot_claude/settings.json` or if
> anything is delivered into `~/.claude/hooks`. Re-adding a hook is a
> deliberate edit to that check.


MEM-2 (dotfiles#339). Mechanism: `~/sept8/MECHANISM-2026-09-07.md` §6b.
Decisions: `~/research-methods/DECISIONS.md` D-E07, D-E13, D-E14.
Expand Down
127 changes: 127 additions & 0 deletions docs/theme-switcher-2026-09-17.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
# Theme switcher: noir, claude-dark, claude-light (2026-09-17)

Three hand-curated themes, one palette source of truth in Nix, one symlink flipped
at runtime. No palette generation from wallpapers (matugen/pywal), no Home Manager
specialisations — see "Why not" below. Research report with the full option
analysis and the light-theme ANSI derivation: `~/colors/theme-switcher-design.md`
(not in this repo; ~/colors holds the colour investigation).

## The three themes

| theme | ground (terminal) | desktop | accents | source |
|---|---|---|---|---|
| `noir` | `#000000` | `#000000` | Claude Dark's 12-role syntax palette | Tom's catppuccin-noir; accents matched claude.ai's bundle bit for bit |
| `claude-dark` | `#1A1A1A` (code-block ground) | `#20201F` (`--bg-000`) | same as noir | `~/colors/waves/capture/claude-code-theme/claude-tokens-dark.json` |
| `claude-light` | `#F9F9F7` (`--bg-100`) | `#F0EFEC` (`--bg-300`) | Claude Light's 12-role palette | `claude-themes-SOURCE.json`, `claude-tokens-light.json` |

noir and claude-dark differ only in grounds (`home/themes/claude-dark.nix` is
`noir // { ground; muted; … }`). Claude ships no 16-slot ANSI palette, so
claude-light's is designed: roles fill slots 1–6, `--warning-100` fills the yellow
Claude has no role for, and `color0/7/15` are dark (punctuation / text-secondary /
fg) because herdr's `terminal` theme uses `White`/`Gray` as foregrounds. Every slot
passes WCAG AA on white. It is a proposal to tune in `~/colors/colorlab`.

## How it is wired

```
home/themes/{noir,claude-dark,claude-light}.nix palettes, keyed by ROLE
home/themes/default.nix render: palette -> per-app fragments
home/theme.nix HM: ~/.config/themes/<name>/{kitty.conf,niri.kdl,colors.fish,theme.lua,meta}
+ activation: bootstrap/heal ~/.config/theme
~/.config/theme -> ~/.config/themes/<name> the pointer; the ONLY runtime state
home/dot_local/bin/theme flip the pointer, fire reloads
```

Every RAW config keeps its body raw and joins its fragment through the app's own
include of `~/.config/theme/<fragment>` — the same move as `kitty-scrollback-nix.conf`
and `niri-local.kdl` (generated files at a neutral path, included from inside a
whole-dir RAW symlink). Home Manager owns `themes/` (plural); the switcher owns
`theme` (singular). Switching needs no rebuild; adding a theme or changing a
palette value does.

| consumer | joins via | live reload on `theme <name>` |
|---|---|---|
| kitty | `include ${HOME}/.config/theme/kitty.conf` (kitty.conf) | `kitten @ --to unix:@kitty-<pid> load-config` per instance (sockets from `/proc/net/unix`) |
| ghostty (cmux Browser panes) | `config-file = ?/home/tom/.config/theme/ghostty` LAST in ghostty/config.ghostty (a config-file loads after its parent, so the fragment wins; absolute because Ghostty resolves relative paths against the symlinked config's dir) | nothing — libghostty reads the config when cmux opens a surface; new panes follow |
| niri | `include optional=true "~/.config/theme/niri.kdl"` LAST in config.kdl; sections merge, later wins | `niri msg action load-config-file` |
| herdr | `[theme] name = "terminal"`: every token is an ANSI slot | nothing — kitty reports the bg change via DEC 2031 (`CSI ?997;n`), herdr re-queries OSC 10/11/4 and repaints chrome + every pane |
| nvim | `~/.local/bin/nvim-lua/theme.lua` dofile()s `theme.lua`; catppuccin/bufferline/lualine read it | `nvim --server <sock> --remote-expr` → `require('theme').reload()` per instance |
| fish | `conf.d/colors.fish` sources the fragment; re-sources at the next prompt when the pointer moved | automatic (no universal variables: `fish_variables` is tracked) |
| starship | already ANSI-named; the one `#F47B85` became `red` | follows kitty |
| Claude Code | `theme` key in `~/.claude.json` ← `meta` `claude_code=` | written tmp+rename; takes effect on next start |
| GTK3 | `gsettings gtk-theme` (`theme apply`) → MacTahoe-Dark-grey / MacTahoe-Claude-{Dark,Light}-orange | live: GTK3's Wayland backend reads org.gnome.desktop.interface from dconf and follows "changed". Needs the schema on XDG_DATA_DIRS (modules/common.nix) and no `GTK_THEME` env — both fixed in this PR |
| GTK4 / libadwaita | `~/.config/gtk-4.0/{gtk.css,gtk-dark.css,assets}` → `~/.config/theme/gtk-4.0/` (home/theme.nix); `color-scheme` via gsettings | color-scheme live (portal); gtk.css on next app start |
| icons / folder colour | `gsettings icon-theme MacTahoe[-<accent>]-{dark,light}` (`theme icons`) | live; accent from the wallpaper, polarity from the theme |
| Chrome | follows `color-scheme` through the portal | live |
| qt6ct | not yet | — |

**GTK themes.** `pkgs/mactahoe-gtk-theme.nix` builds MacTahoe from source per
`variant`: `oled` (noir, the existing OLED-black substitutions) and `claude` (both
the light and the dark branch of `src/sass/_colors.scss` recoloured to claude.ai's
tokens — bg-000/100/200, text-000/200/400, links = accent-100 — with the `orange`
accent slot set to Anthropic clay `#D97757`). Theme dirs: `MacTahoe-Claude-{Dark,Light}[-solid]-orange[-(x)hdpi]`.
`GTK_THEME` is gone from `environment.sessionVariables`: it pinned one theme for
the whole session and could not change live. It had been load-bearing only
because `gsettings-desktop-schemas` was never on XDG_DATA_DIRS, so GTK fell back
to `settings.ini`; verified with `gtk-query-settings` before and after.

**Wallpaper accents and folder colours.** `wallpaper <accent>` sets one of the
seven claude.ai/imagine grounds (oat olive cactus sky fig heather coral, all
rendered at 5120x2880 from the recovered SVG), remembers it in
`~/.local/state/wallpaper/accent`, and calls `theme icons`.
`pkgs/mactahoe-icon-theme.nix` prebuilds `MacTahoe-<accent>{,-light,-dark}` for all
seven — folders in the accent's darker ground (`--bg-primary-dark`) — so every
combination is on the system already; everything but the folder SVGs dedupes.

`F2` opens an fzf picker for the theme, `Shift+F2` one for the accent (wallpaper +
folder colour), both in the F1/F9/F10 prompt style; `Mod+Shift+T` cycles. `theme` prints the current name; `theme list`,
`theme apply` (re-fire hooks, also run at login from startup.kdl), `theme toggle`,
`theme icons`.

## Testing before a switch

`home/themes/default.nix` is pure (`{ lib }`), so a preview renders without a
generation:

```sh
nix eval --json --impure --expr 'let lib = (builtins.getFlake (toString ./.)).inputs.nixpkgs.lib; in (import ./home/themes { inherit lib; }).fragments' \
| python3 -c 'import json,sys,os; [ (os.makedirs(os.path.dirname(p:=os.path.expanduser("~/.cache/theme-preview/"+k.split("/",1)[1])),exist_ok=True), open(p,"w").write(v)) for k,v in json.load(sys.stdin).items() ]'
ln -sfn ~/.cache/theme-preview/noir ~/.config/theme
THEMES_DIR=~/.cache/theme-preview theme claude-dark
```

`home.activation.themePointer` re-targets a pointer left outside `~/.config/themes/`
to the live theme of the same name on the next switch.

## Why not

- **Home Manager specialisations**: HM runs as a NixOS module here
(`flake.nix`, `home-manager.nixosModules.home-manager`), so the base generation
re-activates at every boot and a specialisation does not survive a reboot; and
out-of-store symlinks are theme-invariant by construction, so every themed file
would have to leave the RAW doctrine.
- **matugen / pywal / wallust / stylix**: generators for palettes derived from a
wallpaper; three fixed palettes need only their *template + include + reload*
mechanics, which is what `default.nix` + `theme` are.
- **darkman**: two states, no "noir vs claude-dark". Could later *schedule*
`theme claude-light` / `theme <dark>` by time of day.

## Hazards

- A symlink retarget fires no watcher event: the explicit kitty/niri/nvim reloads
in `theme` are load-bearing.
- kitty's `dynamic_background_opacity` stays in the RAW kitty.conf: it cannot be
changed by reload and must be present at startup.
- herdr's in-app settings overlay upserts `[theme] name` and `auto_switch = false`
into the tracked `config.toml`. Do not use it; `terminal` is the one setting.
- `~/.claude.json` is rewritten by Claude Code itself: the switcher writes tmp+rename
and accepts a lost update against a running instance. Running agents keep their
theme until restarted.
- Client laptop: `git pull` ahead of `nixos-rebuild switch` leaves kitty/niri/fish/nvim
including a pointer that does not exist yet. niri's include is `optional`, nvim's
loader falls back to noir, fish and kitty fall back to their defaults — degraded,
not broken — until the switch renders `~/.config/themes/`.
- Kitty windows reload in place; agents' TUIs and GTK4 apps restart.
- Fonts (Anthropic Sans/Serif/Mono from ~/colors) are NOT part of this: another
session owns that spec; they are proprietary brand faces and must follow the
NAS `requireFile` pattern of pkgs/sf-pro.nix, never land in git.
20 changes: 10 additions & 10 deletions flake.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading