Compare commits

..
Author SHA1 Message Date
Frank 3022277167 Stop the deploy tier from reverting live theme state
Three findings from verifying the #41 merge against the live box with
`deploy.sh --check`. One commit because they share the deploy tier, its
documentation, and the first test file that tier ever had.

**1. Generated palette output was tracked, and sync would have restored it over a
live theme.** `dotfiles/zz-blankpad/niri/.config/niri/noctalia.kdl` (`b3d3df4`,
2026-10-18) sat byte-identical to the live file — harmless while the palette held, and
`--check` was already proposing `ADOPT … noctalia.kdl`. The moment the theme changed, the
next sync would have quarantined the live file and reinstated 2026-10-18's colours.
`deploy.sh` now refuses the class outright (`SKIP … (generated theme output)`), `.gitignore`
carries the matching patterns so it cannot be re-added, and the file is untracked.

**2. A `zz` override older than the file it overrides.** `dotfiles/zz-blankpad/niri/…/
layout.kdl` held focus ring `width 2` / `#e01b24` (`7b7dbf5`, 2026-10-17) while base
absorbed `b3d3df4` (2026-10-18): `width 4`, `#ff8c00`. Stow order makes `zz-*` win, and a
cross-package owner change relinks *silently* — no quarantine, no warning. Live was linked
to base, the newer intent, so `--check` printed `RELINK … layout.kdl` every run and one
unattended `deploy.sh` would have reverted the amber focus ring. The duplicate is deleted;
base owns the file.

**3. Two topologies I had asserted and not measured.** The media law and the `.gitignore`
comment both said `~/.local/share/noctalia/wallpapers` symlinks into the repo, so deleting
the tracked copies would blank the desktop. Measured: it is a real directory of real files
that Noctalia owns; the repo held a duplicate, and the #41 merge deleted that duplicate
without the desktop flickering. The correct general law replaces the false one — untracking
a path deletes the file in every other checkout, `git rm --cached` on the branch
notwithstanding — and the restore procedure now points at the app's directory, which
`deploy.sh` does not manage.

The guard is `tests/deploy.bats`, black-box over exit status, stdout, the symlink shape
under `$HOME`, and the quarantine directory. Before the change: test 1 and test 2 fail,
test 2 because the live palette really is quarantined and reverted. After: 4/4, including
the control that an ordinary identical config is still adopted — an exclusion nobody can
turn off is just a broken tool.

Verification: `bats tests/` 66/66, `kb-lint.sh omp-docs` 0 violations, `kb-coverage.py` 0
issues, and live `deploy.sh --check` on blankpad reports `SKIP … (generated theme output)`
with `actions=0` instead of `ADOPT … noctalia.kdl`. The real `deploy.sh` run that clears the
remaining `RELINK` lines is deliberately left until this is in main — running it from a
worktree would repoint live symlinks into a checkout the heartbeat reaps.

Refs #42
2026-10-20 17:08:06 -07:00
dank 689ada428f Merge pull request 'Quarantine what the gate could not see; delete what the repo only pretended to own' (#41) from fix/40-dotfiles-hygiene into main 2026-10-20 17:02:53 -07:00
Frank 9ebf5fe85f Untrack a retired niri config that was committed as trash; correct two census rows
The repo still tracked `dotfiles/niri/.config/niri/cfg/.AGENTTRASH/display.kdl.superseded`
— a retired output config committed 2026-10-09, five days before `.gitignore` grew the
`.AGENTTRASH/` rule. Tracked-then-ignored: an ignore rule never reaches an already-tracked
path, so `git rm --cached` is the only repair. The working copy joins the rest of today's
tracked debt.

Found while answering "does this repo still track my noctalia and niri settings", which
also falsified two rows of the census committed yesterday: niri is 9 tracked files, not
12; noctalia is 3 authored files against 2672 files of app state and downloaded catalog,
not 2/0. The page's own classifier command reproduces the new numbers — it never
reproduced the old ones, which is the only reason they were wrong and not merely
different.

Two laws recorded alongside, both from the same probe: `config.kdl`'s
`include "noctalia.kdl"` is not `optional=true`, so a fresh box cannot start niri until
the theme pipeline has run once; and `cfg/display.kdl -> desk-1-home.kdl` is desk state a
fresh box does not receive — by design, but nowhere written down.

Refs #40
2026-10-20 17:00:20 -07:00
Frank 86280608e5 Refresh the userspace census from live measurements
`inventory/home-config-census.md` was dated 2026-08-28, tagged `unverified`,
and every path in it was `/home/dank` — it described another box, yet
`inventory/index.md` still advertised it as the input for the pruning session,
so the pruning session would have pruned against fiction.

Rebuilt from this box, purpose stated in line one. Classification is per FILE
because the tracked unit is a symlink into `dotfiles/`: a directory can look
"real" while every file inside it is tracked (`niri` 12/1, `gtk-3.0` 3/0), and
a directory can be one whole symlink (`lazyvim`). The classifier command is in
the page so tomorrow's agent reruns it instead of trusting the table.

The generated/authored split is the load-bearing part: 24 `noctalia*` palette
files across 20 apps are one generator pass, proven by the
`2026-10-14 19:05:40.330…40.813` same-second cluster and later single re-stamps
(`bat` 10-18, `niri` 10-18, `mango` 10-17). A same-second mass write is
generation, never authorship — so the source palette stays tracked and the
expansion never is. Adoption candidates are limited to programs that pass
`command -v`, with authorship read from the file; the prune list extends the
expected 14 with `pywalfox`, `qt5ct`, `qt6ct`, `turborepo`, and notes that six
of the orphan dirs contain nothing but generated palette.

`fib-watchdog.{service,timer}` is recorded as owned by the bench repo, not
adopted here: its `ExecStart` and `WorkingDirectory` are that project's
internals. It is flagged as a reinstall risk instead, which is the honest
status. Deleted: the `/home/dank` rows, `~/.pi`, Steam library sizes, and the
old offender/summary tables — numbers for software that is not installed here
fail the purpose test.

Refs #40
2026-10-20 16:48:33 -07:00
Frank 411b32ff59 Stop tracking fiction, and stop carrying 80 MiB of wallpaper per clone
Three things were in history that should not have been, all invisible to the
tool that deploys them:

- `home/dank/.omp/dotfiles/**` — 3 files re-hosting `niri/cfg/autostart.kdl`,
  `niri/cfg/keybinds.kdl` and `systemd-user/…/swayidle-lock.service` under a
  prefix `scripts/deploy.sh` never reads (it walks `dotfiles/<pkg>/`). The live
  package is deployed and symlinked, so the copy asserted nothing; it only
  misled. Moved to `.AGENTTRASH/20261020-tracked-debt/`.
- `.pre-pull-backup/**` — 2 managed-skill files, a local backup in history,
  which `.gitignore` itself declares must stay on disk only. Same destination.
- `dotfiles/noctalia/.local/share/noctalia/wallpapers/` — 23 images, 80.4 MiB.
  Untracked, files left in place: `~/.local/share/noctalia/wallpapers` symlinks
  into this directory and the theme reads it, so deleting them would have gone
  black. Every filename carries its wallhaven ID, so a fresh box restores the
  set with one `curl` per file plus `deploy.sh`; losing the set is an acceptable
  outcome, 80 MiB per clone forever is not.

New page `omp-docs/tools/dotfiles-deploy.md` owns the deploy contract (the
`--check`/sync/`--adopt` modes, `zz-$(hostname)` override semantics, the dconf
and systemd-user special cases) because nothing documented it, plus two laws:
media the pipeline consumes is content not config, and `systemctl --user
enable` snapshots an absolute path — so enabling from a worktree means the
seven-day reap silently kills the unit while `is-enabled` keeps answering
`enabled`. That second law is not theoretical:
`default.target.wants/blankpad-desk-autoswitch.service` pointed into the reaped
`wt/de-redesign-20261009` and desk auto-switching had been off since. It is
relinked to `dotfiles/zz-blankpad/` and left inactive rather than started mid-
session; starting it applies a desk profile.

Refs #40
2026-10-20 16:48:33 -07:00
Frank 04f93133fc Close the secret-gate scope hole, and make it fail closed
The gate scanned only `*.md *.yml *.yaml *.json *.toml *.sh *.ts *.js`. A
`postgres.env`, a `.container` quadlet, an `.ini`, or an extension-less dotfile
— exactly the shapes the dotfiles tier is made of — was never read at all.
Discovered while deciding what else belongs in `dotfiles/`: `~/.config`
carries `containers/systemd/postgres.env` with a real `POSTGRES_PASSWORD`,
which the ignore list did not cover either (`git check-ignore` says so: `.env`
does not match `postgres.env`).

Token SHAPES now scan the entire staged diff, every file type; bcrypt hashes
and `Authorization: Basic` headers joined the shape list. Credential
ASSIGNMENTS are scanned in non-prose files only — the auto-learned memory tier
is prose *about* credentials (7 such hits measured on the tracked tree), and a
gate that fires there gets overridden until it means nothing. Markdown keeps
shape detection, which is what it had before.

Three invariants, each pinned by a test in tests/secret-gate.bats (13 tests,
black-box: the hook is copied into a scratch repo and git itself runs it):

- Fail closed. The first version of this rewrite passed `-I` to `git diff`,
  which is not valid there; the empty file list made every secret commit sail
  through, and a blind gate looks exactly like a clean tree. If the hook cannot
  read the staged diff it now refuses the commit.
- Only ADDED lines can leak. The gate refused the redaction commit in this
  branch because the diff still carried the removed password — a gate that
  blocks the removal of a secret holds the leak hostage.
- Path-valued variables (`…_FILE=`, `…_PATH=`) and placeholder/indirected
  values are not secrets, so the gate stays worth listening to.

`.gitignore` quarantines `*.env` at any name, and gains the theme-media rule
whose untracking lands in the next commit. `AGENTS.md` states the new contract.

Refs #40
2026-10-20 16:48:33 -07:00
Frank 7da5e9dfa8 Redact a live credential that escaped into tracked memory
The auto-learned memory tier for serverAppConfigs carried, in plaintext, the
Caddy basicauth bcrypt hash for dankuser, the password behind it, and the
Pi-hole internal password. The file is tracked, so all three are in git
history on every machine that has pulled this repo, and in every agent
context window that has read the bucket.

Operational knowledge preserved in full (which auth layer fails, how to
reset, that the CLI hash is what CivetWeb validates, hot-reload through the
file watcher); only the credential values removed.

Both credentials still need rotation: redaction does not un-leak history.
That is an operator action on Dankclaw, not something this commit can do.

Refs #40
2026-10-20 16:47:17 -07:00
Frank 0d43eb0948 live: heartbeat sync 2026-10-20T16:17:41-07:00 (Refs #11) 2026-10-20 16:17:41 -07:00
dank 5110060c24 Merge pull request 'Configure the engineering skills against this repo and this KB' (#39) from feat/agent-skills-config into main 2026-10-20 16:14:43 -07:00
57 changed files with 897 additions and 632 deletions
+87 -10
View File
@@ -1,25 +1,102 @@
#!/usr/bin/env bash
# Secret gate: block any commit whose staged diff contains live-secret shapes.
# This repo is pushed to a Gitea remote; anything committed is instantly visible
# Secret gate: block any commit whose staged diff contains live-secret shapes or
# credential assignments. This repo IS the deployment and every workstation
# pulls it, so a committed secret is instantly in every agent's context window
# on the fleet. Deliberate override: ALLOW_SECRETS=1 git commit ...
#
# Two rules, deliberately different in scope:
#
# 1. Token SHAPES are scanned across the ENTIRE staged diff, whatever the file
# type. The first version of this gate whitelisted extensions
# (*.md *.yml *.yaml *.json *.toml *.sh *.ts *.js), so a `postgres.env`, a
# `.container` quadlet, an `.ini`, or an extension-less dotfile — exactly
# the shapes the dotfiles tier is made of — was never read at all.
# 2. Credential ASSIGNMENTS (PASSWORD=…, secret_key: …) are scanned only in
# non-prose files. The auto-learned memory tier is prose ABOUT credentials,
# and a gate that fires there gets overridden until it means nothing; the
# 2026-10-20 audit measured 7 such prose hits on the tracked tree. Markdown
# keeps shape detection only, which is what it had before.
#
# Bcrypt hashes and Basic-auth headers joined the shape list on 2026-10-20
# (issue #40) after a live Caddy basicauth hash plus its plaintext password —
# and the Pi-hole internal password — were found in a tracked memory file.
set -uo pipefail
staged=$(git diff --cached --diff-filter=ACM -U0 -- '*.md' '*.yml' '*.yaml' '*.json' '*.toml' '*.sh' '*.ts' '*.js')
[ -z "$staged" ] && exit 0
shapes='sk-[A-Za-z0-9]{18,}|ghp_[A-Za-z0-9]{36}|github_pat_[A-Za-z0-9_]{22,}|glpat-[A-Za-z0-9_-]{20}|xox[baprs]-[A-Za-z0-9-]{10,}|AKIA[A-Z0-9]{16}|ASIA[A-Z0-9]{16}|BEGIN [A-Z ]*PRIVATE KEY|aes_[A-Za-z0-9/+]{40}|eyJ[A-Za-z0-9_-]{20,}\.ey|\$2[aby]\$[0-9]{2}\$[./A-Za-z0-9]{20,}|Authorization:[[:space:]]*[Bb]asic[[:space:]]+[A-Za-z0-9+/=]{24,}'
patterns='sk-[A-Za-z0-9]{18,}|ghp_[A-Za-z0-9]{36}|github_pat_[A-Za-z0-9_]{22,}|glpat-[A-Za-z0-9_-]{20}|xox[baprs]-[A-Za-z0-9-]{10,}|AKIA[A-Z0-9]{16}|ASIA[A-Z0-9]{16}|BEGIN [A-Z ]*PRIVATE KEY|aes_[A-Za-z0-9/+]{40}|eyJ[A-Za-z0-9_-]{20,}\.ey'
# `newpass1234`, `secret_key: "rotated-value"` — an assignment with a literal
# value. `$VAR`, `${VAULT}`, <placeholders> and empty values cannot match.
assign='[A-Za-z0-9_]*(PASSW|SECRET|TOKEN|API_?KEY|ACCESS_KEY|PRIVATE_KEY|CREDENTIAL)[A-Za-z0-9_]*[[:space:]]*[:=][[:space:]]*["'"'"']?[A-Za-z0-9/+._@-]{8,}'
placeholders='changeme|change[-_]|placeholder|redacted|example|sample|dummy|nokey|no[-_]key|not[-_]needed|your[-_]|localhost|xxxx|yyyy|\$\{|<[a-z]|\*\*\*|%s|null|false|true'
# Exemptions: a variable naming a location (`…_FILE=`, `…_PATH=`) or a value
# that is a filesystem path is a pointer, not a credential. Flagging those is
# how a gate gets permanently overridden.
assign_exempt='(_FILE|_PATH|_DIR|_URL|_ENDPOINT|_SOCKET)[[:space:]]*[:=]|[:=][[:space:]]*["'"'"']?(/|\.\./)'
# Fail closed: a gate that cannot read the staged diff must not report success.
if ! files=$(git diff --cached --diff-filter=ACM --name-only 2>&1); then
{
echo "SECRET GATE: cannot read the staged diff — refusing to commit."
printf ' %s\n' "$files"
echo "Override deliberately: ALLOW_SECRETS=1 git commit ..."
} >&2
[ -n "${ALLOW_SECRETS:-}" ] && exit 0
exit 1
fi
# stderr is captured so the failure branch can quote it; on success it must not
# leak a `warning:` line into the file list and have it mistaken for a path.
files=$(printf '%s\n' "$files" | grep -vE '^(warning|fatal|error):' || true)
[ -z "${files//[[:space:]]/}" ] && exit 0
report=""
exempt=""
count=0
while IFS= read -r f; do
[ -z "$f" ] && continue
# Self-exemption, deliberately an exact filename list and not `tests/`: the
# gate's fixtures must contain credential-shaped strings or there is nothing to
# test against, and this hook documents the shapes it looks for. A secret parked
# in any other test file is still caught.
case "$f" in
.githooks/pre-commit|tests/secret-gate.bats) exempt="$exempt $f"; continue ;;
esac
diff=$(git diff --cached -U0 -- "$f")
[ -z "$diff" ] && continue
# Only ADDED lines can leak. Matching removals too blocks the very commit that
# deletes a credential — which is exactly what happened on 2026-10-20, when the
# gate refused the redaction of the password it exists to catch.
added=$(printf '%s\n' "$diff" | grep -E '^\+' | grep -vE '^\+\+\+' | sed 's/^+//' || true)
[ -z "$added" ] && continue
hits=$(printf '%s\n' "$added" | grep -Ei "$shapes" || true)
if [ "${f##*.}" != "md" ]; then
assigns=$(printf '%s\n' "$added" | grep -Ei "$assign" | grep -Ev "$assign_exempt" | grep -Eiv "$placeholders" || true)
hits=$(printf '%s\n%s\n' "$hits" "$assigns" | sed '/^[[:space:]]*$/d')
fi
if [ -n "$hits" ]; then
n=$(printf '%s\n' "$hits" | wc -l)
count=$((count + n))
report+=" $f"$'\n'"$(printf '%s\n' "$hits" | cut -c1-100 | sed 's/^/ /')"
fi
done <<< "$files"
# Say so out loud: an exemption that prints nothing is how a carve-out becomes
# invisible, and invisible is the property a secret wants.
if [ -n "$exempt" ]; then
printf 'secret-gate: self-test fixtures exempt:%s\n' "$exempt" >&2
fi
hits=$(printf '%s\n' "$staged" | grep -Ei "$patterns" || true)
if [ -n "${ALLOW_SECRETS:-}" ]; then
[ -n "$hits" ] && printf 'secret-gate: ALLOW_SECRETS set, allowing %s secret-shaped string(s) through.\n' "$(printf '%s\n' "$hits" | wc -l)"
[ -n "$report" ] && printf 'secret-gate: ALLOW_SECRETS set, allowing %s secret-shaped string(s) through.\n' "$count"
exit 0
fi
if [ -n "$hits" ]; then
if [ -n "$report" ]; then
{
echo "SECRET GATE: staged diff contains $(printf '%s\n' "$hits" | wc -l) secret-shaped string(s):"
printf '%s\n' "$hits" | sed 's/^/ /'
echo "SECRET GATE: staged diff contains $count secret-shaped string(s):"
printf '%s\n' "$report"
echo "Fix it, or override deliberately: ALLOW_SECRETS=1 git commit ..."
echo "A credential that reaches a tracked file is already in history on every"
echo "machine that pulls this repo — deleting it later does not un-leak it."
} >&2
exit 1
fi
+24
View File
@@ -26,6 +26,10 @@ auth.json
**/auth.json
.env
**/.env
*.env
# `*.env` is the rule that matters: `.env` alone does not match `postgres.env`
# or `frank-job-hunt.env`, and those are the files that actually carry
# POSTGRES_PASSWORD / API keys on this fleet (issue #40).
trust.json
**/trust.json
*.pem
@@ -74,3 +78,23 @@ wt/
# A session run inside a git worktree mints a bucket named after the worktree
# path; the worktree is deleted, the bucket is orphan state, not config.
agent/memories/--home-blank-.omp-wt-*
# --- theme media: content the pipeline consumes, not configuration ---------
# 23 wallpapers, 80 MiB. Tracked until 2026-10-20: every clone of a config repo was
# carrying a library no diff can review. The live library at
# ~/.local/share/noctalia/wallpapers is a real directory of real files (measured
# 2026-10-20), so this path in the repo was a duplicate of it, not its source — and
# note that untracking deletes the copy in every other checkout regardless of
# `git rm --cached` on the branch. A fresh box starts with an empty library; every
# filename carries its wallhaven ID, so `curl -L -o <name> https://wallhaven.cc/w/<id>`
# puts it back. Law: omp-docs/tools/dotfiles-deploy.md
dotfiles/noctalia/.local/share/noctalia/wallpapers/
# --- generated theme output: machine-written, never reviewable, never deployed ---
# noctalia/matugen rewrite these in place; a tracked copy is a stale snapshot that
# deploy.sh would push back over a live palette. Patterns match the exclusion list in
# scripts/deploy.sh. Law: omp-docs/inventory/home-config-census.md, Refs #42
dotfiles/**/themes/noctalia*
dotfiles/**/colors/noctalia*
dotfiles/**/noctalia.*
dotfiles/**/noctalia-*
@@ -1,28 +0,0 @@
---
name: command-center-dotfiles-import
description: Runbook for importing client env-layer configs into the command-center repo and deploying them via stow on new fleet machines; decisions agreed with Frank 2026-09-09 (pre-execution — verify execution state before assuming done).
---
Design agreed with Frank for importing client application/environment configs into the `~/.omp` command-center repo and deploying them on fresh machines. **Status: plan agreed, not yet executed** — check repo state (does `dotfiles/` or `scripts/deploy.sh` exist?) before re-deriving; if already implemented, this becomes the operating model, not a to-do.
## Agreed decisions (each was an explicit user choice)
1. **Target**: unknown/TBD fresh machine → design stays host-agnostic; deploy derives overrides from `$(hostname)` at run time. Nothing hardcodes a target.
2. **Scope**: environment layer only — niri (+cfg includes), gtk-3.0/4.0 settings, ghostty, alacritty, shell rc files (`.zshrc`, `.bashrc`, `.profile`, `.p10k.zsh`), fish, systemd user units, autostart, dconf. App *data* (browser profiles, vaults, DBs) never imported.
3. **Layout**: stow-style package dirs `dotfiles/<pkg>/<home-relative path>` inside the repo; repo clones to `~/.omp` on every host that runs the harness (full command-center clone, not env-layer only).
4. **Deploy mechanism**: symlink farm — `stow -t ~` from `~/.omp/dotfiles`; relative symlinks within `$HOME` work because repo root lives inside `$HOME`.
5. **Dogfood order**: convert the workstation itself (real dirs → symlinks via adopt) BEFORE any target import; iterate where the compositor reload is observable.
6. **Secrets policy**: never commit secret values. Split rc files into committed logic + untracked sourced-if-exists stub (`~/.config/environment.d/secrets.env` or `.zshrc.local`); real values provisioned out-of-band only on machines that need them. Pre-commit secret scanner stays as safety net, not gate of first resort.
7. **Divergence**: `zz-<hostname>` override package stowed LAST with `--override`; only per-host diffs live there. (Stow descends into conflicting symlinked dirs — per-file overrides only work via this last-stowed `--override` pattern.)
8. **Deploy trigger**: explicit `scripts/deploy.sh` invocation, human- or agent-invoked. NO auto-deploy on pull — pull and deploy are separate decisions; `--check` = drift report mode.
9. **Ownership**: Frank is sole push authority; exactly one editing agent at a time → no multi-writer race machinery needed. Each host's agent edits only its own `zz-<hostname>` package.
10. **Conflict policy**: pre-existing regular file that differs from repo truth → move to timestamped `.quarantine/` dir on the target (never `rm`, same convention as this repo's trash rule), symlink anyway, print every replacement in deploy output and `--check` report. Repo truth always wins; divergence recoverable from quarantine.
11. **First-clone auth**: human types credential once console-side (URL-with-token or prompt); the new machine's secrets exist only there afterward.
## Implementation gotchas baked into the design
- `core.hooksPath` is a per-clone local setting — does NOT survive clone; bootstrap/deploy must set it on every fresh clone or the secret-gate pre-commit hook silently doesn't run there.
- dconf/gsettings are databases, not files: import as `dconf dump /` text into repo; deploy runs `dconf load` instead of symlinking.
- p10k installer and toolchain hooks append to rc files at install time — expect conflicts on `.zshrc`-class files at first adopt; quarantine-and-replace handles it, but diff live-vs-repo before adopting so pre-existing drift is seen, not silently adopted.
- Known secret at import time: `GITEA_TOKEN` export lived in `.zshrc` (line ~22) on the workstation — quarantine BEFORE importing rc files; consider token rotation at cutover since it had been exposed in plaintext rc + remote URL.
## Deploy script spec agreed
`scripts/deploy.sh`: sets hooksPath → stows all base packages → stows `zz-$(hostname)` with `--override` → first run uses `--adopt` for pre-existing identical files → divergent files quarantined-and-reported; `--check` mode emits drift report (modified tracked files vs deployed symlinks) without mutating.
@@ -1,33 +0,0 @@
---
name: stash-supersession-audit-before-drop
description: "Before dropping a stale git stash, prove it's superseded by HEAD via per-file line-set diff of git show stash@{0}:file vs HEAD; archive as an annotated tag and drop only after auditing unique facts — zero-loss alternative to blind drop."
---
# Stash Supersession Audit Before Drop
Use when a repo (especially a live-deployment checkout where the working tree IS the deployment, e.g. `~/.omp` command-center) has an old stash whose content may have been superseded by later commits, and dropping blindly risks losing unique facts.
## Procedure
1. **Inventory the stash** — see which files it touches:
```bash
git stash show --stat stash@{0}
```
2. **Probe for unique content per file.** For each touched file, diff stash version against HEAD and count lines that exist only in the stash version:
```bash
git show "stash@{0}:<path>" > /tmp/stash_copy
# compare normalized line sets; report lines present in stash but absent from HEAD
```
A file with `unique-vs-HEAD = 0` is fully superseded — safe to drop for that file.
3. **Classify each unique-content hit**:
- *Stale fact* (superseded by a later refactor, e.g. old model/provider naming) → drop is correct.
- *Live durable fact* (e.g. an operational invariant never re-recorded) → extract it into the live memory/docs file or flag to the user before dropping.
4. **Archive before dropping** — zero-loss: tag the stash commit, then drop from the stash list:
```bash
git tag archive/stash-<YYYY-MM-DD>-<base-branch>-wip "stash@{0}"
git stash drop "stash@{0}"
```
The tag pins the full stash object (including its index parent); recoverable later via `git show archive/stash-...` or `git checkout archive/stash-...^3 -- <path>` for the untracked-files third parent if present.
## Why
Stashes are invisible in normal history and expire from reflogs (~90 days). A stash that *looks* superseded can still hold the only copy of a durable fact; tagging converts "drop" into a reversible archive with zero workflow cost.
+15 -5
View File
@@ -111,12 +111,22 @@ Single-context, inside the existing knowledge base: `omp-docs/glossary.md` and
## Secret gate (HARD rule)
The pre-commit hook (`.githooks/pre-commit`, enabled via
`git config core.hooksPath .githooks`) scans every staged diff for live-secret
shapes (`sk-…`, `ghp_…`, `github_pat_…`, `glpat-…`, `xox…`, `AKIA…`, PEM headers,
JWTs) and blocks the commit. Quarantine list — never tracked, ever: `auth.json`,
`.env`, `trust.json`, `*.pem`, `*.key`, `identity*`. Deliberate override:
`git config core.hooksPath .githooks`) blocks the commit in two ways. Token
**shapes** (`sk-…`, `ghp_…`, `github_pat_…`, `glpat-…`, `xox…`, `AKIA…`, PEM
headers, JWTs, bcrypt hashes, `Authorization: Basic` headers) are scanned across
the **entire** staged diff — every file type, because the first version of the
gate whitelisted extensions and never read a `.env`, `.container`, `.ini`, or
extension-less dotfile at all. Credential **assignments** (`PASSWORD=…`,
`secret_key: …`) are scanned in non-prose files only; markdown keeps shape
detection, because the auto-learned memory tier is prose *about* credentials and a
gate that fires there gets overridden until it means nothing. The gate **fails
closed**: if it cannot read the staged diff it refuses the commit rather than
passing it. Quarantine list — never tracked, ever: `auth.json`, `.env` and
`*.env` (`postgres.env`, `frank-job-hunt.env` are the ones that carry real
values), `trust.json`, `*.pem`, `*.key`, `identity*`. Deliberate override:
`ALLOW_SECRETS=1 git commit …`. Remember why: a secret in a tracked file is a
secret in every agent's context window on every machine that pulls this repo.
secret in every agent's context window on every machine that pulls this repo, and
redacting it later does not un-leak the history.
## Machine facts (verify before trusting)
@@ -0,0 +1,67 @@
---
name: command-center-bundle-lint-gate
description: "Before committing docs in dank/command-center, lint the real omp-docs bundle directly — the bats suite only lints fixtures, so a green suite is not proof."
---
# The bundle lint gate (`dank/command-center`)
Adding or editing pages under `omp-docs/`? `bats tests/` going green proves
nothing about the pages you just wrote.
## Why this exists
`tests/kb-lint.bats` exercises `scripts/kb-lint.sh` against `mktemp -d` fixtures
(`"conformant bundle passes"`, `"missing frontmatter is reported"`, …). For years
nothing in the suite pointed the linter at the shipped tree, so a page with no
frontmatter, an index that stopped listing a file, or a broken relative link
committed under a green suite. (Two tests appended 2026-10-20 now run both
checkers against `omp-docs/`; verify they are still present before assuming the
suite covers it — `grep -n omp-docs tests/kb-lint.bats`.)
The general law: **a lint suite that tests only the linter does not lint your
content.** Check which one a suite runs before calling green a gate.
## Gate a docs commit
```bash
cd <worktree>
bats tests/ ; grep -E '^1\.\.' /tmp/… # suite (count, failures)
./scripts/kb-lint.sh omp-docs # want: kb-lint: 0 violation(s)
python3 scripts/kb-coverage.py omp-docs # want: index-coverage: 0 issue(s)
```
`kb-lint.sh <kb-dir>` (default `omp-docs`) = OKF v0.1 conformance **plus link
integrity**, so it is the check that absolute bundle links like
`/tools/gitea/client-access.md` resolve. `kb-coverage.py` = every page listed by
its directory `index.md` (repo doctrine: nothing may appear in a directory its
`index.md` does not list).
OKF shape that passes: every concept page carries frontmatter with a `type`
(`type: reference` + `title`/`description`/`tags`/`timestamp`/`verified`);
`index.md` files carry **no** frontmatter; new pages get an entry in their
directory index in the same commit, plus a dated entry in `omp-docs/log.md`
(one entry = what changed + the durable lesson, a few lines, never a narrative).
## Commit-message trap in this harness
A multi-line message written as a heredoc inside a chained `bash` command
(`git commit -q -F - <<'MSG' … && …`) fails with
`pi-natives:command: syntax error at end of input`. Write the message to
`/tmp/msg-<topic>.txt` with the write tool and `git commit -q -F /tmp/msg-<topic>.txt`.
Keep the `&&` chain so a failed gate stops the commit — hooks (secret gate) stay armed,
never `--no-verify`.
## Land and reap
Merge is form-encoded `do=merge&merge_style=no-ff` and answers `200` with an
**empty body**; never read that as ambiguous — verify by landing. Then
`git -C ~/.omp merge --ff-only origin/main` (the checkout *is* the deployment;
dirty `agent/memories/`, `agent/config.yml`, `APPEND_SYSTEM.md` are machine-tier
live state and do not block an ff whose diff misses them), fast-forward the
session's `wt/*` scratch branch the same way, then `git worktree remove <build-wt>`
and `git branch -d <type>/<issue>-<slug>` (`-d` succeeding is itself proof of
merge).
Stray files whose names start with `-` (e.g. `-c`, `-es` — vim takes a
`--startuptime` flag name as the output filename) show as untracked dirt:
`mv ./-c .AGENTTRASH/<date>-<meaning>.log`, never `rm`.
@@ -1,21 +1,27 @@
---
name: gitea-api-pr-operations
description: "\"Open, list, merge, and diagnose issues/PRs on the Dankclaw Gitea API (192.168.0.143:3000, repo dank/command-center) with the token stored in ~/.config/secrets/gitea — incl. the required merge payload, where review comments actually live on 1.27.3, and how to recover the session id for the mandatory body footer.\""
description: "\"Open, list, merge, and diagnose issues/PRs and manage labels on the Dankclaw Gitea API (192.168.0.143:3000) with the token stored in ~/.config/secrets/gitea — incl. the token-only write auth (Basic reads but 422s on POST/PATCH), the required merge payload, where review comments actually live on 1.27.3, and how to recover the session id for the mandatory body footer.\""
---
# Gitea API PR operations (Dankclaw `dank/command-center`)
# Gitea API operations (Dankclaw `dank/command-center`, `dank/serverAppConfigs`)
## Where
- Token: `~/.config/secrets/gitea` (`http://user:TOKEN@192.168.0.143` single line).
- Token: `~/.config/secrets/gitea` (`http://user:TOKEN@192.168.0.143` single line — git-credential URL form, NOT `username=`/`password=` lines).
Extract WITHOUT echoing:
`tok=$(sed -nE 's#^http://[^:/@]+:([^@]+)@192\.168\.0\.143.*#\1#p' ~/.config/secrets/gitea | head -1)`
- Build a 600-perm temp curl config and use `curl -K`:
`cfg=$(mktemp) && chmod 600 "$cfg" && printf 'header = "Authorization: token %s"\n' "$tok" > "$cfg"`
Delete `$cfg` after use. NEVER pass `-H "Authorization: token $tok"` (the literal
header lands in transcript + logs). NEVER echo the token.
- Base URL: `http://192.168.0.143:3000/api/v1/repos/dank/command-center`.
- Base URL: `http://192.168.0.143:3000/api/v1/repos/dank/<repo>`.
## Recipes (all verified live 2026-10-18)
## Auth: reads tolerate Basic, writes DO NOT (measured 2026-10-20)
GETs succeed with an HTTP Basic header built from the same credential, but every
POST/PATCH/DELETE with Basic returns `407/422 {"message":"token is required"}`.
Always use the `Authorization: token <tok>` header for everything — never debug
"permissions" when the header shape is the fault.
## Recipes (all verified live)
- **Open PR** (token only needed for private repos; this one reads publicly but
POST needs auth): `POST /pulls` JSON `{title, head, base:"main", body}`.
- **PATCH PR title/body**: `PATCH /pulls/<n>` with JSON body `{title}` and/or
@@ -36,6 +42,12 @@ description: "\"Open, list, merge, and diagnose issues/PRs on the Dankclaw Gitea
merge --ff-only origin/main` propagates the live deployment.
- **Issues**: same shape at `/issues` (`POST {title, body}`), `/issues/<n>/comments`
(`POST {body}`, `PATCH .../comments/<id>`) — both work with the temp-cfg auth.
- **Labels**: `GET /labels` (list incl. numeric `id`); `POST /labels`
`{"name":"<name>","color":"#rrggbb"}` to create; attach by ID with
`POST /issues/<n>/labels {"labels":[<id>,...]}` (names are rejected);
detach with `DELETE /issues/<n>/labels/<id>`. serverAppConfigs gained
needs-triage / needs-info / ready-for-agent / ready-for-human / wontfix
(2026-10-20) alongside the older `human-only`.
## Review comments: the API lies on 1.27.3 (learned 2026-10-18)
- `GET /pulls/<n>/comments` (inline review comments) → **404 `not found` on every
@@ -0,0 +1,80 @@
---
name: repo-agent-handoff-doc
description: "Use when asked to write or refresh a standing handoff/status doc so agents landing in a repo can continue work isolated from other directories; sections that code cannot reveal, plus moving untracked docs onto a branch without dirtying a shared checkout."
---
# Standing agent handoff doc for a repo
"Put a complete handoff on project X in its directory so agents there can work
isolated" is not a changelog request. The value is the knowledge the code cannot
reveal. Discover the repo's own conventions first, then write one **living** file.
## 0. Fit the repo, do not invent a layout
`ls -a` + `grep -n '^##' README.md` + `find docs -maxdepth 2` before writing:
- existing `docs/` conventions (dated `docs/decisions/*.md` records vs flat pages),
- whether an `AGENTS.md`/`CLAUDE.md` auto-loads (if not, the README is the only
entry point → the handoff MUST be linked from it, same commit),
- the exact numbers you will quote: per-file `grep -c '^@test'`, `package.json`
`scripts`, module list from `ls src/`. Never write a test count or command from
memory.
## 1. Section checklist (point of use — each answers a question the next agent hits)
1. **State line**: the commit it describes (`main` at `<sha>`, "after PRs #N") +
the rule that whoever leaves work updates it in the same commit. A stale
handoff is worse than none: it gets believed.
2. **Commands** — build/typecheck/test with real runtime, plus concurrency
hazards (e.g. a suite that rebuilds `dist/` in `setup()` → two `bats` runs
corrupt each other's byte-identical pin).
3. **Test seam and how to add a test** — where the harness mock lives, scenario
names, how scripted UI answers are consumed, env dials that are *ignored*
(a `setup()` that clobbers your override), and the red-first rule with the
one exception (a characterization test that says so in a comment).
4. **The laws, each with the test that holds it** — table form. Distinguish
load-bearing invariants from incidental behaviour; this is the section that
stops a confident agent "fixing" a chosen design.
5. **Worktree/dev-dir recipe** — symlinked deps, gitignore rule that keeps the
symlink untracked, why the shared checkout stays clean.
6. **Ship path** — branch → tests → rebuild committed artifact → PR → merge on the
owner's OK → land with `--ff-only` → deploy (the gotcha that the upgrade target
is the PACKAGE name, not the directory alias) → `cmp` the live artifact against
the merged build → prune. Include the auth traps of the forge (token via a
600 curl `-K` file, empty token answering 404 not 401, form-encoded merge).
7. **Live verification** — the instrument that produces evidence (stub player,
log line counts, pty submit semantics), and explicitly what is NOT a check
(print-mode invocation, screen reads of transient toasts, single-take audio
comparisons).
8. **Environment reality not visible from this code** — the live endpoints,
measured limits, refusals; crosslink the README if it owns the table.
9. **Open items** — numbered, each actionable alone, each saying who decides if a
human must; name the repo a fix belongs in when it is NOT this one.
10. **History** — one line per issue/PR. No tables of metrics.
One owner per fact: link the README/decision records instead of restating knob
tables or rationale.
## 2. Tree hygiene (the costly step)
Writing straight into a shared checkout leaves an untracked file on `main` — a
half-deployed state where the checkout is the deployment, and a dirty tree for
everyone else. Move it onto a branch without switching anyone's HEAD:
```bash
mv <repo>/docs/handoff.md /tmp/handoff.md
git -C <repo> fetch -q origin
git -C <repo> worktree add -q -b docs/<slug> ~/Projects/wt-<topic> origin/main
ln -s <repo>/node_modules ~/Projects/wt-<topic>/node_modules # if deps are needed
mv /tmp/handoff.md ~/Projects/wt-<topic>/docs/handoff.md
git -C <repo> status --porcelain # shared checkout clean again
```
README pointer edit happens **in the worktree** (an edit there is a separate
checkout; editing the shared one dirties a tracked file). Then commit, run the
suite, push, open the PR, and merge only on the owner's explicit OK.
## 3. Voice rules
Prose in the repo's own style (match an existing decision record). State reasons
and consequences, not bookkeeping. No measured numbers for things that drift;
keep the law, drop the figure.
@@ -7,9 +7,9 @@
- Hermes (Dankclaw) model routing as of 2026-09-05: main default = frank-large (Dankclaw 192.168.0.143:7770/v1), small/aux routes = frank-small (Dankserver 192.168.0.235:7770/v1), vision + title_generation route to the exact FLM registry tag gemma4-it:e4b (192.168.0.143:7771/v1). The pre-rename aliases frank-default-large/-small/-sightsound are dead since the 2026-09-03 rename cutover — never route on them. Live config lives at /home/dank/Containers/hermes/data/config.yaml (owned by uid 109999; needs sudo to read/edit); the repo's dankclaw/hermes/config.yaml.example is a sanitized backup and can lag live, so diff live first before editing either. Signal adapter is currently disabled in live config (platforms.signal.enabled: false).
- On the guarded franksdank.cloud vhosts (Caddy → Anubis :8923 → Authentik outpost), every unauthenticated request — roots and deep paths alike — is answered with HTTP **200** and an HTML challenge page (titleMaking sure you're not a bot!/title), not a 4xx. Verifying guard health by status code alone is meaningless; assert on leakage markers instead (copyparty UI title, homepage id="grid", services.json "category" JSON shape). Non-JS clients (curl/wget/WebDAV) can never solve the PoW challenge, so they are permanently parked at the challenge page — this is the entire security boundary for copyparty, which serves authenticated traffic with anonymous read perms (-v /w::r:ra,dank). Pinned by tests/e2e/specs/15-unauth-fetch.spec.ts (guest project, empty storage state): any guard-route regression that lets app bytes answer fails loudly; any gate implementation passes.
- On Dankclaw's Pi-hole (FTL v6.7, quadlet at /home/dank/Containers/pihole), /etc/pihole/hosts/custom.list (host path /home/dank/Containers/pihole/etc-pihole/hosts/custom.list) is a LIVE-ONLY Custom-DNS source NOT tracked in the repo — it held ~22 A records pointing every *.franksdank.cloud vhost at 192.168.0.143, including names with no public Porkbun record. gravity.db's domainlist table has zero custom-DNS rows on this build, and the pihole CLI's sqlite path (pihole-FTL sqlite3) works but the web API auth was failing at discovery time, so retiring a name from LAN DNS requires editing custom.list directly (sudo sed -i '/name/d') followed by podman exec pihole pihole restartdns reload; deleting only the repo-managed addn-hosts record is not sufficient. Verify with dig +short name @192.168.0.143 → empty answer.
- Pi-hole admin panel at pihole.franksdank.cloud has two auth layers: Caddy basicauth (edge, username dankuser + hash) and Pi-hole internal password (CivetWeb, set via pihole setpassword). The Pi-hole internal password was the actual failure point, not the Caddy hash. Pi-hole resolves to 192.168.0.143 via addn-hosts (all 28 names point here), making it a LAN service behind Caddy's public TLS edge. Options to remove public exposure: (1) remove the Caddy vhost entirely — LAN clients hit Pi-hole quadlet directly on :8083; (2) keep Caddy vhost but remove the Porkbun A record so external DNS won't resolve pihole.franksdank.cloud. Current state: Caddy basicauth hash = $2b$14$cJm.hbvDi1dEh7x0XRMnseaIdvIrRyH.AcTL56jGJf3wd54qvEb9K (dankuser, password pihole2026); Pi-hole internal password = pihole.2026 (set via pihole setpassword, survives restart).
- Pi-hole admin panel at pihole.franksdank.cloud has two auth layers: Caddy basicauth (edge, username dankuser + hash) and Pi-hole internal password (CivetWeb, set via pihole setpassword). The Pi-hole internal password was the actual failure point, not the Caddy hash. Pi-hole resolves to 192.168.0.143 via addn-hosts (all 28 names point here), making it a LAN service behind Caddy's public TLS edge. Options to remove public exposure: (1) remove the Caddy vhost entirely — LAN clients hit Pi-hole quadlet directly on :8083; (2) keep Caddy vhost but remove the Porkbun A record so external DNS won't resolve pihole.franksdank.cloud. Current state: Caddy basicauth hash and the Pi-hole internal password are BOTH redacted (2026-10-20, issue #40) — they sat here in plaintext, which means they are in git history on every machine that ever pulled this repo, so both need rotation before this note is trustworthy; the live values are the Dankclaw host `.env` (`CADDY_BASICAUTH_HASH`) and `pihole setpassword`, never this file.
- Pi-hole admin at pihole.franksdank.cloud has two auth layers: (1) Caddy basicauth edge (username dankuser, hash in rendered Caddyfile from CADDY_BASICAUTH_HASH in .env), (2) Pi-hole internal CivetWeb password (stored in cli_pw as bcrypt hash, set via pihole setpassword CLI). The env file WEBPASSWORD is used at container start but the CLI hash is what the running web server validates. When "I can't get into Pi-hole admin" returns 403 even after Caddy basicauth passes, the Pi-hole internal password is the likely culprit. Fix: podman exec pihole pihole setpassword newpw to update the CLI hash, then verify podman exec pihole cat /etc/pihole/cli_pw shows updated value. Verify by curling with basicauth: curl -sk -H 'Authorization: Basic ZGFua3VzZXI6...' https://pihole.franksdank.cloud/admin/ should return 302 (not 401 or 403).
- Pi-hole admin panel at pihole.franksdank.cloud uses Caddy basicauth (not Pi-hole's own CivetWeb login). The hash is shared across services (RomM, Copyparty, Pi-hole) via CADDY_BASICAUTH_HASH in the host .env. The rendered Caddyfile at /home/dank/Containers/caddy/Caddyfile is mounted into the Caddy container — updates are picked up by Caddy's file watcher without restart. Password reset: generate bcrypt hash with python3 + bcrypt (hashpw + gensalt), update the rendered Caddyfile line directly or update .env + deploy. Caddy accepts both $2a$ and $2b$ bcrypt variants. _(context: Pi-hole admin password reset session: discovered Caddy basicauth (not Anubis), could not find matching password in common candidates, reset to pihole2026, updated rendered Caddyfile directly, synced repo .env files, Caddy file watcher handled hot-reload.)_
- Pi-hole admin panel at pihole.franksdank.cloud uses Caddy basicauth (not Pi-hole's own CivetWeb login). The hash is shared across services (RomM, Copyparty, Pi-hole) via CADDY_BASICAUTH_HASH in the host .env. The rendered Caddyfile at /home/dank/Containers/caddy/Caddyfile is mounted into the Caddy container — updates are picked up by Caddy's file watcher without restart. Password reset: generate bcrypt hash with python3 + bcrypt (hashpw + gensalt), update the rendered Caddyfile line directly or update .env + deploy. Caddy accepts both $2a$ and $2b$ bcrypt variants. _(context: Pi-hole admin password reset session: discovered Caddy basicauth (not Anubis), could not find matching password in common candidates, reset to the current password [redacted 2026-10-20, issue #40], updated rendered Caddyfile directly, synced repo .env files, Caddy file watcher handled hot-reload.)_
- On the FrankBook-Pro *arr fleet (current lsio images: Sonarr v4/Radarr v5 API v3, Lidarr v2/Readarr API v1), the download-client validation endpoints are NOT usable via API: POST /api/{v}/downloadclient/test/{id} and .../test?forceTest=true both return 405, and POST .../downloadclient/action/getTest exists but 400s without a bespoke body. To prove download-client health after changing a qB host/port, skip the API test route and prove behaviorally instead: podman exec arr curl -s -o /dev/null -w '%{http_code}' http://127.0.0.1:8080/ (expect 200) plus a real qB login from inside the container (curl --data-urlencode username=admin --data-urlencode password=$QBIT_PW .../api/v2/auth/login, expect 200), then confirm /api/{v}/health shows no DownloadClient-*/wbrrelated warnings. Also: arr APIs redact stored client passwords as ******** (sonarr/radarr/lidarr) — PUT the object back with the sentinel untouched to preserve the stored value; readarr (v1) returns the plaintext. When editing via the edit tool, body rows must be prefixed with +, not : — : prefixes get written literally into the file. _(context: Capture turn after the arr→qB loopback repoint (2026-09-04): app-side download-client test endpoints were unreachable, so client health was proven behaviorally instead.)_
- The *arr fleet (Prowlarr, Sonarr, Radarr, Lidarr, Readarr, qBittorrent) runs ONLY on FrankBook-Pro (192.168.0.112, Mac podman machine) since 2026-09-04 — the dankserver compose copy was removed (config roots in ~/.AGENTTRASH/20260904-dankserver-arr-teardown, 22 MB, awaiting purge approval). Fleet invariant, enforced by tests/frankbook_quadlet.bats: ALL six containers share gluetun's netns (Network=gluetun.container, Wants/After gluetun.service) and publish NO ports; the gluetun unit publishes the union (8080, 6881 tcp+udp, 9696, 8989, 7878, 8686, 8787), so all egress exits ProtonVPN by construction (AllowedIPs=0.0.0.0/0; proven: netns egress 51.179.177.14 vs residential 71.227.227.172). deploy-plan.py couples gluetun → all six joiners (a gluetun recreate kills the whole family). Consequences: (1) the old arr→qB addressing hack 10.88.0.1:8080 (podman-bridge gateway) routes INTO the tunnel from inside the netns — download clients must use 127.0.0.1:8080 (done via arr APIs, verified by real qB login from inside sonarr's container); same rule for any future Prowlarr Application rows (baseUrl 127.0.0.1:arr port). (2) Caddy arr vhosts and homepage probes target 192.168.0.112:port (host-side published ports unaffected by netns changes). (3) Known defects as of 2026-09-04: Prowlarr's Applications table is EMPTY (the four rows from 2026-08-30 were lost — prowlarr→arr sync broken until rebuilt); readarr's qB proxy auth defect needs retest post-move; EZTV/1337x "CloudFlare-blocked" parking is stale (egress now VPN — retest before enabling). (4) E2E lesson: playwright specs 11-admin/12-crawl now derive expected open-link counts from services.json (showLink) instead of hardcoded counts — keep new registry entries consistent. Also: deploy.sh --service X dispatches ONLY X on frankbook (path-coupling applies to changed-path planning); gluetun restart self-heals all netns joiners via Restart=always so a single gluetun deploy converges the family. _(context: 2026-09-04 operator decision session: dankserver arr compose copy (booted that morning) removed after Frank chose FrankBook-Pro as the sole arr deployment; netns consolidation + test seam + loopback repoint executed same session. Commits 4c7465d, df4339a.)_
- Rootless podman on Dankserver (5.7.0, verified 2026-09-04 via /proc/pid/uid_map across 7 containers): the DEFAULT user namespace maps container uid 0 → host uid 1000 (dank) and container uid N → host 100000+N-1 (e.g. calibre-web PUID=1000 writes as 100999). Consequences: (1) PUID=0 + data roots owned 1000:1000 is the complete fleet recipe — NO UserNS/userns_mode key is needed on quadlets or compose services; the plex quadlet works without one. (2) NEVER add userns_mode: host to a podman-compose service: podman-compose 1.5.0 runs every compose service inside a pod and podman hard-rejects the combo (--userns and --pod cannot be set together), leaving the stack down with no recreate path. (3) Diagnostics: podman exec c id always shows uid=0 inside any mapping — meaningless; ground truth is /proc/container-pid/uid_map, host-side ps -o user= -p pid, or ownership of freshly-written files. (4) *arr boot-crash signature: UnauthorizedAccessException: Access to the path '/config/...' at logger init = data root not writable (e.g. root-owned dir created by sudo mkdir); the app exits before binding its port, so the port probe reads "dead" rather than "crashed" — check podman logs tail for "Press enter to exit" before assuming an outage has an infra cause. (5) NEVER pkill -f rootlessport on these hosts: it kills the port-forward helper of EVERY published container simultaneously (2026-09-03 outage); restart the owning systemd units instead. _(context: Dankserver *arr stack restore session, 2026-09-04: stack committed 2026-08-29 never booted (Sentry crash, dead ports read as outage); userns_mode: host fix was a red herring — rejected by podman-compose pods; real fix was chown 1000:1000 of /opt config dirs. Commit e454159 (revert), 8a63624 (docs).)_
@@ -0,0 +1,4 @@
{
"v": 1,
"lastConsolidatedAt": 1792536631688
}
@@ -3,4 +3,8 @@
- Typed intro: "Update, [randomly generated epithet]!"
- Heckle mode option
- Typed outro: "More later, over and out!"
- Generation frequency control
- Generation frequency control
## Documentation Flow
- Put any created docs in the existing knowledgebase instead of creating a new one
@@ -1,10 +1,10 @@
{
"v": 1,
"lastConsolidatedAt": 1792524641573,
"lastConsolidatedAt": 1792538189166,
"lastResult": {
"at": 1792452243238,
"at": 1792537233005,
"sessions": 1,
"deltas": 4,
"deltas": 1,
"model": "frank-zealot"
}
}
@@ -1,2 +1,2 @@
## Handoff Location
- put it at ~/Projects
- put it at ~/Projects
@@ -1,8 +1,8 @@
{
"v": 1,
"lastConsolidatedAt": 1792534456791,
"lastConsolidatedAt": 1792538116812,
"lastResult": {
"at": 1792532921161,
"at": 1792537164207,
"sessions": 1,
"deltas": 1,
"model": "frank-zealot"
@@ -1,4 +1,4 @@
{
"v": 1,
"lastConsolidatedAt": 1792534385325
"lastConsolidatedAt": 1792537985341
}
@@ -0,0 +1,3 @@
## Documentation structure
- Store agent-related docs in `dankvoice-docs`
@@ -0,0 +1,10 @@
{
"v": 1,
"lastConsolidatedAt": 1792538005214,
"lastResult": {
"at": 1792537052571,
"sessions": 1,
"deltas": 2,
"model": "frank-zealot"
}
}
@@ -0,0 +1,4 @@
{
"v": 1,
"lastConsolidatedAt": 1792536921764
}
@@ -1,21 +0,0 @@
// ────────────── Output Configuration ──────────────
output "Samsung Electric Company Odyssey G75F HNTY601056" {
mode "3840x2160@60"
scale 1
transform "normal"
position x=0 y=0
}
output "DP-1" {
transform "90"
position x=0 y=0
}
output "DP-3" {
transform "270"
position x=1200 y=0
}
output "eDP-1" {
// no position — auto-placed to the right of DP-* outputs
}
@@ -275,13 +275,13 @@ transition = [ "fade" ]
interval_seconds = 1200
[wallpaper.default]
path = "/home/blank/.local/share/noctalia/wallpapers/wallhaven_nzz3gy.jpg"
path = "/home/blank/.local/share/noctalia/wallpapers/wallhaven_v93dk3.jpg"
[wallpaper.last]
path = "/home/blank/.local/share/noctalia/wallpapers/wallhaven_nzz3gy.jpg"
path = "/home/blank/.local/share/noctalia/wallpapers/wallhaven_v93dk3.jpg"
[wallpaper.monitors.DP-1]
path = "/home/blank/.local/share/noctalia/wallpapers/wallhaven_nzz3gy.jpg"
path = "/home/blank/.local/share/noctalia/wallpapers/wallhaven_v93dk3.jpg"
[wallpaper.monitors.DP-2]
path = "/home/blank/.local/share/noctalia/wallpapers/wallhaven_oxex69.jpg"
@@ -290,7 +290,7 @@ transition = [ "fade" ]
path = "/home/blank/.local/share/noctalia/wallpapers/wallhaven_v9e2w3.jpg"
[wallpaper.monitors.eDP-1]
path = "/home/blank/.local/share/noctalia/wallpapers/wallhaven_nzz3gy.jpg"
path = "/home/blank/.local/share/noctalia/wallpapers/wallhaven_v93dk3.jpg"
[weather]
unit = "imperial"
@@ -1,29 +0,0 @@
layout {
gaps 16 // Gap between windows
center-focused-column "never" // Don’t auto-center focused column
background-color "transparent" // <- needed for noctalia-shell to set wallpaper
preset-column-widths {
proportion 0.25 // 1/4
proportion 0.3333 // 1/3
proportion 0.5 // 1/2
}
preset-window-heights {
proportion 0.25 // 1/4
proportion 0.5 // 1/2
}
struts {
}
shadow
focus-ring {
width 2
active-color "#e01b24"
}
border {
off
}
}
@@ -1,32 +0,0 @@
layout {
focus-ring {
active-color "#ff8c00"
inactive-color "#181818"
urgent-color "#ff8c00"
}
border {
active-color "#f43841"
inactive-color "#181818"
urgent-color "#f43841"
}
tab-indicator {
active-color "#f43841"
inactive-color "#74050b"
urgent-color "#f43841"
}
insert-hint {
color "#f4384180"
}
}
recent-windows {
highlight {
active-color "#f43841"
urgent-color "#f43841"
}
}
@@ -1,4 +0,0 @@
// ────────────── Startup Applications ──────────────
// https://github.com/YaLTeR/niri/wiki/Configuration:-Miscellaneous#spawn-sh-at-startup
spawn-at-startup "noctalia"
@@ -1,142 +0,0 @@
binds {
// ────────────── Keybindings ──────────────
// https://github.com/YaLTeR/niri/wiki/Configuration:-Key-Bindings
// ─── noctalia-shell keybinds ───
// https://docs.noctalia.dev/getting-started/keybinds/
Mod+Shift+Escape { show-hotkey-overlay; }
// ─── Applications ───
Mod+Return hotkey-overlay-title="Open Terminal: Ghostty" { spawn "ghostty"; }
Mod+CTRL+Return hotkey-overlay-title="Open App Launcher: noctalia launcher" { spawn-sh "noctalia msg panel-toggle launcher"; }
Mod+B hotkey-overlay-title="Open Browser: firefox" { spawn "firefox"; }
Mod+ALT+L hotkey-overlay-title="Lock Screen: noctalia lock" { spawn-sh "noctalia msg session lock"; }
Mod+Shift+Q hotkey-overlay-title="Session Menu: noctalia session" { spawn-sh "noctalia msg panel-toggle session"; }
// Please choose your own file manager.
Mod+E hotkey-overlay-title="File Manager: Nautilus" { spawn "nautilus"; }
// ─── Media Controls ───
XF86AudioRaiseVolume allow-when-locked=true { spawn-sh "noctalia msg volume-up"; }
XF86AudioLowerVolume allow-when-locked=true { spawn-sh "noctalia msg volume-down"; }
XF86AudioMute allow-when-locked=true { spawn-sh "noctalia msg volume-mute"; }
XF86AudioMicMute allow-when-locked=true { spawn-sh "noctalia msg mic-mute"; }
XF86AudioNext allow-when-locked=true { spawn-sh "noctalia msg media next"; }
XF86AudioPrev allow-when-locked=true { spawn-sh "noctalia msg media previous"; }
XF86AudioPlay allow-when-locked=true { spawn-sh "noctalia msg media playPause"; }
XF86AudioPause allow-when-locked=true { spawn-sh "noctalia msg media playPause"; }
// ─── Brightness Controls ───
XF86MonBrightnessUp allow-when-locked=true { spawn-sh "noctalia msg brightness-up"; }
XF86MonBrightnessDown allow-when-locked=true { spawn-sh "noctalia msg brightness-down"; }
// ─── Window Movement and Focus ───
Mod+Q { close-window; }
Mod+Left { focus-column-left; }
Mod+H { focus-column-left; }
Mod+Right { focus-column-right; }
Mod+L { focus-column-right; }
Mod+Up { focus-window-up; }
Mod+K { focus-window-up; }
Mod+Down { focus-window-down; }
Mod+J { focus-window-down; }
// Win+Ctrl consume/expel windows to/from columns
Mod+CTRL+Left { consume-or-expel-window-left; }
Mod+CTRL+H { consume-or-expel-window-left; }
Mod+CTRL+Right { consume-or-expel-window-right; }
Mod+CTRL+L { consume-or-expel-window-right; }
Mod+CTRL+UP { expel-window-from-column; }
Mod+CTRL+K { expel-window-from-column; }
Mod+CTRL+Down { expel-window-from-column; }
Mod+CTRL+J { expel-window-from-column; }
Mod+Home { focus-column-first; }
Mod+End { focus-column-last; }
Mod+CTRL+Home { move-column-to-first; }
Mod+CTRL+End { move-column-to-last; }
Mod+Shift+Left { focus-monitor-left; }
Mod+Shift+Right { focus-monitor-right; }
Mod+Shift+UP { focus-monitor-up; }
Mod+Shift+Down { focus-monitor-down; }
Mod+Shift+CTRL+Left { move-column-to-monitor-left; }
Mod+Shift+CTRL+Right { move-column-to-monitor-right; }
Mod+Shift+CTRL+UP { move-column-to-monitor-up; }
Mod+Shift+CTRL+Down { move-column-to-monitor-down; }
// ─── Workspace Switching ───
Mod+WheelScrollDown cooldown-ms=150 { focus-workspace-down; }
Mod+WheelScrollUp cooldown-ms=150 { focus-workspace-up; }
Mod+CTRL+WheelScrollDown cooldown-ms=150 { move-column-to-workspace-down; }
Mod+CTRL+WheelScrollUp cooldown-ms=150 { move-column-to-workspace-up; }
Mod+WheelScrollRight { focus-column-right; }
Mod+WheelScrollLeft { focus-column-left; }
Mod+CTRL+WheelScrollRight { move-column-right; }
Mod+CTRL+WheelScrollLeft { move-column-left; }
Mod+Shift+WheelScrollDown { focus-column-right; }
Mod+Shift+WheelScrollUp { focus-column-left; }
Mod+CTRL+Shift+WheelScrollDown { move-column-right; }
Mod+CTRL+Shift+WheelScrollUp { move-column-left; }
Mod+1 { focus-workspace 1; }
Mod+2 { focus-workspace 2; }
Mod+3 { focus-workspace 3; }
Mod+4 { focus-workspace 4; }
Mod+5 { focus-workspace 5; }
Mod+6 { focus-workspace 6; }
Mod+7 { focus-workspace 7; }
Mod+8 { focus-workspace 8; }
Mod+9 { focus-workspace 9; }
Mod+CTRL+1 { move-column-to-workspace 1; }
Mod+CTRL+2 { move-column-to-workspace 2; }
Mod+CTRL+3 { move-column-to-workspace 3; }
Mod+CTRL+4 { move-column-to-workspace 4; }
Mod+CTRL+5 { move-column-to-workspace 5; }
Mod+CTRL+6 { move-column-to-workspace 6; }
Mod+CTRL+7 { move-column-to-workspace 7; }
Mod+CTRL+8 { move-column-to-workspace 8; }
Mod+CTRL+9 { move-column-to-workspace 9; }
Mod+TAB { focus-workspace-previous; }
// ─── Layout Controls ───
Mod+CTRL+F { expand-column-to-available-width; }
Mod+C hotkey-overlay-title="Calculator" { spawn "gnome-calculator"; }
Mod+CTRL+C { center-visible-columns; }
Mod+Minus { set-column-width "-10%"; }
Mod+Equal { set-column-width "+10%"; }
Mod+Shift+Minus { set-window-height "-10%"; }
Mod+Shift+Equal { set-window-height "+10%"; }
// ─── Split / Consume / Expel ───
Mod+Shift+H { consume-or-expel-window-left; }
Mod+Shift+L { consume-or-expel-window-right; }
Mod+Shift+J { expel-window-from-column; }
// ─── Modes ───
Mod+T { toggle-window-floating; }
Mod+F { fullscreen-window; }
Mod+W { toggle-column-tabbed-display; }
// ─── Screenshots ───
CTRL+Shift+1 hotkey-overlay-title="Screenshot: flameshot full" { spawn-sh "flameshot full"; }
CTRL+Shift+2 hotkey-overlay-title="Screenshot: flameshot screen" { spawn-sh "flameshot screen"; }
CTRL+Shift+3 hotkey-overlay-title="Screenshot: flameshot region" { spawn-sh "flameshot gui"; }
// ─── Emergency Escape Key ───
// Use this when a fullscreen app blocks your keybinds.
// It disables any active keyboard shortcut inhibitor, restoring control.
Mod+ESCAPE allow-inhibiting=false { toggle-keyboard-shortcuts-inhibit; }
// ─── Exit / Power ───
CTRL+ALT+Delete { quit; } // Also quits Niri
Mod+Shift+P { power-off-monitors; } // Turn off screens (useful for OLED or privacy)
Mod+O repeat=false { toggle-overview; }
}
@@ -1,18 +0,0 @@
[Unit]
Description=swayidle idle lock (locks via noctalia-shell IPC, suspends on lid)
Documentation=https://github.com/swaywm/swayidle
After=graphical-session.target
[Service]
# Locks via the noctalia v5 shell; `noctalia msg` resolves the running instance
# through the D-Bus session bus, so no WAYLAND_DISPLAY pinning is needed in unit context.
# deploy.sh enables it only where the ExecStart binary resolves, so it stays inert until swayidle is installed.
ExecStart=/usr/bin/swayidle -w \
timeout 300 '/usr/bin/noctalia msg session lock' \
before-sleep '/usr/bin/noctalia msg session lock' \
after-resume '/usr/bin/noctalia msg session lock'
Restart=on-failure
RestartSec=5s
[Install]
WantedBy=default.target
+171 -222
View File
@@ -1,241 +1,190 @@
---
type: reference
title: Home Directory Configuration Census
description: Complete inventory of userspace program configurations on the workstation — top-level dotfiles, ~/.config, ~/.local/share state, and special directories (~/.pi, ~/.omp).
tags: [inventory, unverified, census, dotfiles, config]
title: Home Configuration Census (blankpad)
description: Per-file tracked-vs-box-only census of ~/.config on blankpad — the input for the dotfiles pruning/adoption session.
tags: [inventory, census, dotfiles, config]
created: 2026-08-28
census-date: 2026-08-28
source: Workstation CachyOS laptop
census-date: 2026-10-20
verified: 2026-10-20
source: blankpad (CachyOS, /home/blank)
---
## Scope & Methodology
This page is the input for the dotfiles pruning/adoption session: for every
`~/.config` entry it separates what the repo already tracks from what lives only
on this box, and sorts the box-only half into adopt / never-track / prune.
Census covers four areas: (1) top-level dotfiles/dotdirs in `/home/dank` (excluding `.omp` which is documented separately here), (2) every entry under `~/.config`, (3) non-cache state dirs under `~/.local/share`, (4) special directories `~/.pi` and `~/.omp`. Each entry assessed for installed status (`command -v`), size (`du -sh`), and age (mtime). Uninstalled programs flagged as `prune-candidate`.
## Method
All measurements are live — no invented numbers.
Classification is per **FILE**, not per directory: the tracked unit is a symlink
into `~/.omp/dotfiles/`. A directory can look "real" (a plain folder, not itself
a symlink) while every file inside it is a tracked symlink — `niri` and `gtk-3.0`
do exactly that. Conversely a directory can be a whole symlink into `dotfiles`
(`lazyvim`) and be entirely tracked. So a directory-level verdict lies; only the
leaf symlink target is ground truth.
---
- **tracked** = a file whose fully-resolved target (`readlink -f`) is under
`/home/blank/.omp/dotfiles/`.
- **box-only** = a regular file, or a symlink that resolves elsewhere.
## 1. Top-Level Dotfiles / Dotdirs
Rerun this classifier rather than trusting the table below:
> 38 entries in `/home/dank` beginning with `.` (excluding `.omp`).
```bash
DOT=/home/blank/.omp/dotfiles; CFG=/home/blank/.config
for d in "$CFG"/*; do
[ -L "$d" ] && case "$(readlink -f "$d")" in "$DOT"/*) echo "$(basename "$d"): tracked=ALL box-only=0"; continue;; esac
[ -d "$d" ] || { echo "$(basename "$d"): FILE (top-level)"; continue; }
tr=0; bo=0; names=""
while IFS= read -r -d '' f; do
if [ -L "$f" ]; then case "$(readlink -f "$f")" in "$DOT"/*) tr=$((tr+1)); continue;; esac; fi
bo=$((bo+1)); names+=" ${f#"$CFG"/}"
done < <(find "$d" -mindepth 1 \( -type f -o -type l \) -print0 2>/dev/null)
printf '%s: tracked=%d box-only=%d%s\n' "$(basename "$d")" "$tr" "$bo" "$names"
done
```
| Path | What it is | Installed? | Size | Age (mtime) | Prune? |
|---|---|---|---|---|---|
| `.bash_history` | Bash command history | yes (bash) | 12K | 2026-08-27 | — |
| `.bash_logout` | Bash logout script | yes (bash) | 4.0K | 2026-07-03 | — |
| `.bash_profile` | Bash login script | yes (bash) | 4.0K | 2026-07-03 | — |
| `.bashrc` | Bash config | yes (bash) | 4.0K | 2026-07-12 | — |
| `.bun` | Bun runtime install | yes | 2.3G | 2026-07-05 | — |
| `.cache` | XDG cache directory | system | 6.4G | 2026-08-28 | — |
| `.claude` | Claude Code config | **no** | 6.4M | 2026-07-12 | **prune-candidate** |
| `.config` | XDG config directory | system | 574M | 2026-08-25 | — |
| `.env` | Environment variables | system | 4.0K | 2026-08-21 | — |
| `.git-credentials` | Git auth cache | yes (git) | 4.0K | 2026-08-28 | — |
| `.gitconfig` | Git global config | yes (git) | 4.0K | 2026-08-05 | — |
| `.local` | XDG local directory | system | 552G | 2026-07-24 | — |
| `.npm` | Node package manager cache | yes (node) | 1.7G | 2026-07-23 | — |
| `.nv` | nvim (Neovim) config | **no** | 8.0K | 2026-07-03 | **prune-candidate** |
| `.nvidia-settings-rc` | NVIDIA Settings config | yes | 4.0K | 2026-07-04 | — |
| `.nvm` | Node Version Manager | **no** | 256M | 2026-07-06 | **prune-candidate** |
| `.openpeon` | OMP Peon session state | yes (omp) | 4.4M | 2026-07-16 | — |
| `.p10k.zsh` | Powerlevel10k zsh theme | yes (zsh) | 96K | 2026-07-03 | — |
| `.paradoxlauncher` | Paradox Launcher state | **no** | 1.1G | 2026-07-31 | **prune-candidate** |
| `.pi` | Legacy PI agent (git repo) | — | 2.5G | 2026-08-17 | see §4 |
| `.pki` | System PKI certs | system | 120K | 2026-07-03 | — |
| `.pulse-cookie` | PulseAudio session | system | 4.0K | 2026-07-03 | — |
| `.python_history` | Python REPL history | yes (python3) | 4.0K | 2026-08-06 | — |
| `.ssh` | SSH keys & config | yes | 28K | 2026-08-24 | — |
| `.steam` | Steam runtime + config | yes | 44K | 2026-08-26 | — |
| `.steampath` | Steam executable path | yes (steam) | 4.0K | 2026-07-31 | — |
| `.steampid` | Steam PID file | yes (steam) | 4.0K | 2026-07-31 | — |
| `.tmux-resurrect` | Tmux resurrect state | yes (tmux) | 0 | 2026-08-25 | — |
| `.tmux.conf` | Tmux configuration | yes (tmux) | 4.0K | 2026-07-18 | — |
| `.wget-hsts` | Wget HSTS database | yes (wget) | 4.0K | 2026-07-06 | — |
| `.zcompdump-<host>-5.9.1` | Zsh completions v5.9.1 | yes (zsh) | 56K | 2026-07-12 | — |
| `.zcompdump-<host>-5.9.1.zwc` | Zsh compiled completions v5.9.1 | yes (zsh) | 120K | 2026-07-12 | — |
| `.zcompdump-<host>-5.9.2` | Zsh completions v5.9.2 | yes (zsh) | 56K | 2026-08-19 | — |
| `.zcompdump-<host>-5.9.2.lock` | Zsh completions lock | yes (zsh) | 0 | 2026-08-25 | — |
| `.zcompdump-<host>-5.9.2.zwc` | Zsh compiled completions v5.9.2 | yes (zsh) | 120K | 2026-08-19 | — |
| `.zsh_history` | Zsh command history | yes (zsh) | 68K | 2026-08-28 | — |
| `.zshrc` | Zsh configuration | yes (zsh) | 4.0K | 2026-08-25 | — |
## ~/.config — tracked vs box-only
---
Profile/browser dirs (`mozilla` 5471 files, `obsidian` 99, `FreeTube` 56,
`nirimod` 116, `google-chrome-for-testing` 5) are app state, never tracked, and
are omitted as rows. Box-only bulk that is regenerated-by-the-program is named
by category, not file-by-file.
## 2. ~/.config — Program Configuration Directory
| `~/.config/` dir | tracked | box-only | box-only files |
|---|--:|--:|---|
| alacritty | 1 | 1 | `themes/noctalia.toml` (generated) |
| autostart | 0 | 3 | `Flameshot.desktop`, `cachyos-hello.desktop`, `.AGENTTRASH/Handy.desktop` |
| bat | 0 | 2 | `config`, `themes/noctalia.tmTheme` (generated) |
| btop | 0 | 2 | `btop.conf`, `themes/noctalia.theme` (generated) |
| cachyos | 0 | 1 | `cachyos-hello/cachyos-hello.log` (state) |
| cava | 0 | 1 | `themes/noctalia` (generated) — program absent, prune |
| crush | 0 | 37 | `skills/<37>` — skills-CLI install |
| containers | 0 | 2 | `systemd/franksign-db.container`, `systemd/postgres.env` (see §Owned-elsewhere / secret tier) |
| dconf | 0 | 1 | `user` (compiled dconf db, state) |
| devin | 0 | 37 | `skills/<37>` — skills-CLI install |
| fastfetch | 0 | 1 | `themes/noctalia.jsonc` (generated) |
| fish | 2 | 1 | `fish_variables` (fish var store, state) |
| flameshot | 0 | 1 | `flameshot.ini` |
| foot | 0 | 2 | `foot.ini`, `themes/noctalia` (generated) — program absent, prune |
| fzf | 0 | 2 | `themes/noctalia.sh`, `themes/noctalia.fish` (generated) |
| ghostty | 2 | 1 | `themes/noctalia` (generated) |
| glow | 0 | 2 | `glow.yml`, `noctalia.json` (generated) |
| goose | 0 | 37 | `skills/<37>` — skills-CLI install |
| gtk-3.0 | 3 | 0 | — |
| gtk-4.0 | 2 | 1 | `servers` (KDE-generated) |
| helix | 0 | 1 | `themes/noctalia.toml` (generated) — `hx` absent, prune |
| herdr | 0 | 1 | `noctalia-colors.toml` (generated) |
| hypr | 0 | 2 | `hyprland.conf`, `noctalia.conf` (generated) — `hyprland` absent, prune |
| kimchi | 0 | 37 | `harness/skills/<37>` — skills-CLI install |
| kitty | 0 | 2 | `kitty.conf`, `themes/noctalia.conf` (generated) — program absent, prune |
| labwc | 0 | 2 | `rc.xml`, `noctalia.conf` (generated) — program absent, prune |
| lazygit | 0 | 2 | `config.yml`, `themes/noctalia.yml` (generated) |
| lazyvim | ALL | 0 | — (whole dir symlink → `dotfiles/lazyvim`) |
| mango | 0 | 2 | `config.conf`, `noctalia.conf` (generated) — program absent, prune |
| micro | 0 | 154 | adopt: `settings.json`, `bindings.json`; generated: `colorschemes/noctalia.micro`; bundled/state: `syntax/*` (146), `buffers/history` |
| mpv | 0 | 0 | — (empty) |
| nautilus | 0 | 0 | — (empty) |
| niri | 8 | 2 | 8 live KDL includes (5 from `dotfiles/niri`, 3 overridden by the `zz-blankpad` tier, whose own 7 niri files are counted in that tier — the stale `layout.kdl` duplicate and generated `noctalia.kdl` were removed by #43) + see *Never track* for the relic. Box-only: `noctalia.kdl` (generated include, refused by `deploy.sh`) and the retired copy |
| noctalia | 3 | 2672 | Tracked and live-linked: `config.toml`, `palettes/Gruber Red.json`, `state/noctalia/settings.toml`. Everything else is app state plus noctalia's downloaded catalog: `community-palettes/` 66, `community-templates/` 80, `wallhaven/thumbs`, `clipboard/`, `plugins/`, `state.toml`, `usage_counts.json`. The app's wallpaper library is `~/.local/share/noctalia/wallpapers` — real files it owns; the repo's duplicate copy of 23 JPEGs was untracked by #41 and deleted from this checkout by the merge, which is what untracking does in every checkout |
| nvim | 0 | 0 | — (empty) |
| pywalfox | 0 | 1 | `config.json` — program absent, prune |
| qt5ct | 0 | 1 | `colors/noctalia.conf` (generated) — program absent, prune |
| qt6ct | 0 | 1 | `colors/noctalia.conf` (generated) — program absent, prune |
| rofi | 0 | 1 | `noctalia.rasi` (generated) — program absent, prune |
| scroll | 0 | 2 | `config`, `noctalia` (generated) — program absent, prune |
| secrets | 0 | 4 | `gitea`, `hf_token`, `hf_token_write`, `kagi_api_key` (names only — secret tier) |
| shelly | 0 | 2 | `config.json`, `settings.json` |
| sway | 0 | 2 | `config`, `noctalia` (generated) — program absent, prune |
| systemd | 9 | 3 | `user/fib-watchdog.service`, `user/fib-watchdog.timer`, `user/timers.target.wants/fib-watchdog.timer` |
| tmux | 0 | 1 | `themes/noctalia.conf` (generated) |
| turborepo | 0 | 1 | `telemetry.json` — `turbo` absent, prune |
| umbriel | 0 | 2 | `config.toml`, `noctalia.toml` (generated) — program absent, prune |
| unity3d | 0 | 235 | RimWorld state: `Saves` (201), `Config` (21), other (13) — never track |
| wezterm | 0 | 1 | `colors/Noctalia.toml` (generated) — program absent, prune |
| yay | 0 | 0 | — (empty) |
| ytm-player | 0 | 1 | `theme.toml` — program absent, prune |
> 38 entries (30 directories + 8 standalone files) under `~/.config`. Files like `mimeapps.list`, `user-dirs.dirs`, `background`, etc. are XDG desktop integration files, not program configs.
### Top-level `~/.config` files (not directories)
### Programs (directories with config files)
All regular files, none symlinked into `dotfiles`:
`starship.toml` (prune — `starship` absent), `frank-job-hunt.env` (secret tier),
`kdeglobals`, `partitionmanagerrc`, `pavucontrol.ini`, `cachyos-hello.json`,
`user-dirs.dirs`, `user-dirs.locale` (XDG/desktop state).
| Path | What it is | Installed? | Size | Files | Age | Prune? |
|---|---|---|---|---|---|---|
| `.config/alacritty` | Alacritty terminal emulator | yes | 4.0K | 1 | 2026-07-24 | — |
| `.config/btop` | btop system monitor | yes | 12K | 1 | 2026-08-11 | — |
| `.config/cachyos` | CachyOS distro config | system | 8.0K | 3 | 2026-07-03 | — |
| `.config/chromium` | Chromium browser | yes | 158M | 423 | 2026-07-15 | — |
| `.config/chromium-headless` | Chromium headless | **no** | 2.6M | 165 | 2026-07-20 | **prune-candidate** |
| `.config/dconf` | Dconf database | yes | 4.0K | 1 | 2026-07-03 | — |
| `.config/fish` | Fish shell config | yes | 24K | 3 | 2026-07-06 | — |
| `.config/gh` | GitHub CLI config | yes | 8.0K | 2 | 2026-07-12 | — |
| `.config/ghostty` | Ghostty terminal emulator | yes | 24K | 3 | 2026-08-24 | — |
| `.config/glow` | Glow markdown renderer | yes | 4.0K | 1 | 2026-07-03 | — |
| `.config/google-chrome-for-testing` | Google Chrome (testing) | **no** | 4.0K | 1 | 2026-07-03 | **prune-candidate** |
| `.config/gtk-3.0` | GTK 3.0 theme settings | system | 12K | 3 | 2026-07-03 | — |
| `.config/gtk-4.0` | GTK 4.0 theme settings | system | 12K | 3 | 2026-07-03 | — |
| `.config/micro` | Micro text editor | yes | 696K | 154 | 2026-08-17 | — |
| `.config/mozilla` | Firefox preferences | yes | 334M | 2,918 | 2026-08-24 | — |
| `.config/nautilus` | Nautilus file manager | yes | 4.0K | 0 | 2026-07-03 | — |
| `.config/niri` | Niri Wayland WM | yes | 44K | 9 | 2026-07-15 | — |
| `.config/noctalia` | Noctalia game config | yes | 212K | 28 | 2026-07-05 | — |
| `.config/nvim` | Neovim config | yes | 64K | 13 | 2026-07-23 | — |
| `.config/obsidian` | Obsidian note-taking app | yes | 79M | 139 | 2026-08-24 | — |
| `.config/pulse` | PulseAudio config | system | 4.0K | 1 | 2026-07-03 | — |
| `.config/qBittorrent` | qBittorrent torrent client | yes | 24K | 5 | 2026-08-05 | — |
| `.config/qmd` | QMD (bun-based CLI) | yes | 4.0K | 1 | 2026-07-03 | — |
| `.config/rpiv-web-tools` | RPi web tools config | yes | 4.0K | 1 | 2026-07-03 | — |
| `.config/shelly` | Shelly home automation | yes | 8.0K | 2 | 2026-07-03 | — |
| `.config/unity3d` | Unity game engine config | yes | 188K | 3 | 2026-07-05 | — |
| `.config/vlc` | VLC media player | yes | 96K | 2 | 2026-07-03 | — |
## Adoption candidates (program passes `command -v`)
### Desktop / XDG Integration (no owning program)
Pre-vetted box-only files worth moving into `dotfiles`. Authorship is from the
file contents, read directly.
| Path | What it is | Installed? | Size | Age |
|---|---|---|---|---|
| `.config/autostart` | XDG autostart entries | system | 0 | 2026-07-03 |
| `.config/background` | Desktop background setting | system | 0 | 2026-07-03 |
| `.config/cachyos-hello.json` | CachyOS welcome JSON | system | 0 | 2026-07-03 |
| `.config/menus` | XDG menu spec | system | 0 | 2026-07-03 |
| `.config/mimeapps.list` | MIME handler associations | system | 0 | 2026-07-03 |
| `.config/paradox-launcher-v2` | Paradox Launcher v2 config | **no** | 0 | 2026-08-05 | **prune-candidate** |
| `.config/pavucontrol.ini` | PulseAudio Volume Control config | system | 0 | 2026-07-03 |
| `.config/spectaclerc` | Spectacle screenshot tool config | system | 0 | 2026-07-03 |
| `.config/systemd` | User systemd config | system | 16K | 2026-07-06 | — |
| `.config/user-dirs.dirs` | XDG user directory dirs | system | 0 | 2026-07-03 |
| `.config/user-dirs.locale` | XDG locale | system | 0 | 2026-07-03 |
---
## 3. ~/.local/share — Non-Cache State & Config
> 26 entries. Excluded pure caches; included game data dirs, launcher configs, and application state.
| Path | What it is | Installed? | Size | Age | Prune? |
|---|---|---|---|---|---|
| `.local/share/Colossal Order` | Cities: Skylines 2 game data | via steam | 27M | 2026-08-26 | — |
| `.local/share/Larian Studios` | Baldur's Gate 3 game data | via steam | 1.5G | 2026-08-25 | — |
| `.local/share/Paradox Interactive` | Paradox game saves/config | via steam/flatpak | 34M | 2026-08-26 | — |
| `.local/share/Shelly` | Shelly home automation data | yes | 4.0K | 2026-07-03 | — |
| `.local/share/Steam` | Steam library + runtime | yes | 550G | 2026-08-28 | — |
| `.local/share/Trash` | Desktop trash | system | 424M | 2026-08-27 | — |
| `.local/share/Tropico6` | Tropico 6 game data | via steam | 3.1M | 2026-08-26 | — |
| `.local/share/applications` | Desktop entry files | system | 56K | 2026-07-03 | — |
| `.local/share/com.pais.handy` | Handy app (likely flatpak) | unknown | 140K | 2026-08-25 | — |
| `.local/share/desktop-directories` | XDG desktop dirs | system | 0 | 2026-07-03 | — |
| `.local/share/fish` | Fish shell data | yes | 4.0K | 2026-07-03 | — |
| `.local/share/flatpak` | Flatpak runtime/pkgs | yes | 8.0K | 2026-07-03 | — |
| `.local/share/gvfs-metadata` | GVFS metadata | system | 156K | 2026-08-25 | — |
| `.local/share/icons` | XDG icon theme data | system | 1.1M | 2026-07-03 | — |
| `.local/share/inxi` | Inxi system info data | yes | 0 | 2026-07-03 | — |
| `.local/share/keyrings` | System keyring data | system | 8.0K | 2026-07-03 | — |
| `.local/share/nautilus` | Nautilus state | yes | 164K | 2026-07-03 | — |
| `.local/share/noctalia` | Noctalia game state | yes | 48K | 2026-08-25 | — |
| `.local/share/nvim` | Neovim local data | yes | 186M | 2026-08-25 | — |
| `.local/share/qBittorrent` | qBittorrent download state | yes | 8.5M | 2026-08-05 | — |
| `.local/share/sddm` | SDDM display manager state | system | 4.0K | 2026-07-03 | — |
| `.local/share/shelly-icons` | Shelly icon assets | yes | 67M | 2026-07-03 | — |
| `.local/share/uv` | UV (Python) package data | yes | 59M | 2026-07-03 | — |
| `.local/share/vlc` | VLC cache/state | yes | 4.0K | 2026-07-03 | — |
| `.local/share/vulkan` | Vulkan shader cache | system | 16K | 2026-07-03 | — |
---
## 4. Special Directories
### ~/.pi — Legacy PI Agent (git repo)
> 31 entries. **2.5G total.** Remote: `dankclaw:3000/dank/agent.git`. Last commit: `eda2edb docs: update frank-default-large model to Qwen3.8-Flash-Next Uncensored ROCmFP4 STRIX_LEAN`.
**Top-level contents:**
| Path | What it is | Size | Prune? |
|---|---|---|---|
| `agent/` | PI agent core — git-tracked code, sessions, node_modules | 2.2G | — |
| `browser-profile/` | Chromium browser profile (artifacts) | 200M | **prune-candidate** |
| `trash/` | Agent session trash | 1.8M | **prune-candidate** |
| `docs/` | Documentation | 684K | — |
| `npm/` | NPM global packages | 368K | — |
| `knowledge/` | Knowledge base | 120K | — |
| `skills/` | Agent skills | 112K | — |
| `scripts/` | Agent scripts | 92K | — |
| `extensions/` | Agent extensions | 60K | — |
| `system-agent-prompts-guide.md` | Agent prompt guide | 56K | — |
| `research/` | Research artifacts | 44K | **prune-candidate** |
| `okf-*.md` | OKF project documents (6 files) | ~96K total | — |
| `web-search.json` | Web search config | 4.0K | — |
| `trust.json` | Trust registry | 4.0K | — |
| `skills-lock.json` | Skills lock file | 4.0K | — |
| `settings.json` / `settings.json.save` | Agent settings | 8.0K total | — |
| `peon` | Peon integration | 4.0K | — |
| `oldAGENTS.md` | Legacy AGENTS.md | 4.0K | — |
| `keybindings.json` | Key bindings | 4.0K | — |
| `PROMPTS` | Prompt templates | 4.0K | — |
| `AGENTS.md` | Agent config | 4.0K | — |
| `web-search-cache` | Search cache (empty) | 0 | **prune-candidate** |
**Size offenders inside `agent/` (≥10M):**
| File | Size |
|---|---|
| `agent/npm/node_modules/onnxruntime-node/bin/.../libonnxruntime_providers_cuda.so` | 302M |
| `agent/npm/node_modules/@lancedb/lancedb-linux-x64-gnu/lancedb.linux-x64-gnu.node` | 137M |
| `.git/objects/pack/pack-ff7cc5418a978a5839d587472bec385f4c9ce72d.pack` | 81M |
| `agent/npm/node_modules/onnxruntime-node/bin/napi-v6/...` (multi-platform binaries) | 56M |
| `agent/npm/node_modules/webgpu/dist/...` (multi-platform .node) | 55M |
| `agent/npm/node_modules/pdf-oxide-wasm/...` (Wasm bundles) | 51M |
| `agent/npm/node_modules/onnxruntime-web/dist/` (Wasm runtime) | 43M |
| `agent/sessions/--home-dank-Projects-*/...` (session JSONL files) | 80M+ |
| `agent/npm/node_modules/playwright-core/lib/` (browser bundle) | 6.1M |
**Prune candidates flagged by task scope:**
- `research/` (44K) — research artifacts, likely stale
- `browser-profile/` (200M) — browser profile, not needed for agent
- `trash/` (1.8M) — agent session trash, purgeable
### ~/.omp — Command Center
> **411M.** Git repo, 7,726,074 commits. Remote: `192.168.0.143:3000/dank/command-center.git`. Last commit: `7726074 chore: birth — state-out ignore policy + secret-gate pre-commit hook`. Active configuration center for the OMP harness — **do not prune.**
---
## Summary
| Metric | Value |
|---|---|
| **Total config items** | 133 (38 top-level + 38 ~/.config + 26 ~/.local/share + 27 ~/.pi top-level entries + 1 ~/.omp) |
| **Orphaned configs** (owning program uninstalled) | **7** (.claude, .nv, .nvm, .paradoxlauncher at top-level; chromium-headless, google-chrome-for-testing, paradox-launcher-v2 in ~/.config) |
| **Total storage for configs** | ~567M (dotfiles) + 574M (~/.config) + 765M (~/.local/share excl. Steam) + 2.5G (~/.pi) = **~4.4G** |
| **Steam library** | 550G (not counted in config total — it's game data) |
### Top 10 Largest Offenders by du -sh
| Rank | Path | Size | Notes |
|---|---|---|---|
| 1 | `~/.local/share/Steam/` | 550G | Game library (not config) |
| 2 | `~/.cache/` | 6.4G | System cache |
| 3 | `~/.pi/` | 2.5G | Legacy agent (git repo) |
| 4 | `~/.bun/` | 2.3G | Bun runtime |
| 5 | `~/.npm/` | 1.7G | Node modules cache |
| 6 | `~/.local/share/Larian Studios/` | 1.5G | Baldur's Gate 3 (via Steam) |
| 7 | `~/.omp/` | 411M | Command center (keep) |
| 8 | `~/.local/share/Trash/` | 424M | Desktop trash |
| 9 | `~/.config/mozilla/` | 334M | Firefox profile |
| 10 | `~/.local/share/nvim/` | 186M | Neovim data/cache |
### Prune Summary
| Category | Count | Entries |
| File | Program (path) | Authored or default |
|---|---|---|
| Full program uninstalled | 7 | .claude, .nv, .nvm, .paradoxlauncher, chromium-headless, google-chrome-for-testing, paradox-launcher-v2 |
| Stale session/trash data | 3 | .pi/trash, .pi/browser-profile, .pi/research |
| **Total recommended for review** | **10** | — |
| `~/.gitconfig` | git (`/usr/bin/git`) | Authored — single setting line: `credential.helper = store --file ~/.config/secrets/gitea` (points at the secret tier; adopt the line, never the file it names) |
| `~/.config/bat/config` | bat (`/usr/bin/bat`) | Authored — one line `--theme=noctalia` |
| `~/.config/lazygit/config.yml` | lazygit (`/usr/bin/lazygit`) | Authored — 28-line custom noctalia color theme |
| `~/.config/micro/settings.json` | micro (`/usr/bin/micro`) | Authored — `{"colorscheme":"noctalia"}` |
| `~/.config/micro/bindings.json` | micro (`/usr/bin/micro`) | Default — empty `{}`, no custom bindings |
| `~/.config/flameshot/flameshot.ini` | flameshot (`/usr/bin/flameshot`) | Authored — `contrastOpacity=188`, `startupLaunch=true` |
| `~/.config/glow/glow.yml` | glow (`/usr/bin/glow`) | Default — shipped template, all default values (noctalia look comes from the generated `noctalia.json`, not this file) |
| `~/.config/autostart/Flameshot.desktop` | flameshot (`/usr/bin/flameshot`) | Authored — `Exec=flameshot`, `X-GNOME-Autostart-enabled=true` |
Every `~/.config` box-only row above lists each file, so the session can extend
beyond these seven (e.g. `btop/btop.conf`, `shelly/*` are also installed-program
configs).
## Never track (box-only)
| Reason | Contents |
|---|---|
| **Generated palette expansion** | Every `themes/noctalia*`, `*/noctalia.*`, `*/colors/noctalia*` / `Noctalia*` (24 files outside app-profile dirs). Machine-written by matugen/noctalia, not hand-authored: the `stat -c '%y %n'` cluster `2026-10-14 19:05:40.330…40.813 -0700` writes 24 files in one second (`alacritty/themes/noctalia.toml` at `.330212397`, `btop/themes/noctalia.theme` at `.334212456`, `tmux/themes/noctalia.conf` at `.813219586`, …); later re-runs re-stamp singles — `bat/themes/noctalia.tmTheme` `2026-10-18 17:11:22`, `niri/noctalia.kdl` `2026-10-18 17:36:27`, `mango/noctalia.conf` `2026-10-17 18:23:37`. A same-second mass-write is generation, never an edit. |
| **App state / profiles** | `mozilla` (5471), `unity3d` RimWorld state (235), `nirimod` (116), `obsidian` (99), `FreeTube` (56), `google-chrome-for-testing` (5); plus per-program state inside tracked dirs: `dconf/user`, `fish/fish_variables`, `micro/syntax/*`+`micro/buffers/history`, `gtk-4.0/servers`, `cachyos/cachyos-hello.log`. |
| **Skills-CLI installs** | `crush` (37), `devin` (37), `goose` (37), `kimchi/harness` (37) skill trees — provisioned by the skills CLI, owned by the store, not this repo. See [skills/store-wiring](/skills/store-wiring.md). |
| **Secret tier** | `~/.config/secrets/` (`gitea`, `hf_token`, `hf_token_write`, `kagi_api_key` — names only), top-level `frank-job-hunt.env`, `containers/systemd/postgres.env`. Never tracked, values never printed. |
| **Trash committed inside the tree** | `dotfiles/niri/.config/niri/cfg/.AGENTTRASH/display.kdl.superseded` — a retired output config committed 2026-10-09, five days before `.gitignore:54` gained `.AGENTTRASH/` (2026-10-14). Tracked-then-ignored: an ignore rule never reaches an already-tracked path, so the only repair is `git rm --cached`, done by #41. The rule does match at depth — `git check-ignore --no-index -v dotfiles/niri/.config/niri/cfg/.AGENTTRASH/x` resolves to `.gitignore:54` — so a fresh in-tree trash dir is ignored from birth and this does not recur on its own. |
## Owned-elsewhere (do not adopt here)
- `~/.config/containers/systemd/franksign-db.container` — header:
*"Installed by `infra/setup-dev.sh` into `~/.config/containers/systemd/`."*
Owned by the franksign repo; its quadlet, not this box's config.
- `~/.config/systemd/user/fib-watchdog.service` + `.timer` — box-only.
`ExecStart=/usr/bin/bun src/cli.ts worker` with
`WorkingDirectory=/home/blank/Projects/frank-intelligence-bench`.
`systemctl --user is-active fib-watchdog.timer` → **inactive**. This box holds
the only copy. Do **not** adopt it into `dotfiles`: `ExecStart` and
`WorkingDirectory` are the bench project's internals, so that repo owns the
unit — land it there and this row becomes "deployed by the bench repo". Until
then it is a reinstall risk recorded here, not a dotfiles gap.
- **Hygiene finding, fixed 2026-10-20:** the enable-symlink
`~/.config/systemd/user/default.target.wants/blankpad-desk-autoswitch.service`
pointed into `~/.omp/wt/de-redesign-20261009/…`, a worktree the heartbeat had
already reaped — so desk auto-switching was silently off while
`systemctl --user is-enabled` still answered `enabled`. Relinked to
`dotfiles/zz-blankpad/.config/systemd/user/blankpad-desk-autoswitch.service`
and `daemon-reload`ed; left `inactive` on purpose, because starting it mid-
session applies a desk profile and the box is in use. Law and diagnosis:
[dotfiles-deploy](/tools/dotfiles-deploy.md).
## Prune list (owning program absent from `command -v`)
All verified with `command -v <prog>` (git resolves to `/usr/bin/git`; a session
alias can mask it, so probe in a clean shell).
| Config | Program checked |
|---|---|
| `sway`, `hypr`, `labwc`, `mango`, `scroll`, `umbriel` | `sway` `hyprland` `labwc` `mango` `scroll` `umbriel` — all ABSENT |
| `wezterm`, `kitty`, `foot` | `wezterm` `kitty` `foot` — all ABSENT |
| `rofi`, `helix`, `cava`, `ytm-player`, `starship.toml` | `rofi` `hx` `cava` `ytm-player` `starship` — all ABSENT |
| `pywalfox`, `qt5ct`, `qt6ct`, `turborepo` | `pywalfox` `qt5ct` `qt6ct` `turbo` — all ABSENT |
The first three rows are the expected set (all 14 confirmed absent). **My check
disagrees with the expected set by extending it**: `pywalfox`, `qt5ct`, `qt6ct`,
`turborepo` are also installed-nowhere config dirs and belong in the prune list.
Note the orphan dirs `cava`, `rofi`, `helix`, `wezterm`, `qt5ct`, `qt6ct` contain
*only* a generated noctalia palette file — pruning them removes no authored
config. `crush`/`devin`/`goose`/`kimchi` also fail `command -v` but are the
skills-CLI tier above, not this session's prune targets.
## Links
- [skills/store-wiring](/skills/store-wiring.md) — owns the skills-CLI tier
claim (`crush`/`devin`/`goose`/`kimchi`).
- [KB index](/index.md) — knowledge-base entry point.
+26
View File
@@ -16,6 +16,32 @@ lesson, a few lines. Full incident narratives are recoverable from git
(`git log --follow omp-docs/log.md`); knowledge that outlives an entry lives
on its concept page, linked here. Never re-expand an entry in place.
## 2026-10-20 — the secret gate was reading the wrong files, and one credential had already escaped
Found while deciding what else belongs in `dotfiles/`. (1) The gate whitelisted
extensions (`*.md *.yml *.json *.toml *.sh *.ts *.js`), so a `.env`, `.container`,
`.ini` or extension-less dotfile was never read — exactly the shapes the dotfiles
tier is made of. It now scans the whole staged diff for token shapes and, in
non-prose files, for credential assignments, and it fails closed when `git diff`
itself errors. That last clause earned its place: the rewrite's first attempt
passed `-I` to `git diff`, which is not valid there, got an empty file list, and
reported success on every commit — that is what a blind gate looks like, and it
looks identical to a clean tree. (2) A live credential had already escaped into
tracked content: a Caddy basicauth bcrypt hash, its plaintext password, and the
Pi-hole internal password, in an auto-learned memory file. Redacted — but both
credentials need **rotation**, because redaction does not un-leak history that
every machine has already pulled. (3) `home/dank/.omp/dotfiles/**` and
`.pre-pull-backup/**` were tracked fiction invisible to `deploy.sh`, and 23
wallpaper JPEGs (80 MiB) rode along in every clone. Laws: a gate must fail closed,
since a silently-passing gate is worse than none — it buys the belief of safety;
auto-learned memory is written by a process that has read your terminal, so it is
a leak surface and gets gated like any config; `systemctl --user enable`
snapshots an absolute path, so enabling from a worktree means the seven-day reap
silently kills the unit while `is-enabled` goes on saying `enabled`
(`blankpad-desk-autoswitch.service` was dangling that way). Details:
[dotfiles-deploy](tools/dotfiles-deploy.md),
[home-config-census](inventory/home-config-census.md).
## 2026-10-20 — engineering skills configured against this KB
`setup-matt-pocock-skills` ran, and its output moved out of the skill's default
+127
View File
@@ -0,0 +1,127 @@
---
type: reference
title: Dotfiles deployment tier
description: How dotfiles/ packages become live configuration — stow-equivalent symlinks, host override packages, and the two laws about theme media and systemd enable symlinks.
tags:
- dotfiles
- deployment
- systemd
- noctalia
timestamp: 2026-10-20
verified: 2026-10-20
---
# Dotfiles deployment tier
`dotfiles/` holds home-relative trees and `scripts/deploy.sh` is a stow-equivalent: the
repo checkout **is** the deployment, and deploying replaces live files with symlinks into
the repo, so `git pull` + `deploy.sh` is the whole propagation story. One directory per
tool (`niri`, `ghostty`, `gtk-3.0`, `systemd-user`, …); a package named `zz-$(hostname)`
is stowed last and its files replace symlinks coming from base packages.
| Mode | Effect |
|---|---|
| `deploy.sh --check` | dry run — adopts identicals, reports what would be linked, lists divergents, writes nothing. Run this first, always |
| `deploy.sh` (sync) | adopt identicals, link new, quarantine divergents into `.quarantine/` and replace them |
| `deploy.sh --adopt` | like sync, but divergents are reported and skipped rather than quarantined |
Two package shapes are not symlinks: `dconf/dump.txt` is loaded with `dconf load /`, and
`systemd-user` units get `daemon-reload` plus `enable --now` only when their `ExecStart`
binary resolves on the current machine, so a unit belonging to another box stays inert
instead of crash-looping.
**The tracked unit is the file, not the directory.** A live directory can be a real
directory and still be fully tracked, because every file inside it is a symlink into the
repo (`niri` is 9 tracked / 1 box-only; `gtk-3.0` is 3/0). Any audit that classifies by
directory mislabels the tree — classify per file with `readlink -f`. The live census and
its classifier command are in [home-config-census](/inventory/home-config-census.md).
## Law: media the theme pipeline consumes is content, not config
Wallpapers live at `dotfiles/noctalia/.local/share/noctalia/wallpapers/` and are
gitignored. They were tracked until 2026-10-20 — 23 files, 80 MiB — which meant every
clone of a *configuration* repo carried a media library no diff can review, and the
payload only ever grows because deleting a tracked blob from the tree does not delete it
from history. The topology is not what one would guess: [Noctalia](noctalia-shell.md)
keeps its own library at `~/.local/share/noctalia/wallpapers/` as a **real directory of
real files**, so the tracked tree was a duplicate of it and never its source — measured
2026-10-20, when the merge of #41 deleted the repo copies and the desktop did not
flicker. Two consequences to know before untracking media: the copy that survives belongs
to the application, not to the repo; and untracking deletes the file in every other
checkout, `git rm --cached` on the branch notwithstanding.
Restore on a fresh box: the library lives in `~/.local/share/noctalia/wallpapers/`, a path
`deploy.sh` does not manage. Every filename is `wallhaven_<id>.<ext>` and the ID is the
whole address — `curl -L -o <name> "https://wallhaven.cc/w/<id>"` into that directory puts
the set back. Losing it is an acceptable outcome; 80 MB per clone forever is not.
## Law: never `systemctl --user enable` a unit from a worktree
`enable` writes `~/.config/systemd/user/default.target.wants/<unit>` as a symlink to the
unit path it was handed, and that path is absolute. The heartbeat reaps `wt/*` worktrees
older than seven days ([live-sync](live-sync.md)), so a unit enabled from a worktree turns
into a dangling symlink while `systemctl --user is-enabled` still answers `enabled` — the
unit is dead and the status lies about it. Found on 2026-10-20:
`blankpad-desk-autoswitch.service` pointed into the reaped `wt/de-redesign-20261009`, so
desk auto-switching had been silently off since the reap.
Check the link target before the unit file:
```bash
ls -l ~/.config/systemd/user/default.target.wants/
# every target must resolve; a dangling one is a reaped worktree, not a config bug
```
Fix is to relink into the deployment checkout (`~/.omp/dotfiles/…`), `systemctl --user
daemon-reload`, then confirm with `is-active` — `is-enabled` proves nothing.
## Law: a generated file must never be a required include
`~/.config/niri/config.kdl` ends with `include "noctalia.kdl"` — no `optional=true`.
That file is noctalia's palette output (`#ff8c00`, `#f43841`, one re-stamp
`2026-10-18 17:36:27`), so it is machine state and correctly untracked. But on a fresh
box `deploy.sh` links `config.kdl` first, and niri then refuses to start until noctalia
has run once and written the file — the compositor is bricked by the order of operations,
not by a bad config. Close it at the include, which survives someone skipping the theme
pipeline: mark generated includes `optional=true`, as the `display.kdl` and
`switch-events.kdl` lines in the same file already are.
## Desk choice is live state, never config
`~/.config/niri/cfg/display.kdl` is a relative symlink to whichever profile is active
(`desk-1-home.kdl`, `desk-2-portable.kdl`), written by `desk.sh`. Both profiles are
tracked in `dotfiles/zz-blankpad/`; the pointer is not, because which desk the box sits on
is not a fact about the repo. A fresh box therefore starts with no output configuration —
`config.kdl` includes `display.kdl` with `optional=true`, so niri comes up usable but with
default monitors until `desk.sh` is run once.
## Law: generated theme output is never deployed
noctalia rewrites palette output in place — `noctalia.*`, `themes/noctalia*`,
`colors/noctalia*`, `noctalia-*` (the classes the [census](/inventory/home-config-census.md)
proves by same-millisecond mtime). A tracked copy of one is a stale snapshot of a colour
scheme, and the failure is quiet: `dotfiles/zz-blankpad/niri/.config/niri/noctalia.kdl`
(commit `b3d3df4`, 2026-10-18) sat byte-identical to the live file for two days, harmless,
while `--check` cheerfully proposed `ADOPT … noctalia.kdl`. The moment the palette changed,
the next sync would have quarantined the live file and restored 2026-10-18's colours —
a tool silently reverting a user-visible theme choice.
`deploy.sh` now refuses the class (`SKIP … (generated theme output)`), `.gitignore` carries
the matching patterns so it cannot be re-added, and `tests/deploy.bats` pins both
directions: generated output is never adopted, and an ordinary identical config still is.
Untracked in #43, Refs #42.
## Law: a `zz` override older than the file it overrides is a time bomb
Stow order means `zz-$HOST` wins, and when both packages carry the same path, sync relinks
it *silently* — the script's own comment: "managed file changing owners between packages:
silent relink, nothing to quarantine". `dotfiles/zz-blankpad/niri/.config/niri/cfg/layout.kdl`
held focus ring `width 2` / `active-color "#e01b24"` from `7b7dbf5` (2026-10-17) while the
base package absorbed `b3d3df4` (2026-10-18): `width 4`, `"#ff8c00"`. Live was linked to
base, the newer intent, so `--check` printed `RELINK .config/niri/cfg/layout.kdl` on every
run and one unattended `deploy.sh` would have reverted the amber focus ring with no
quarantine and no warning. The duplicate is deleted in #43.
Rule: a `zz` file exists only to differ. When base absorbs the change, delete the `zz` copy
in the same commit — and never read `RELINK` lines from `--check` as cosmetic noise; they
are the sound of a stale override waiting for someone to run the tool for real.
+1
View File
@@ -9,6 +9,7 @@ Configuration guides and reference material for installed software. Each tool ha
* [applications](applications.md) — Default userspace applications on CachyOS with Niri — browsers, editor, terminal, file manager, audio, screenshots, gaming.
* [cachyos](cachyos.md) — Arch-based rolling release with performance-optimized kernels, repositories, and gaming stack. Running on the workstation.
* [comfyui](comfyui/index.md) — Node-based AI image generation engine on dankclaw. FLUX.2 klein 4B, CPU backend, systemd managed.
* [dotfiles-deploy](dotfiles-deploy.md) — How `dotfiles/` packages become live config: `deploy.sh` modes, host override packages, and why theme media and worktree-enabled units are traps.
* [ghostty](ghostty/index.md) — GPU-accelerated terminal emulator with extensive configuration, keybindings, and shaders.
* [gitea](gitea/index.md) — Private Git hosting on dankclaw. Quadlet-managed containers: Gitea, PostgreSQL, Caddy.
* [handy](handy.md) — Offline speech-to-text transcription with WebKitGTK backend and Tauri v2 UI.
+8
View File
@@ -44,6 +44,14 @@ actions=0; quarantined=0; skipped=0
deploy_file() { # $1 = repo file path, $2 = home-relative target path
local src="$1" tgt="$HOME/$2"
# Generated theme output is never deployment state: noctalia rewrites these files in
# place, so a tracked copy is a stale snapshot and syncing one reverts a live palette to
# the colours of the day it was committed. Classes are those the census measures by
# mtime — see omp-docs/inventory/home-config-census.md, "Generated palette expansion".
case "$2" in
*/themes/noctalia*|*/colors/noctalia*|*/noctalia.*|*/noctalia-*|*/Noctalia*)
echo "SKIP $2 (generated theme output)"; skipped=$((skipped+1)); return 0 ;;
esac
if [ -L "$tgt" ]; then
if [ "$(realpath "$tgt")" = "$(realpath "$src")" ]; then return 0; fi # already deployed
case "$MODE" in
+76
View File
@@ -0,0 +1,76 @@
#!/usr/bin/env bats
# tests/deploy.bats — black-box tests for scripts/deploy.sh (Refs #42)
#
# Contract (see script header): the repo checkout IS the deployment; sync adopts
# identicals, links new files, quarantines-and-replaces divergents, and stows the
# zz-$(hostname) package last. One thing it must NEVER do is deploy generated theme
# output: noctalia rewrites `noctalia.*` / `themes/noctalia*` in place, so a tracked
# copy is a stale snapshot, and a sync would revert a live palette to whatever colour
# scheme was current on the day it was committed.
#
# Nothing is observed except exit status, stdout, the file/symlink shape under $HOME,
# and the quarantine directory.
setup() {
TEST_TMP="$(mktemp -d)"
REPO="$TEST_TMP/repo"
DEPLOY="$BATS_TEST_DIRNAME/../scripts/deploy.sh"
mkdir -p "$REPO/scripts" "$REPO/dotfiles"
# A git repo, because the deployment checkout IS one and deploy.sh arms hooksPath.
git init -q -b main "$REPO"
cp "$DEPLOY" "$REPO/scripts/deploy.sh"
chmod +x "$REPO/scripts/deploy.sh"
export HOME="$TEST_TMP/home"
mkdir -p "$HOME"
export DEPLOY_REPO="$REPO" DEPLOY_HOST=box
}
teardown() { rm -rf "$TEST_TMP"; }
# repo_file <pkg-relative path> <content>
repo_file() { mkdir -p "$(dirname "$REPO/dotfiles/$1")"; printf '%s\n' "$2" > "$REPO/dotfiles/$1"; }
# live_file <absolute path under $HOME> <content>
live_file() { mkdir -p "$(dirname "$1")"; printf '%s\n' "$2" > "$1"; }
@test "generated theme output is never adopted into a symlink" {
repo_file "ghostty/.config/ghostty/themes/noctalia" 'background = "#f43841"'
live_file "$HOME/.config/ghostty/themes/noctalia" 'background = "#f43841"'
run "$REPO/scripts/deploy.sh"
[ "$status" -eq 0 ]
[[ "$output" == *"SKIP"*".config/ghostty/themes/noctalia"* ]]
[ -f "$HOME/.config/ghostty/themes/noctalia" ]
[ ! -L "$HOME/.config/ghostty/themes/noctalia" ]
}
@test "a diverged live palette is never quarantined and reverted to a stale copy" {
# The dangerous shape: the tracked file matches today's palette, the theme changes
# tomorrow, and sync would quarantine the live file and restore the stale one.
repo_file "niri/.config/niri/noctalia.kdl" 'active-color "#e01b24"'
live_file "$HOME/.config/niri/noctalia.kdl" 'active-color "#ff8c00"'
run "$REPO/scripts/deploy.sh"
[ "$status" -eq 0 ]
grep -q 'active-color "#ff8c00"' "$HOME/.config/niri/noctalia.kdl"
[ ! -L "$HOME/.config/niri/noctalia.kdl" ]
[ ! -d "$REPO/.quarantine" ]
}
@test "an ordinary identical config is still adopted" {
# Control against an exclusion so broad the tool stops deploying anything.
repo_file "ghostty/.config/ghostty/config.ghostty" 'theme = "gruber"'
live_file "$HOME/.config/ghostty/config.ghostty" 'theme = "gruber"'
run "$REPO/scripts/deploy.sh"
[ "$status" -eq 0 ]
[ -L "$HOME/.config/ghostty/config.ghostty" ]
[ "$(realpath "$HOME/.config/ghostty/config.ghostty")" = "$(realpath "$REPO/dotfiles/ghostty/.config/ghostty/config.ghostty")" ]
}
@test "--check reports and writes nothing" {
repo_file "ghostty/.config/ghostty/config.ghostty" 'theme = "gruber"'
live_file "$HOME/.config/ghostty/config.ghostty" 'theme = "tokyo-night"'
run "$REPO/scripts/deploy.sh" --check
[ "$status" -eq 0 ]
[[ "$output" == *"DRIFT"* || "$output" == *"QUAR"* ]]
[ ! -L "$HOME/.config/ghostty/config.ghostty" ]
grep -q 'theme = "tokyo-night"' "$HOME/.config/ghostty/config.ghostty"
[ ! -d "$REPO/.quarantine" ]
}
+159
View File
@@ -0,0 +1,159 @@
#!/usr/bin/env bats
# tests/secret-gate.bats — black-box tests for .githooks/pre-commit
#
# Contract: a commit whose staged content contains a credential is blocked
# (non-zero exit, stderr names the file); a commit whose content is benign
# configuration or prose about credentials is allowed. The gate is copied into a
# scratch repo so git itself invokes it — no sourcing, no internal state.
setup() {
HOOK="$BATS_TEST_DIRNAME/../.githooks/pre-commit"
REPO="$(mktemp -d)/g"
mkdir -p "$REPO/.git/hooks"
git init -q "$REPO"
git -C "$REPO" config user.email gate@test
git -C "$REPO" config user.name gate
cp "$HOOK" "$REPO/.git/hooks/pre-commit"
chmod +x "$REPO/.git/hooks/pre-commit"
}
teardown() { rm -rf "$(dirname "$REPO")"; }
# try_commit <file> <content> -> sets status/output from the real commit attempt
try_commit() {
local file="$1" content="$2"
mkdir -p "$REPO/$(dirname "$file")"
printf '%s\n' "$content" > "$REPO/$file"
run git -C "$REPO" add -A
run git -C "$REPO" commit -q -m "attempt: $file"
}
@test "blocks a low-entropy password in a .env file" {
try_commit "postgres.env" 'POSTGRES_PASSWORD=hunter2corp2026'
[ "$status" -ne 0 ]
[[ "$output" == *"SECRET GATE"* ]]
[[ "$output" == *"postgres.env"* ]]
}
@test "blocks a credential in a file type the extension whitelist used to skip" {
try_commit "app.conf" '[app]
secret_key = "rotatedvalue99"'
[ "$status" -ne 0 ]
[[ "$output" == *"app.conf"* ]]
}
@test "blocks a token shape in a plain text file" {
try_commit "notes.txt" "backup remote: ghp_$(printf 'a%.0s' {1..36})"
[ "$status" -ne 0 ]
[[ "$output" == *"notes.txt"* ]]
}
@test "blocks a bcrypt hash" {
try_commit "dump.txt" 'basicauth: $2b$14$'"$(printf 'a%.0s' {1..53})"
[ "$status" -ne 0 ]
[[ "$output" == *"dump.txt"* ]]
}
@test "blocks a credential in a quadlet unit" {
try_commit "db.container" '[Container]
Environment=POSTGRES_PASSWORD=devpostgres2026'
[ "$status" -ne 0 ]
[[ "$output" == *"db.container"* ]]
}
@test "prose about credentials in markdown is still allowed" {
try_commit "docs/runbook.md" 'The GITEA_TOKEN moved out of .zshrc into secrets.env.
Set password: user.set_password(value) and api_key = "sk-no-key-required".'
[ "$status" -eq 0 ]
}
@test "placeholder and indirected values in config files are allowed" {
try_commit "app.conf" '[db]
PASSWORD = CHANGEME
SECRET_KEY = ${VAULT_SECRET}
api_key = "sk-no-key-required"
token_file = <operator_token>'
[ "$status" -eq 0 ]
}
@test "environment variable indirection is not a secret" {
try_commit "start.sh" '#!/bin/sh
export GITEA_TOKEN="$GITEA_TOKEN"
export API_KEY_FILE=/run/secrets/api_key'
[ "$status" -eq 0 ]
}
@test "binary content never trips the gate" {
head -c 4096 /dev/urandom > "$REPO/wall.jpg"
run git -C "$REPO" add -A
run git -C "$REPO" commit -q -m "binary"
[ "$status" -eq 0 ]
}
@test "ALLOW_SECRETS still overrides, and says so" {
try_commit "postgres.env" 'POSTGRES_PASSWORD=hunter2corp2026'
[ "$status" -ne 0 ]
mkdir -p "$REPO/.git/hooks"
run env ALLOW_SECRETS=1 git -C "$REPO" commit -q -m "override"
[ "$status" -eq 0 ]
[[ "$output" == *"ALLOW_SECRETS set"* ]]
}
@test "this repo's ignore policy quarantines env files at any name" {
repo="$BATS_TEST_DIRNAME/.."
run git -C "$repo" check-ignore -q --no-index dotfiles/db/postgres.env
[ "$status" -eq 0 ]
run git -C "$repo" check-ignore -q --no-index dotfiles/app/frank-job-hunt.env
[ "$status" -eq 0 ]
run git -C "$repo" check-ignore -q --no-index dotfiles/bat/config
[ "$status" -ne 0 ]
}
@test "the gate refuses to pass when it cannot read the staged diff" {
# A gate that swallows a git error reports success on every commit. This is not
# hypothetical: the 2026-10-20 rewrite passed `-I` to git diff, which is not a
# valid flag there, and the empty file list made every secret commit sail through.
nodir="$(mktemp -d)"
run bash -c "cd '$nodir' && '$HOOK'"
rm -rf "$nodir"
[ "$status" -ne 0 ]
[[ "$output" == *"cannot read the staged diff"* ]]
}
@test "deleting a credential is never blocked" {
# The commit that removes a secret must always be possible, or the gate holds a
# leak hostage. It did exactly that on 2026-10-20: matching removed lines made
# the redaction commit look like the leak. Seed with the documented override,
# then remove with an ordinary commit.
printf 'POSTGRES_PASSWORD=hunter2corp2026\n' > "$REPO/db.conf"
run git -C "$REPO" add -A
run env ALLOW_SECRETS=1 git -C "$REPO" commit -q -m "seed a leak"
[ "$status" -eq 0 ]
printf '[db]\nPASSWORD = ${VAULT_SECRET}\n' > "$REPO/db.conf"
run git -C "$REPO" add -A
run git -C "$REPO" commit -q -m "redact the leaked password"
[ "$status" -eq 0 ]
}
@test "the gate can commit itself" {
# Fixtures have to look like credentials, so a gate that scans its own test file
# without exemption can never be committed — it holds its own maintenance
# hostage. The exemption is two exact paths, and it must announce itself.
mkdir -p "$REPO/.githooks" "$REPO/tests"
cp "$HOOK" "$REPO/.githooks/pre-commit"
printf 'POSTGRES_PASSWORD=hunter2corp2026\n' > "$REPO/tests/secret-gate.bats"
run git -C "$REPO" add -A
run git -C "$REPO" commit -q -m "gate and its fixtures"
[ "$status" -eq 0 ]
[[ "$output" == *"self-test fixtures exempt"* ]]
}
@test "a credential hidden in some other test file is still caught" {
# The exemption is a filename list, not the tests/ directory.
mkdir -p "$REPO/tests"
printf 'POSTGRES_PASSWORD=hunter2corp2026\n' > "$REPO/tests/other.bats"
run git -C "$REPO" add -A
run git -C "$REPO" commit -q -m "smuggle"
[ "$status" -ne 0 ]
[[ "$output" == *"tests/other.bats"* ]]
}