Compare commits
24
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
004962ffce | ||
|
|
56fc899763 | ||
|
|
35ceae6d7b | ||
|
|
655a39ffb6 | ||
|
|
b4fc881164 | ||
|
|
6df45d709a | ||
|
|
5ab0a49ea4 | ||
|
|
4666d0dce0 | ||
|
|
d013795922 | ||
|
|
2b7b0bc3cd | ||
|
|
3809e1ed22 | ||
|
|
e278e4b863 | ||
|
|
c05f5ad451 | ||
|
|
ecc089c6ef | ||
|
|
7523ca5c19 | ||
|
|
3022277167 | ||
|
|
689ada428f | ||
|
|
9ebf5fe85f | ||
|
|
86280608e5 | ||
|
|
411b32ff59 | ||
|
|
04f93133fc | ||
|
|
7da5e9dfa8 | ||
|
|
0d43eb0948 | ||
|
|
5110060c24 |
+87
-10
@@ -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
@@ -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.
|
||||
@@ -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
|
||||
}
|
||||
@@ -9,8 +9,6 @@ window-size=(360, -1)
|
||||
|
||||
[org/gnome/desktop/background]
|
||||
picture-options='zoom'
|
||||
picture-uri='file:///home/dank/.config/background'
|
||||
picture-uri-dark='file:///home/dank/.config/background'
|
||||
|
||||
[org/gnome/desktop/interface]
|
||||
color-scheme='prefer-dark'
|
||||
@@ -26,14 +24,6 @@ default-visible-columns=['name', 'size', 'type', 'date_modified']
|
||||
default-folder-viewer='list-view'
|
||||
migrated-gtk-settings=true
|
||||
|
||||
[org/gnome/nautilus/window-state]
|
||||
initial-size=(890, 550)
|
||||
initial-size-file-chooser=(890, 550)
|
||||
maximized=false
|
||||
|
||||
[org/gnome/portal/filechooser/firefox]
|
||||
last-folder-path='/home/dank/Downloads'
|
||||
|
||||
[org/gtk/gtk4/settings/file-chooser]
|
||||
show-hidden=false
|
||||
sort-directories-first=false
|
||||
|
||||
@@ -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
|
||||
}
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -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"
|
||||
|
||||
@@ -2,6 +2,13 @@
|
||||
Description=swayidle idle lock (locks via noctalia-shell IPC, suspends on lid)
|
||||
Documentation=https://github.com/swaywm/swayidle
|
||||
After=graphical-session.target
|
||||
# Restart together with the session, not merely at boot: this unit is wanted by
|
||||
# default.target, so a compositor restart used to leave it orphaned while the target came
|
||||
# back without it. Refs #47.
|
||||
PartOf=graphical-session.target
|
||||
# With Restart=always below, a fast failure loop must never wedge the watcher into `failed`:
|
||||
# a lock that keeps trying beats a lock that stopped trying. Refs #47.
|
||||
StartLimitIntervalSec=0
|
||||
|
||||
[Service]
|
||||
# Locks via the noctalia v5 shell; `noctalia msg` resolves the running instance
|
||||
@@ -11,7 +18,10 @@ 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
|
||||
# `swayidle -w` exits 0 on SIGTERM, and that is how this watcher died on 2026-10-12: a clean
|
||||
# exit that `Restart=on-failure` cannot see, leaving the box unlocked for a week. A supervisor
|
||||
# restarts always. Refs #47.
|
||||
Restart=always
|
||||
RestartSec=5s
|
||||
|
||||
[Install]
|
||||
|
||||
@@ -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
|
||||
@@ -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`. State hides inside a tracked file too: `dconf/dump.txt` carried `last-folder-path`, a nautilus `window-state` section and `picture-uri`/`picture-uri-dark` — all `/home/dank` values, and the wallpaper URIs were live on blankpad, i.e. already deployed here — dropped by #44, see [tools/dotfiles-deploy](/tools/dotfiles-deploy.md). |
|
||||
| **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.
|
||||
|
||||
@@ -16,6 +16,55 @@ 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 — two instruments rescued from `$HOME` script sediment
|
||||
|
||||
`$HOME` had accumulated 123 loose scripts at its root (archived to
|
||||
`.AGENTTRASH/home-scripts-2026-10-20`, ~1.8 GiB of stray audio consolidated into
|
||||
`~/audiosamples`). Two were durable instruments and now live in the repo with
|
||||
black-box tests and pages: [tools/remote-job](tools/remote-job.md) and
|
||||
[tools/pcie-probe](tools/pcie-probe.md). Refs #53. Laws learned:
|
||||
|
||||
- **A readiness pattern in a log is not evidence of completion.** `ssh host 'cmd |
|
||||
tail -f log | sed -n /ready/q'` fails three independent ways — the broker aborts
|
||||
the long connection, the pattern can match a header printed before the work
|
||||
starts, and `sed -n /x/q` exits the moment it matches, tearing down the job it was
|
||||
watching. The only honest signal is a sentinel the job writes last, read over a
|
||||
short-lived connection.
|
||||
- **Never benchmark beside a live service, and never let a tool decide to stop one.**
|
||||
The guard refuses before anything is built; unloading a production endpoint is a
|
||||
human's call. A number taken as noise beside serving traffic is both disruptive and
|
||||
wrong.
|
||||
- **An instrument that segfaults is not an instrument.** Two of the three shipped
|
||||
`.hip` probes called `hipGetDeviceProperties`, which crashes on this ROCm build
|
||||
while `hipGetDeviceCount` works fine; device identity now comes from sysfs, and the
|
||||
trap is recorded in the sources so it is not reintroduced.
|
||||
|
||||
## 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
|
||||
|
||||
@@ -0,0 +1,255 @@
|
||||
---
|
||||
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`; a unit is **started** only when
|
||||
systemd reports it as nothing-yet (`""`/`disabled`), and never when its `ExecStart` binary
|
||||
is absent here, so a unit belonging to another box stays inert instead of crash-looping.
|
||||
Starting something already installed is what makes `deploy.sh` unsafe to run beside a live
|
||||
session — see *Never start a unit a session may already be running*.
|
||||
|
||||
**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 8 tracked / 2 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.
|
||||
|
||||
How to read `--check`, measured on blankpad: it prints `RELINK` for **every** path the `zz`
|
||||
tier carries, even when the live link already points at the override — `rules.kdl`,
|
||||
`keybinds.kdl`, `misc.kdl`, `autostart.kdl` and `.zshrc` all did — because the walk
|
||||
processes the base package first and reports the intermediate owner change. A `RELINK` line
|
||||
therefore proves a pending change only when `readlink` shows the live target is *not*
|
||||
already the `zz-$HOST` file. `actions=0` is the only honest summary.
|
||||
## Law: never start a unit a session may already be running
|
||||
|
||||
`sync` used to end with `systemctl --user enable --now` for every unit in `systemd-user/`
|
||||
whose `ExecStart` resolves. Measured on blankpad before one such run:
|
||||
|
||||
|unit|`UnitFileState`|`ActiveState`|
|
||||
|---|---|---|
|
||||
|`live-sync.service`|`linked`|`inactive`|
|
||||
|`shelly-notifications.service`|`enabled`|`active`|
|
||||
|`swayidle-lock.service`|`enabled`|`inactive`|
|
||||
|
||||
`swayidle-lock.service` was installed and **not running** — nothing started it, no swayidle
|
||||
process existed (`pgrep -c swayidle` = 0), and `autostart.kdl` does not launch one. A
|
||||
`deploy.sh` run would have armed it: monitors blanking and locking on the idle timer while
|
||||
the operator was mid-sentence. `live-sync.service` is `linked`, driven by its timer;
|
||||
`--now` forces a heartbeat commit at an arbitrary moment.
|
||||
|
||||
Two rules follow. Install state must be read from the **printed state**, not the exit
|
||||
status — `is-enabled` exits 1 for `linked` exactly as for `disabled`, so exit status cannot
|
||||
tell "installed, driven by a timer" apart from "never installed". And a unit is started only
|
||||
in the latter case: a machine with a session on it has already decided what runs, while a
|
||||
fresh box — where everything reports `disabled` — still gets its units up. Both directions
|
||||
are pinned in `tests/deploy.bats` over a stub `systemctl`, because the real one on this box
|
||||
is exactly what must not be invoked from a test. Refs #44.
|
||||
|
||||
## Law: expand systemd specifiers before calling a unit foreign
|
||||
|
||||
`ExecStart=%h/.omp/scripts/live-sync.sh` is valid systemd and meaningless to a shell. The
|
||||
resolution check handed that literal string to `command -v`, which fails everywhere, so the
|
||||
deployment concluded the unit belonged to another machine and skipped it — silently, through
|
||||
the same code path that is supposed to keep a *foreign* unit inert. The unit it kept skipping
|
||||
was this repo's own heartbeat, and `%h` is precisely where this checkout lives, so the false
|
||||
verdict was the same on every box that deployed it. Measured on blankpad: the first real
|
||||
`deploy.sh` run printed `SKIP live-sync.service (ExecStart '%h/.omp/scripts/live-sync.sh'
|
||||
absent on this host)` while `/home/blank/.omp/scripts/live-sync.sh` sits right there.
|
||||
|
||||
Expand the specifiers a user unit can carry — `%h`, `%u`, `%U` — before resolving, and let an
|
||||
unknown specifier fail rather than guess at it. The probe needs no privileges and writes
|
||||
nothing:
|
||||
|
||||
```bash
|
||||
bin=$(sed -n 's/^ExecStart=\([^ ]*\).*/\1/p' \
|
||||
~/.omp/dotfiles/systemd-user/.config/systemd/user/live-sync.service | head -1)
|
||||
command -v "${bin//%h/$HOME}" # resolves → the unit is this box's; the SKIP was the lie
|
||||
```
|
||||
|
||||
## Law: the dconf dump holds preferences, not per-box state
|
||||
|
||||
`dconf load /` writes every key in `dotfiles/dconf/dump.txt`, so the dump carries whatever
|
||||
box it was taken from. Comparing key by key against live state on blankpad found **six of
|
||||
24** keys that were not this box's, and not one of them was a preference:
|
||||
|
||||
[org/gnome/portal/filechooser/firefox] last-folder-path
|
||||
repo: '/home/dank/Downloads' live: '/home/blank/Downloads'
|
||||
[org/gnome/nautilus/window-state] initial-size, initial-size-file-chooser, maximized
|
||||
[org/gnome/desktop/background] picture-uri, picture-uri-dark
|
||||
both: 'file:///home/dank/.config/background'
|
||||
|
||||
A file-chooser memory, a window geometry, a wallpaper URI: application state that arrived
|
||||
with the dotfiles import. The last five did not appear as conflicts at all, because
|
||||
blankpad's **live** tree already held the same `/home/dank` values — an earlier deploy had
|
||||
written them here. A foreign value that agrees with live is invisible to any diff and still
|
||||
wrong. All six come out in #44; the two wallpaper URIs are also cleared on this box
|
||||
(`gsettings set org.gnome.desktop.background picture-uri ''`, and its `-dark` twin), because
|
||||
niri draws the wallpaper, noctalia owns it, and nothing reads a dead path.
|
||||
|
||||
Compare **key by key**, not with a plain `diff`: `dconf load` writes only the keys present,
|
||||
so live-only keys survive and a diff overstates the damage in one direction and understates
|
||||
it in the other. Re-dump on the box whose settings are canonical (`dconf dump / >
|
||||
dotfiles/dconf/dump.txt`), then delete the state sections before committing — the census's
|
||||
*Never track: app state* class applies inside a tracked file, not just to whole directories.
|
||||
Refs #44.
|
||||
|
||||
## Law: a watcher must restart on a clean exit too
|
||||
|
||||
`swayidle-lock.service` was `enabled`, present in `default.target.wants/`, and **dead for eight
|
||||
days**: `Result=success`, `NRestarts=0`, gone since 2026-10-12 16:22 after 2d 21h of uptime.
|
||||
`Restart=on-failure` cannot see a clean exit, and `swayidle -w` exits 0 on SIGTERM. The trigger is
|
||||
attributable to eight seconds: **`niri` and `noctalia` both started `Mon Oct 12 16:22:31 2026`**
|
||||
(`ps -o lstart=`) while swayidle's last state change was `16:22:23` — the compositor session
|
||||
restarted, the watcher went with it, and because it was wanted by `default.target` rather than tied
|
||||
to the session target, nothing brought it back. The user journal keeps no entries for that window,
|
||||
so the attribution rests on process ages rather than logs, which is its own lesson: diagnose a
|
||||
lapse within the retention you actually have.
|
||||
|
||||
**The severity claim in this page's first draft was wrong, and is corrected here.** It said closing
|
||||
the lid suspended the machine unlocked, because the `before-sleep` hook lived in the dead unit. But
|
||||
`systemd-inhibit --list` shows `noctalia … sleep "Lock before sleep" delay` — the shell holds its
|
||||
own sleep inhibitor and locks on suspend independently of swayidle, so the suspend path was covered
|
||||
the whole time. What was actually dead for eight days is the **idle** lock: five minutes of
|
||||
inactivity locked nothing, and `after-resume` never fired. `pgrep -c -u blankpad
|
||||
'swayidle|swaylock|hyprlock|gtklock'` = 0 remains true; it simply did not mean *no lock path at
|
||||
all*, because a locker can be a hook inside a running shell rather than a name on the process list.
|
||||
Read the inhibitor table, not only `pgrep`.
|
||||
|
||||
Three settings, each with a job: `Restart=always`, because a supervisor must not assume it can
|
||||
only die badly; `StartLimitIntervalSec=0`, because a fast loop must never wedge it into `failed` —
|
||||
a lock that keeps trying beats one that stopped trying; and `PartOf=graphical-session.target`,
|
||||
because the unit is wanted by `default.target`, so the session coming back did not bring *it*
|
||||
back.
|
||||
|
||||
Verify by behaviour rather than by reading the file — the point is surviving the event that killed
|
||||
it in October:
|
||||
|
||||
```bash
|
||||
systemctl --user start swayidle-lock.service
|
||||
kill -TERM "$(systemctl --user show swayidle-lock.service -p MainPID --value)"
|
||||
sleep 8 && systemctl --user show swayidle-lock.service -p NRestarts -p MainPID -p ActiveState
|
||||
```
|
||||
|
||||
An idle inhibitor also disarms the timeout outright, which surprises people: while an `omp` agent
|
||||
session is running, swayidle logs `Not enabling timeouts: idle inhibitor found` and arms no idle
|
||||
timeout at all — `systemd-inhibit --list` shows `omp … idle "omp agent session" block`. A box with
|
||||
an agent working therefore does not lock on inactivity, by harness design, while `before-sleep` and
|
||||
`after-resume` keep working. Anyone asking "why did the screen never lock" should read the inhibitor
|
||||
table before suspecting the unit.
|
||||
|
||||
The general shape: `deploy.sh` only ever *enables*. Nothing on any box re-checks that a unit the
|
||||
system depends on is still running, so a security posture can lapse silently and stay lapsed for a
|
||||
week. A liveness check for this tier belongs in the heartbeat, not in a human's memory. Refs #47.
|
||||
@@ -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.
|
||||
@@ -18,6 +19,9 @@ Configuration guides and reference material for installed software. Each tool ha
|
||||
* [neovim](neovim.md) — Two editors in one binary: vanilla `nvim` for quick edits and the `lazyvim` NVIM_APPNAME profile, with the base16 wallpaper-palette hook and fresh-box install.
|
||||
* [niri](niri/index.md) — Scrollable-tiling Wayland compositor — configuration, keybindings, and architecture for the workstation.
|
||||
* [noctalia-shell](noctalia-shell.md) — Native desktop environment for Niri — launcher, control center, settings, lock screen, session menu, hotkey overlay, media controls, and system management.
|
||||
* [packages](packages.md) — `packages/*.pkgs` desired state and the two tools that keep a machine and its manifest agreeing: `install.sh` installs the delta, `pkg-inventory.sh` reports what the machine has outgrown.
|
||||
* [pcie-probe](pcie-probe.md) — HIP probes that separate PCIe link bandwidth from the engine's read path at the expert-slab size, and the refuse-by-default guard against benchmarking beside a live service.
|
||||
* [remote-job](remote-job.md) — Detached job runner for `ssh` work: completion sentinels on the host replace the `tail -f | sed -n /ready/q` pattern that reported success on jobs that never finished.
|
||||
* [tmux](tmux.md) — Terminal multiplexer configuration, extended-keys setup, keyboard input best practices, and reload/restart procedures.
|
||||
|
||||
## Adding a New Tool
|
||||
|
||||
@@ -0,0 +1,106 @@
|
||||
---
|
||||
type: reference
|
||||
title: package desired state
|
||||
description: packages/*.pkgs plus the two tools that keep a machine and its manifest agreeing — install.sh installs desired-minus-installed, pkg-inventory.sh reports installed-minus-declared.
|
||||
tags:
|
||||
- packages
|
||||
- pacman
|
||||
- provisioning
|
||||
- fleet
|
||||
- blankpad
|
||||
verified: 2026-10-20
|
||||
timestamp: 2026-10-20
|
||||
---
|
||||
|
||||
# Package desired state
|
||||
|
||||
Two tools, one invariant: **the manifests must describe the machine.** A rebuild is only faithful
|
||||
if both directions are checked.
|
||||
|
||||
|tool|direction|writes|
|
||||
|---|---|---|
|
||||
|`scripts/install.sh`|desired → installed|installs `desired − installed` with `--apply`; never removes|
|
||||
|`scripts/pkg-inventory.sh`|installed → declared|nothing — read-only report|
|
||||
|
||||
## Manifests
|
||||
|
||||
`packages/common.pkgs` (every host) + `packages/<host>.pkgs` (this host) + `packages/aur.pkgs`
|
||||
(AUR-only, installed through `yay`). The parser is `sed 's/#.*//'` then the first field, so `#`
|
||||
comments trail freely and one token per line is the whole grammar. An unknown `--host` gets the
|
||||
common manifest only: a wrong-host run is a no-op, not a hazard.
|
||||
|
||||
## Rebuild a workstation
|
||||
|
||||
1. Install the OS (CachyOS, niri/noctalia profile). The installer's own package set is *not* in the
|
||||
manifests and must not be — see the baseline law below.
|
||||
2. Clone this repo to `~/.omp`, then `scripts/install.sh --apply` — packages first, because the
|
||||
enablement step needs `sshd` and `firewalld` present ([dotfiles-deploy](dotfiles-deploy.md)).
|
||||
3. `scripts/deploy.sh sync` for the config tier ([dotfiles-deploy](dotfiles-deploy.md)).
|
||||
4. `scripts/pkg-inventory.sh --emit` on a *known-good* machine and fold the output into
|
||||
`packages/<host>.pkgs` / `packages/aur.pkgs` before step 2 — that is how a hand install stops
|
||||
being invisible to the next rebuild.
|
||||
|
||||
## pkg-inventory report
|
||||
|
||||
```
|
||||
undeclared additions (declare these): N ← exit 1 when N > 0
|
||||
repo (sync dbs): <names> → belongs in packages/<host>.pkgs
|
||||
aur (needs a helper): <names> → belongs in packages/aur.pkgs
|
||||
installer-provided, undeclared: N + names → a reinstall supplies these; do not declare
|
||||
declared, not installed: <names> → install.sh installs these
|
||||
orphans ignored: <names> → pacman -Qtdq
|
||||
flatpak: <app ids> → reported only, install.sh installs no flatpaks
|
||||
```
|
||||
|
||||
Exit codes: `0` clean · `1` drift · `2` usage · `3` classification impossible. Overrides
|
||||
`PKGINV_REPO` / `PKGINV_HOST` / `PKGINV_LOG` / `PKGINV_BASELINE` exist for tests.
|
||||
|
||||
## Law: the installer's own day is the baseline
|
||||
|
||||
Whether a package is *yours* is decided by the pacman log, not by opinion: the baseline date is the
|
||||
oldest `[ALPM] installed` entry — Calamares' pacstrap — and everything installed **on or before** it
|
||||
is installer furniture. Without that cut, a base install reports several hundred "additions" and the
|
||||
list nobody can act on is worse than no list.
|
||||
|
||||
- **Read the rotated logs too** (`pacman.log.N`, `.N.gz`). On a machine older than the rotation
|
||||
window the install day exists only there; skip them and the whole base system is reported as
|
||||
hand-installed.
|
||||
- Calamares keeps installing the desktop for minutes after the base transaction, which is why the
|
||||
date cut is inclusive.
|
||||
|
||||
## Law: refuse rather than guess the AUR split
|
||||
|
||||
`aur.pkgs` vs `<host>.pkgs` is decided by whether any sync database can serve the name — Chaotic-AUR
|
||||
counts as a sync database, so only true AUR belongs in `aur.pkgs`. If the sync databases are
|
||||
unavailable the tool exits `3` instead of assuming: guessing sends every package to `yay`, which is
|
||||
how a manifest becomes a pile of source builds.
|
||||
|
||||
Classify with `pacman -Sl` (world-readable). **`pacman -Sqq` requires root** — a tool that only
|
||||
reads the machine must not need sudo to answer a question about it.
|
||||
|
||||
**Known gap:** `aur.pkgs` is read for *every* host, so a desktop-only AUR package (a niri module, a
|
||||
GUI client) cannot be declared there without asking the headless servers to build it. Such a name is
|
||||
still reported as an AUR addition; leave it undeclared until `install.sh` grows a per-host AUR
|
||||
manifest.
|
||||
|
||||
## Law: orphans never reach a manifest
|
||||
|
||||
`pacman -Qtdq` names are packages nothing depends on; emitting them would teach a fresh machine to
|
||||
install the same dead weight. They are printed as `orphans ignored` and excluded from `--emit`.
|
||||
|
||||
## Two traps that produced this tool
|
||||
|
||||
- **`set -o pipefail` plus `head -1`**: `head` closes the pipe, `sort` dies of `SIGPIPE`, the
|
||||
substitution returns `141`, and `set -e` aborts the script. It survives a small test fixture and
|
||||
fails on a real 1000-line install-day log — take a minimum in one pass (`awk 'END'`-style), never
|
||||
sort-then-first. The same applies to `awk 'NR==1{exit}'`, which closes the pipe exactly as `head`
|
||||
does.
|
||||
- **`tr '\n' ' '` swallows the final newline** and `fold` faithfully preserves that absence, so the
|
||||
next report label lands on the same line as the last name. `tr '\n' ' '; echo` instead.
|
||||
|
||||
## Related
|
||||
|
||||
- [tools/index](index.md) · [dotfiles-deploy](dotfiles-deploy.md) (the config tier that follows) ·
|
||||
[live-sync](live-sync.md) (the heartbeat that commits machine state)
|
||||
- Tests: `tests/pkg-inventory.bats` (fixtures + stubbed `pacman`, no real package database touched).
|
||||
- Refs #51.
|
||||
@@ -0,0 +1,92 @@
|
||||
---
|
||||
type: reference
|
||||
title: pcie-probe — link or read path
|
||||
description: HIP instruments that separate PCIe link bandwidth from the engine's read path at the real expert-slab size, plus the refuse-by-default guard that stops a benchmark from running beside a live service.
|
||||
tags:
|
||||
- gpu
|
||||
- rocm
|
||||
- rdna4
|
||||
- benchmarking
|
||||
- dankserver
|
||||
verified: 2026-10-20
|
||||
timestamp: 2026-10-20
|
||||
---
|
||||
|
||||
# pcie-probe
|
||||
|
||||
`scripts/pcie-probe.sh` answers one question, which comes up every time MoE
|
||||
expert offload is discussed on the R9700 box
|
||||
([dankserver](../system/dankserver.md)): **is the stall the PCIe link, or is it
|
||||
the engine's read path?** The two look identical from the outside — decode is
|
||||
slow while weights stream in — and they have opposite fixes.
|
||||
|
||||
## Instruments
|
||||
|
||||
Sources in `scripts/probes/`, built with the host `hipcc`:
|
||||
|
||||
| Probe | Question it answers |
|
||||
|-------|---------------------|
|
||||
| `hip_smoke.hip` | is host ROCm usable from a plain process at all (count, alloc, copy) |
|
||||
| `pcie_bw.hip` | host→device bandwidth at the engine's exact slab size, pinned vs pageable |
|
||||
| `pcie_probe.hip` | the size sweep — where throughput collapses as transfers shrink |
|
||||
|
||||
```bash
|
||||
scripts/pcie-probe.sh # guard, build, topology, measure
|
||||
scripts/pcie-probe.sh --device 0 --device 1
|
||||
scripts/pcie-probe.sh --build-only # compile, never touch the cards
|
||||
```
|
||||
|
||||
## The law: never benchmark beside a live service
|
||||
|
||||
The guard runs **before anything is built or run** and refuses when the GPU is
|
||||
occupied — `gpu_busy_percent` above `PCIE_PROBE_MAX_BUSY` (default 5 %) or any
|
||||
process holding `/dev/kfd`. Refusal is exit `3`, and it stops nothing.
|
||||
|
||||
Auto-stopping a production endpoint is not this tool's decision to make, and a
|
||||
measurement taken as noise beside serving traffic is worthless anyway: it is
|
||||
both disruptive and wrong. Take the service down deliberately, or pass `--force`
|
||||
and mean it. `tests/pcie-probe.bats` pins that the refusal happens before the
|
||||
build step, so a contended box cannot be measured by accident.
|
||||
|
||||
## Reading the numbers
|
||||
|
||||
The reference is the **link**, printed as a topology block from sysfs
|
||||
(`current_link_width` / `current_link_speed` against `max_*`): a card trained to
|
||||
fewer lanes or a lower generation than it is wired for caps everything
|
||||
downstream, and no engine change will recover it.
|
||||
|
||||
Then the sweep table, whose first row is the engine's actual expert-slab size —
|
||||
throughput at *that* size is what matters, not the big-transfer figure:
|
||||
|
||||
- Small transfers reach the trained-link ceiling → the link is healthy, the
|
||||
engine's read path (fetch granularity, pipelining, pinning) is the limit.
|
||||
- Small transfers stall far below a large-transfer figure → the wall is the
|
||||
link or the IOMMU path, not the engine.
|
||||
- Pinned versus pageable columns apart means pinning is load-bearing; together
|
||||
means the copies are already pinned by someone else.
|
||||
|
||||
Measure one device at a time (`--device`). A run that lets the runtime pick
|
||||
several devices cannot attribute bandwidth to a card at all.
|
||||
|
||||
## The trap carried in the sources
|
||||
|
||||
`hipGetDeviceProperties` **segfaults** on the ROCm build these probes were
|
||||
written against, while `hipGetDeviceCount` works fine and reports every device.
|
||||
None of the three sources calls it; device identity comes from sysfs instead.
|
||||
The setvbuf call in `hip_smoke.hip` is a fossil of discovering that — an
|
||||
unflushed stdout hid the crash point for an afternoon. Keep both facts intact
|
||||
when touching these files.
|
||||
|
||||
## Exit status and seams
|
||||
|
||||
`0` measured · `2` usage or no sources · `3` refused, GPU busy · `4` no HIP
|
||||
toolchain or build failed. A client box without the SDK exits `4` loudly rather
|
||||
than reporting an empty success; the probes belong on the box with the cards.
|
||||
|
||||
Test seams (all documented in the script header): `PCIE_PROBE_SRC`,
|
||||
`PCIE_PROBE_BUILD`, `PCIE_PROBE_SYSFS`, `PCIE_PROBE_KFD`, `PCIE_PROBE_HIPCC`,
|
||||
`PCIE_PROBE_MAX_BUSY` — the sysfs/kfd paths are what make a refusal testable on
|
||||
a machine with no cards, which is the only honest way to test a refusal.
|
||||
|
||||
Measured campaign numbers are not kept here; they belong with the endpoint they
|
||||
were taken on, in the serverAppConfigs LLM runbook vault.
|
||||
@@ -0,0 +1,83 @@
|
||||
---
|
||||
type: reference
|
||||
title: remote-job — detached jobs that outlive the session
|
||||
description: Run a command on a remote box so its result survives the connection, the restart, and the agent that started it — the replacement for ssh piped through tail and sed, which reported success on jobs that never finished.
|
||||
tags:
|
||||
- ssh
|
||||
- fleet
|
||||
- automation
|
||||
- verification
|
||||
verified: 2026-10-20
|
||||
timestamp: 2026-10-20
|
||||
---
|
||||
|
||||
# remote-job
|
||||
|
||||
`scripts/remote-job.sh` starts a command **detached on the host**, and reports
|
||||
what actually happened from files on disk. It exists because
|
||||
`ssh host 'cmd | tail -f log | sed -n /ready/q'` lied repeatedly during the
|
||||
worktree campaign.
|
||||
|
||||
## Why the pipe lies
|
||||
|
||||
Three independent failure modes, all of them structural:
|
||||
|
||||
- **The broker kills long-lived ssh.** A job longer than the connection's
|
||||
patience dies mid-run; the caller sees a closed pipe, not a failure.
|
||||
- **A readiness pattern can match a log header.** `sed -n /building/q` fires on
|
||||
a banner printed before the work starts — the job is at line 1 and the caller
|
||||
already moved on.
|
||||
- **`sed -n /x/q` exits the instant it matches.** Even when the pattern is
|
||||
genuinely ready-shaped, the pipeline tears down the remote command it was
|
||||
watching. Result: green report, dead job.
|
||||
|
||||
The only reliable signal is a **completion sentinel written by the job itself**,
|
||||
read afterwards by a short-lived connection. That is what this is.
|
||||
|
||||
## Contract
|
||||
|
||||
```bash
|
||||
remote-job.sh start NAME [--] CMD... # launch detached, return at once
|
||||
remote-job.sh status [NAME] # one line per job; exit 0 only if all DONE
|
||||
remote-job.sh wait NAME [SEC] [N] # block until DONE or timeout, then tail N lines
|
||||
remote-job.sh out NAME [N] # tail a job's output
|
||||
remote-job.sh tail [NAME] # follow one job, or every job at once
|
||||
remote-job.sh watch # refreshing status, for a spare pane
|
||||
```
|
||||
|
||||
Job dir `$REMOTE_JOBS_DIR` (default `$HOME/jobs`). Per job: `NAME.out` (stdout+
|
||||
stderr), `NAME.rc` (exit code), `NAME.done` (sentinel, **written last**).
|
||||
|
||||
Exit status of the *report*: `0` all reported jobs finished · `1` unfinished or
|
||||
timed out · `2` usage. The `rc=` column is the job's own code — a job that
|
||||
failed is still `DONE rc=7` and `status` exits `0`, because "is it finished" and
|
||||
"did it work" are different questions a supervisor must be able to ask
|
||||
separately.
|
||||
|
||||
Written for the case where the job **restarts the endpoint the agent itself is
|
||||
running on**: the ssh connection dies, the session may die, and the answer is
|
||||
still sitting in `$JOBS` when a session comes back.
|
||||
|
||||
## Use over ssh
|
||||
|
||||
```bash
|
||||
ssh dankserver ~/.omp/scripts/remote-job.sh start swap -- \
|
||||
bash -c 'systemctl --user restart frank-archon && <the health probe that endpoint answers>'
|
||||
ssh dankserver ~/.omp/scripts/remote-job.sh wait swap 300 20 # short-lived reconnect
|
||||
```
|
||||
|
||||
Each ssh call is short, so there is nothing there to abort, and the second call
|
||||
can be made from a fresh session on a machine that rebooted underneath it.
|
||||
|
||||
## Failure modes
|
||||
|
||||
| Event | Result |
|
||||
|-------|--------|
|
||||
| Session/connection dies mid-job | job keeps running; `status` from anywhere reports it |
|
||||
| Command name is wrong | `PENDING (no output yet)`, exit 1 — never a false DONE |
|
||||
| Job exits non-zero | `DONE rc=N`; the code is data, not the report's status |
|
||||
| Two jobs, same NAME | the second replaces the first's state files |
|
||||
| Job dir missing | created on first use |
|
||||
|
||||
Pinned by `tests/remote-job.bats` (black-box: output, exit codes, sentinel
|
||||
ordering — never internals).
|
||||
@@ -3,3 +3,35 @@
|
||||
# pkgbase amd-ucode (split pkgname amd-ucode). No module options needed.
|
||||
|
||||
amd-ucode
|
||||
|
||||
# Hand-installed additions, measured by scripts/pkg-inventory.sh on 2026-10-20 against the
|
||||
# Calamares install day (baseline 2026-09-07). These stay out of common.pkgs: none of them belong
|
||||
# on the headless servers. install.sh installs them only where they are missing, so this list
|
||||
# changes nothing on the running box — it is what makes the next rebuild faithful.
|
||||
bun
|
||||
cachyos-gaming-applications
|
||||
curlftpfs
|
||||
flatpak
|
||||
fuse2
|
||||
fuseiso
|
||||
ghostty-nautilus
|
||||
glow
|
||||
gnome-calculator
|
||||
gnome-disk-utility
|
||||
nodejs
|
||||
npm
|
||||
obsidian
|
||||
partitionmanager
|
||||
plezy
|
||||
proton-mail-bin
|
||||
python-yaml
|
||||
quickshell
|
||||
shelly-flatpak-backend
|
||||
sshfs
|
||||
tmux
|
||||
ttf-proggyclean-nerd
|
||||
ttf-terminus-nerd
|
||||
wine-cachyos
|
||||
|
||||
# nirimod-git is an AUR addition deliberately left undeclared: aur.pkgs is read by every host, and
|
||||
# a niri module must not be built on the servers. See the known gap in omp-docs/tools/packages.md.
|
||||
|
||||
+30
-2
@@ -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
|
||||
@@ -115,9 +123,29 @@ if [ "$MODE" != "check" ] && [ -d "$PKGS_DIR/systemd-user/.config/systemd/user"
|
||||
for unit in "$PKGS_DIR"/systemd-user/.config/systemd/user/*.service; do
|
||||
[ -e "$unit" ] || continue
|
||||
bin="$(sed -n 's/^ExecStart=\([^ ]*\).*/\1/p' "$unit" | head -1)"
|
||||
# systemd expands specifiers inside ExecStart; this shell does not. Matching the literal
|
||||
# `%h/…` makes a unit of THIS box look foreign and skips it in silence — which is exactly
|
||||
# how live-sync.service, the repo's own heartbeat, has been reported "absent on this
|
||||
# host" on every machine that deployed it. Expand what a user unit can carry; leave any
|
||||
# other specifier unmatched rather than guess at it. Refs #44.
|
||||
bin="${bin//%h/$HOME}"
|
||||
bin="${bin//%u/$(id -un)}"
|
||||
bin="${bin//%U/$(id -u)}"
|
||||
if [ -n "$bin" ] && command -v "$bin" >/dev/null 2>&1; then
|
||||
systemctl --user enable --now "$(basename "$unit")" || true
|
||||
echo "ENABLE $(basename "$unit") (ExecStart resolves here)"
|
||||
name="$(basename "$unit")"
|
||||
# Read the state, not the exit status: `is-enabled` exits 1 for a `linked` unit exactly
|
||||
# as for a `disabled` one. Starting something already installed is how a deploy locks a
|
||||
# live session — swayidle-lock sits enabled/inactive on blankpad and `--now` arms it
|
||||
# mid-conversation. Only a unit nothing has installed yet gets started. Refs #44.
|
||||
state="$(systemctl --user is-enabled "$name" 2>/dev/null || true)"
|
||||
case "$state" in
|
||||
"" | disabled)
|
||||
systemctl --user enable --now "$name" || true
|
||||
echo "ENABLE $name (ExecStart resolves here; nothing running it yet)" ;;
|
||||
*)
|
||||
systemctl --user enable "$name" || true
|
||||
echo "ENABLE $name (already $state; not started — a live session decides what runs)" ;;
|
||||
esac
|
||||
else
|
||||
echo "SKIP $(basename "$unit") (ExecStart '$bin' absent on this host)"
|
||||
fi
|
||||
|
||||
Executable
+126
@@ -0,0 +1,126 @@
|
||||
#!/usr/bin/env bash
|
||||
# scripts/pcie-probe.sh — is the decode bottleneck the LINK, or the engine's read path?
|
||||
#
|
||||
# Instruments live in scripts/probes/: hip_smoke.hip answers "is the host ROCm usable from a plain
|
||||
# process", pcie_bw.hip measures host→device bandwidth at the engine's real expert-slab size, and
|
||||
# pcie_probe.hip sweeps sizes so a stall can be attributed to the link rather than to the engine.
|
||||
# Written during the RDNA4 expert-offload campaign, kept because the same cards are in Dankserver.
|
||||
#
|
||||
# LAW — never benchmark beside a live service. The guard below runs BEFORE anything is built or run,
|
||||
# and it stops nothing: putting a production endpoint down is not this tool's decision to make. When
|
||||
# you have deliberately decided that contention is acceptable, say so with --force.
|
||||
#
|
||||
# Trap carried in the sources: hipGetDeviceProperties SEGFAULTS on this ROCm build even though
|
||||
# hipGetDeviceCount works fine and reports every device. Nothing here calls it. Keep it that way.
|
||||
#
|
||||
# usage: pcie-probe.sh [--device N]... [--build-only] [--force]
|
||||
#
|
||||
# --device N measure this device; repeat for each card. Without it the probes run once with the
|
||||
# environment's own device selection — one shared run cannot attribute bandwidth.
|
||||
# --build-only compile and stop; never touches the cards.
|
||||
# --force measure even though the guard says the GPU is busy.
|
||||
#
|
||||
# Environment (defaults in parentheses) — the sysfs and kfd paths are overridable so the guard can
|
||||
# be tested on a machine with no cards, which is the only honest way to test a refusal:
|
||||
# PCIE_PROBE_SRC probe sources (<repo>/scripts/probes)
|
||||
# PCIE_PROBE_BUILD build output ($HOME/.cache/pcie-probe)
|
||||
# PCIE_PROBE_SYSFS drm sysfs root (/sys/class/drm)
|
||||
# PCIE_PROBE_KFD KFD device node (/dev/kfd)
|
||||
# PCIE_PROBE_HIPCC compiler (hipcc)
|
||||
# PCIE_PROBE_MAX_BUSY refusal threshold, percent (5)
|
||||
#
|
||||
# Exit status: 0 measured · 2 usage or missing sources · 3 REFUSED, GPU busy · 4 no toolchain or
|
||||
# build failed.
|
||||
|
||||
set -uo pipefail
|
||||
|
||||
SRC="${PCIE_PROBE_SRC:-$(dirname "$(realpath "$0")")/probes}"
|
||||
BUILD="${PCIE_PROBE_BUILD:-$HOME/.cache/pcie-probe}"
|
||||
SYSFS="${PCIE_PROBE_SYSFS:-/sys/class/drm}"
|
||||
KFD="${PCIE_PROBE_KFD:-/dev/kfd}"
|
||||
HIPCC="${PCIE_PROBE_HIPCC:-hipcc}"
|
||||
MAX_BUSY="${PCIE_PROBE_MAX_BUSY:-5}"
|
||||
|
||||
force=0
|
||||
build_only=0
|
||||
devices=()
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--device) [[ $# -ge 2 ]] || { echo "pcie-probe: --device needs an argument" >&2; exit 2; }
|
||||
devices+=("$2"); shift 2 ;;
|
||||
--build-only) build_only=1; shift ;;
|
||||
--force) force=1; shift ;;
|
||||
*) echo "usage: pcie-probe.sh [--device N]... [--build-only] [--force]" >&2; exit 2 ;;
|
||||
esac
|
||||
done
|
||||
|
||||
mapfile -t sources < <(find "$SRC" -maxdepth 1 -name '*.hip' 2>/dev/null | sort)
|
||||
if (( ${#sources[@]} == 0 )); then
|
||||
echo "pcie-probe: no .hip probe sources in $SRC" >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
# ---- the guard, before anything touches the card --------------------------------
|
||||
if (( ! force )); then
|
||||
busy=""
|
||||
if [[ -e "$KFD" ]] && command -v fuser > /dev/null 2>&1; then
|
||||
holders="$(fuser "$KFD" 2>/dev/null | tr -s '[:space:]' ' ')"
|
||||
[[ -n "${holders// /}" ]] && busy="KFD in use by:${holders}"
|
||||
fi
|
||||
for card in "$SYSFS"/card*; do
|
||||
[[ -f "$card/device/gpu_busy_percent" ]] || continue
|
||||
pct="$(cat "$card/device/gpu_busy_percent" 2>/dev/null)"
|
||||
[[ "$pct" =~ ^[0-9]+$ ]] || continue
|
||||
(( pct > MAX_BUSY )) && busy="${busy:+$busy; }$(basename "$card") at ${pct}%"
|
||||
done
|
||||
if [[ -n "$busy" ]]; then
|
||||
printf 'pcie-probe: REFUSED — GPU busy (%s)\n' "$busy" >&2
|
||||
printf ' never benchmark beside a live service. Take the service down deliberately,\n' >&2
|
||||
printf ' or pass --force if you have decided the contention is acceptable.\n' >&2
|
||||
exit 3
|
||||
fi
|
||||
fi
|
||||
|
||||
# ---- build ----------------------------------------------------------------------
|
||||
if ! command -v "$HIPCC" > /dev/null 2>&1; then
|
||||
printf 'pcie-probe: no HIP toolchain (%s) — run this on the box with the cards, not here\n' "$HIPCC" >&2
|
||||
exit 4
|
||||
fi
|
||||
mkdir -p "$BUILD"
|
||||
for src in "${sources[@]}"; do
|
||||
bin="$BUILD/$(basename "${src%.hip}")"
|
||||
if ! "$HIPCC" -O2 -o "$bin" "$src" 2>&1 | tail -3; then
|
||||
printf 'pcie-probe: build failed for %s\n' "$(basename "$src")" >&2
|
||||
exit 4
|
||||
fi
|
||||
done
|
||||
if (( build_only )); then
|
||||
printf 'pcie-probe: built %s probe(s) into %s\n' "${#sources[@]}" "$BUILD"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# ---- topology: the link the numbers must be read against ------------------------
|
||||
printf 'pcie-probe host=%s hipcc=%s probes=%s\n' "$(hostname)" "$HIPCC" "${#sources[@]}"
|
||||
for card in "$SYSFS"/card*; do
|
||||
[[ -f "$card/device/current_link_width" ]] || continue
|
||||
printf ' link %-9s current x%s @ %s max x%s @ %s\n' "$(basename "$card")" \
|
||||
"$(cat "$card/device/current_link_width")" \
|
||||
"$(cat "$card/device/current_link_speed" 2>/dev/null || echo '?')" \
|
||||
"$(cat "$card/device/max_link_width" 2>/dev/null || echo '?')" \
|
||||
"$(cat "$card/device/max_link_speed" 2>/dev/null || echo '?')"
|
||||
done
|
||||
|
||||
# ---- measure, per device --------------------------------------------------------
|
||||
(( ${#devices[@]} > 0 )) || devices=(default)
|
||||
for d in "${devices[@]}"; do
|
||||
if [[ $d == default ]]; then
|
||||
printf '\n########## engine device selection ##########\n'
|
||||
else
|
||||
printf '\n########## device %s ##########\n' "$d"
|
||||
fi
|
||||
for name in hip_smoke pcie_bw pcie_probe; do
|
||||
bin="$BUILD/$name"
|
||||
[[ -x "$bin" ]] || continue
|
||||
if [[ $d == default ]]; then "$bin"; else HIP_VISIBLE_DEVICES="$d" "$bin" "$d"; fi
|
||||
done
|
||||
done
|
||||
Executable
+159
@@ -0,0 +1,159 @@
|
||||
#!/usr/bin/env bash
|
||||
# scripts/pkg-inventory.sh — what does this machine have that packages/*.pkgs does not declare?
|
||||
#
|
||||
# install.sh walks desired → installed. This walks installed → declared, and the pair is what
|
||||
# makes a rebuild honest: a manifest the machine has quietly outgrown reproduces a machine that
|
||||
# is missing the tools you installed by hand six weeks ago, and nothing notices until the new box
|
||||
# has no tmux. Refs #51.
|
||||
#
|
||||
# The baseline is what separates "you installed this" from "the installer installed this". It is
|
||||
# read from the pacman log — the install-day transactions, dated at the oldest `[ALPM] installed`
|
||||
# line — because declaring Calamares furniture in a manifest is bookkeeping for something a
|
||||
# reinstall provides for free. Rotated logs (pacman.log.N, .N.gz) are read too: on a machine older
|
||||
# than the rotation window the install day lives only there, and skipping them would report the
|
||||
# entire base system as hand-installed.
|
||||
#
|
||||
# Report (stdout):
|
||||
# undeclared additions (declare these): N hand-installed, absent from the manifests
|
||||
# repo (sync dbs): <names> → belongs in packages/<host>.pkgs
|
||||
# aur (needs a helper): <names> → belongs in packages/aur.pkgs (yay)
|
||||
# installer-provided, undeclared: N a reinstall supplies these; do not declare
|
||||
# declared, not installed: N install.sh installs these
|
||||
# orphans ignored: <names> pacman -Qtdq — never emitted
|
||||
# flatpak: <app ids> reported only, install.sh installs no flatpaks
|
||||
#
|
||||
# --emit prints the additions as manifest lines, ready to fold into packages/. Names are bare
|
||||
# tokens so install.sh's `sed 's/#.*//'` + first-field parser reads them unchanged.
|
||||
#
|
||||
# Exit status: 0 clean · 1 drift (undeclared additions exist) · 2 usage · 3 cannot classify
|
||||
# (pacman log unreadable or sync databases unavailable — worse to guess than to say so, because
|
||||
# guessing would dump every package into aur.pkgs).
|
||||
#
|
||||
# Environment overrides (tests):
|
||||
# PKGINV_REPO repo root holding packages/ (default: git toplevel of this script)
|
||||
# PKGINV_HOST host name (default: $(hostname))
|
||||
# PKGINV_LOG space-separated pacman logs (default: /var/log/pacman.log + rotated)
|
||||
# PKGINV_BASELINE YYYY-MM-DD, inclusive (default: oldest install date in the logs)
|
||||
|
||||
set -euo pipefail
|
||||
export LC_ALL=C # comm/sort agreement: pacman's order is not this order
|
||||
|
||||
MODE="report"
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--emit) MODE="emit"; shift ;;
|
||||
-h | --help) sed -n '2,40p' "$0"; exit 0 ;;
|
||||
*) echo "usage: pkg-inventory.sh [--emit]" >&2; exit 2 ;;
|
||||
esac
|
||||
done
|
||||
|
||||
REPO="${PKGINV_REPO:-}"
|
||||
if [[ -z "$REPO" ]]; then
|
||||
REPO="$(git -C "$(dirname "$(realpath "$0")")" rev-parse --show-toplevel 2>/dev/null || true)"
|
||||
fi
|
||||
[[ -n "$REPO" && -d "$REPO/packages" ]] || { echo "pkg-inventory: no packages/ under ${REPO:-<repo>}" >&2; exit 2; }
|
||||
HOST="${PKGINV_HOST:-$(hostname)}"
|
||||
|
||||
TMP="$(mktemp -d)"; trap 'rm -rf "$TMP"' EXIT
|
||||
|
||||
read_manifest() { # same parser install.sh uses, so a name means the same thing to both
|
||||
sed 's/#.*//' "$1" | tr -d '\r' | awk '{if ($1) print $1}'
|
||||
}
|
||||
# ---- installed state ---------------------------------------------------------
|
||||
# Four queries are the tool's whole interface to pacman. -Sl is the unprivileged sync listing:
|
||||
# -Sqq asks for root, and a read of the machine must not need sudo. Field 2 of -Sl is the package
|
||||
# name, and one name can appear in several databases (core/extra/cachyos), hence the sort -u.
|
||||
pacman -Qqe > "$TMP/explicit"
|
||||
pacman -Qq > "$TMP/installed"
|
||||
pacman -Qtdq > "$TMP/orphans" 2>/dev/null || true # exit 1 when the list is empty: no orphans
|
||||
if ! pacman -Sl > "$TMP/sync.raw" 2>/dev/null || [[ ! -s "$TMP/sync.raw" ]]; then
|
||||
echo "pkg-inventory: no sync database available (run: sudo pacman -Sy) — repo vs AUR cannot be classified" >&2
|
||||
exit 3
|
||||
fi
|
||||
awk '{print $2}' "$TMP/sync.raw" | sort -u > "$TMP/sync"
|
||||
|
||||
# ---- declared desired state --------------------------------------------------
|
||||
: > "$TMP/declared"
|
||||
for m in common.pkgs "$HOST.pkgs" aur.pkgs; do
|
||||
[[ -f "$REPO/packages/$m" ]] || continue
|
||||
read_manifest "$REPO/packages/$m" >> "$TMP/declared"
|
||||
done
|
||||
|
||||
# ---- baseline: what the installer put here -----------------------------------
|
||||
logs=()
|
||||
if [[ -n "${PKGINV_LOG:-}" ]]; then
|
||||
read -ra logs <<< "$PKGINV_LOG"
|
||||
else
|
||||
logs=(/var/log/pacman.log)
|
||||
for f in /var/log/pacman.log.[0-9]*; do [[ -e "$f" ]] && logs+=("$f"); done
|
||||
fi
|
||||
read_logs() {
|
||||
local f
|
||||
for f in "${logs[@]}"; do
|
||||
[[ -r "$f" ]] || continue
|
||||
case "$f" in *.gz) zcat "$f" ;; *) cat "$f" ;; esac
|
||||
done
|
||||
}
|
||||
# "YYYY-MM-DD name" for every install event the log remembers.
|
||||
read_logs | sed -nE 's/^\[([0-9-]{10})T[^]]*\] \[ALPM\] installed ([^ ]+) \(.*/\1 \2/p' > "$TMP/events"
|
||||
[[ -s "$TMP/events" ]] || { echo "pkg-inventory: no install events readable in ${logs[*]} — baseline unknown" >&2; exit 3; }
|
||||
|
||||
# Minimum date in a single awk pass. Sorting and then taking the first line — whether with `head`
|
||||
# or with an awk that exits early — closes the pipe, and pipefail turns sort's SIGPIPE into a dead
|
||||
# script; a real install-day log is long enough for that to bite (it killed the first real run).
|
||||
BASELINE="${PKGINV_BASELINE:-$(awk '{ if (m == "" || $1 < m) m = $1 } END { print m }' "$TMP/events")}"
|
||||
[[ "$BASELINE" =~ ^[0-9]{4}-[0-9]{2}-[0-9]{2}$ ]] || { echo "pkg-inventory: bad PKGINV_BASELINE '$BASELINE' (want YYYY-MM-DD)" >&2; exit 2; }
|
||||
|
||||
# Inclusive: packages installed ON the baseline date came with the install too — Calamares keeps
|
||||
# installing the desktop for minutes after the base transaction.
|
||||
awk -v d="$BASELINE" '$1 <= d {print $2}' "$TMP/events" | sort -u > "$TMP/base"
|
||||
|
||||
for f in explicit installed sync declared base orphans; do sort -u -o "$TMP/$f" "$TMP/$f"; done
|
||||
|
||||
# ---- the sets ----------------------------------------------------------------
|
||||
# additions: explicit, not declared, and not something the installer handed us.
|
||||
comm -23 "$TMP/explicit" "$TMP/declared" > "$TMP/undeclared"
|
||||
comm -23 "$TMP/undeclared" "$TMP/base" > "$TMP/additions"
|
||||
comm -12 "$TMP/undeclared" "$TMP/base" > "$TMP/installside"
|
||||
comm -12 "$TMP/additions" "$TMP/sync" > "$TMP/add.repo"
|
||||
comm -23 "$TMP/additions" "$TMP/sync" > "$TMP/add.aur"
|
||||
comm -23 "$TMP/declared" "$TMP/installed" > "$TMP/missing"
|
||||
|
||||
n() { wc -l < "$1" | tr -d ' '; }
|
||||
|
||||
if [[ "$MODE" == emit ]]; then
|
||||
if [[ -s "$TMP/add.repo" ]]; then
|
||||
printf '# packages/%s.pkgs — additions measured %s (pkg-inventory.sh, baseline %s)\n' \
|
||||
"$HOST" "$(date +%F)" "$BASELINE"
|
||||
cat "$TMP/add.repo"
|
||||
fi
|
||||
if [[ -s "$TMP/add.aur" ]]; then
|
||||
printf '# packages/aur.pkgs — additions measured %s (AUR: not in any sync db, install via yay)\n' \
|
||||
"$(date +%F)"
|
||||
cat "$TMP/add.aur"
|
||||
fi
|
||||
exit 0
|
||||
fi
|
||||
|
||||
printf 'pkg-inventory host=%s baseline=%s repo=%s\n' "$HOST" "$BASELINE" "$REPO"
|
||||
printf 'counts declared=%s explicit=%s baseline=%s installed=%s\n' \
|
||||
"$(n "$TMP/declared")" "$(n "$TMP/explicit")" "$(n "$TMP/base")" "$(n "$TMP/installed")"
|
||||
printf 'undeclared additions (declare these): %s\n' "$(n "$TMP/additions")"
|
||||
printf ' repo (sync dbs): %s\n' "$(tr '\n' ' ' < "$TMP/add.repo")"
|
||||
printf ' aur (needs a helper): %s\n' "$(tr '\n' ' ' < "$TMP/add.aur")"
|
||||
printf 'installer-provided, undeclared (a reinstall provides these): %s\n' "$(n "$TMP/installside")"
|
||||
# The names, folded: this is the answer to "what did the installer mark explicit that I never
|
||||
# declared", and it is long — a count alone would force a second run to see it.
|
||||
# The echo matters: tr eats the final newline, fold preserves that absence, and the next label
|
||||
# would otherwise be glued to the last folded line.
|
||||
{ tr '\n' ' ' < "$TMP/installside"; echo; } | fold -s -w 88 | sed 's/^/ /'
|
||||
printf 'declared, not installed (install.sh installs these): %s\n' "$(tr '\n' ' ' < "$TMP/missing")"
|
||||
printf 'orphans ignored (pacman -Qtdq): %s\n' "$(tr '\n' ' ' < "$TMP/orphans")"
|
||||
if command -v flatpak > /dev/null 2>&1 && flatpak list --app --columns=application > "$TMP/flatpak" 2>/dev/null; then
|
||||
printf 'flatpak (reported only; install.sh installs no flatpaks): %s\n' "$(tr '\n' ' ' < "$TMP/flatpak")"
|
||||
else
|
||||
printf 'flatpak: not installed on this host\n'
|
||||
fi
|
||||
|
||||
[[ -s "$TMP/additions" ]] && exit 1
|
||||
exit 0
|
||||
@@ -0,0 +1,36 @@
|
||||
// Minimal HIP smoke test: is the host ROCm usable from a plain process?
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <hip/hip_runtime.h>
|
||||
|
||||
int main(void) {
|
||||
int n = -1;
|
||||
hipError_t e = hipGetDeviceCount(&n);
|
||||
printf("hipGetDeviceCount -> %d (%s), count=%d\n",
|
||||
(int)e, hipGetErrorString(e), n);
|
||||
if (e != hipSuccess || n <= 0) return 1;
|
||||
setvbuf(stdout, NULL, _IONBF, 0); /* an earlier crash lost the buffer, hiding the crash point */
|
||||
// Device properties are deliberately NOT queried here: hipGetDeviceProperties segfaults on the
|
||||
// ROCm build this probe was written for, even though hipGetDeviceCount above works fine. Card
|
||||
// identity comes from sysfs anyway — scripts/pcie-probe.sh prints the link topology.
|
||||
|
||||
const size_t bytes = 2459136; /* 2.344 MiB: the engine's expert slab */
|
||||
void *dev = NULL, *host = NULL;
|
||||
if (hipMalloc(&dev, bytes) != hipSuccess) { printf("hipMalloc failed\n"); return 1; }
|
||||
if (hipHostMalloc(&host, bytes, hipHostMallocDefault) != hipSuccess) {
|
||||
printf("hipHostMalloc failed\n"); return 1;
|
||||
}
|
||||
memset(host, 0x5a, bytes);
|
||||
|
||||
hipEvent_t a, b; hipEventCreate(&a); hipEventCreate(&b);
|
||||
hipDeviceSynchronize();
|
||||
hipEventRecord(a, 0);
|
||||
const int iters = 200;
|
||||
for (int i = 0; i < iters; i++) hipMemcpy(dev, host, bytes, hipMemcpyHostToDevice);
|
||||
hipEventRecord(b, 0); hipEventSynchronize(b);
|
||||
float ms = 0; hipEventElapsedTime(&ms, a, b);
|
||||
printf("\n %.3f MiB x %d iters in %.2f ms -> %.2f GB/s H2D (pinned)\n",
|
||||
bytes/1048576.0, iters, ms, (double)bytes*iters/(ms/1000.0)/1e9);
|
||||
printf(" Gen5 x8 ceiling = 31.5 GB/s\n");
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
// H2D bandwidth at the engine's expert-slab size.
|
||||
// Deliberately avoids hipGetDeviceProperties: it segfaults on this ROCm build
|
||||
// (hipGetDeviceCount works fine and reports 3 devices).
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <cstdlib>
|
||||
#include <hip/hip_runtime.h>
|
||||
|
||||
static double one(size_t bytes, bool pinned, int iters) {
|
||||
void *dev = NULL, *host = NULL;
|
||||
if (hipMalloc(&dev, bytes) != hipSuccess) return -1;
|
||||
if (pinned) { if (hipHostMalloc(&host, bytes, hipHostMallocDefault) != hipSuccess) return -1; }
|
||||
else { host = malloc(bytes); }
|
||||
memset(host, 0x5a, bytes);
|
||||
|
||||
hipEvent_t a, b; hipEventCreate(&a); hipEventCreate(&b);
|
||||
for (int i = 0; i < 3; i++) hipMemcpy(dev, host, bytes, hipMemcpyHostToDevice);
|
||||
hipDeviceSynchronize();
|
||||
|
||||
hipEventRecord(a, 0);
|
||||
for (int i = 0; i < iters; i++) hipMemcpy(dev, host, bytes, hipMemcpyHostToDevice);
|
||||
hipEventRecord(b, 0); hipEventSynchronize(b);
|
||||
float ms = 0; hipEventElapsedTime(&ms, a, b);
|
||||
double gb = (double)bytes * iters / (ms / 1000.0) / 1e9;
|
||||
|
||||
if (!pinned) free(host); else hipHostFree(host);
|
||||
hipFree(dev); hipEventDestroy(a); hipEventDestroy(b);
|
||||
return gb;
|
||||
}
|
||||
|
||||
int main(int argc, char **argv) {
|
||||
setvbuf(stdout, NULL, _IONBF, 0);
|
||||
int dev = (argc > 1) ? atoi(argv[1]) : 0;
|
||||
if (hipSetDevice(dev) != hipSuccess) { printf("setDevice(%d) failed\n", dev); return 1; }
|
||||
printf("device %d selected\n", dev);
|
||||
|
||||
size_t sz[] = {2459136UL, 3UL<<20, 16UL<<20, 64UL<<20, 256UL<<20, 1UL<<30};
|
||||
printf(" %10s | %7s | %7s | %7s\n", "MiB", "pinned", "pagin.", "xfer us");
|
||||
printf(" %s\n", "-------------------------------------------");
|
||||
for (int i = 0; i < 6; i++) {
|
||||
size_t b = sz[i];
|
||||
int iters = (b >= (1UL<<30)) ? 10 : (b >= (256UL<<20)) ? 20 : 100;
|
||||
double p = one(b, true, iters);
|
||||
double g = one(b, false, iters);
|
||||
// single-transfer latency
|
||||
void *dv, *hv;
|
||||
if (hipMalloc(&dv, b) == hipSuccess && hipHostMalloc(&hv, b, hipHostMallocDefault) == hipSuccess) {
|
||||
memset(hv, 0x5a, b);
|
||||
hipEvent_t a, c; hipEventCreate(&a); hipEventCreate(&c);
|
||||
hipMemcpy(dv, hv, b, hipMemcpyHostToDevice); hipDeviceSynchronize();
|
||||
hipEventRecord(a, 0);
|
||||
hipMemcpy(dv, hv, b, hipMemcpyHostToDevice);
|
||||
hipEventRecord(c, 0); hipEventSynchronize(c);
|
||||
float ms = 0; hipEventElapsedTime(&ms, a, c);
|
||||
printf(" %10.3f | %7.2f | %7.2f | %7.1f\n", b/1048576.0, p, g, ms*1000.0);
|
||||
hipHostFree(hv); hipFree(dv);
|
||||
} else {
|
||||
printf(" %10.3f | %7.2f | %7.2f | (alloc failed)\n", b/1048576.0, p, g);
|
||||
}
|
||||
}
|
||||
printf("\n 2.344 MiB is the engine's exact expert slab size.\n");
|
||||
printf(" Gen5 x8 ceiling = 31.5 GB/s per direction.\n");
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
// HIP adaptation of the pcie-link-capability-probe.
|
||||
// Question: is the decode bottleneck the LINK, or the engine's read PATH?
|
||||
// Measure H2D at the engine's actual expert slab size (2.344 MiB) and across
|
||||
// a size sweep. If 2.344 MiB copies reach ~31 GB/s, the link is healthy and
|
||||
// the engine is leaving bandwidth on the table. If they stall near 10 GB/s,
|
||||
// the link is the wall.
|
||||
//
|
||||
// build: hipcc -O2 -o pcie_probe pcie_probe.hip (scripts/pcie-probe.sh does this and guards the run)
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <cstdlib>
|
||||
#include <hip/hip_runtime.h>
|
||||
|
||||
#define CK(x) do { hipError_t e = (x); if (e != hipSuccess) { \
|
||||
fprintf(stderr, "HIP %s (%s:%d)\n", hipGetErrorString(e), __FILE__, __LINE__); \
|
||||
return 1; } } while (0)
|
||||
|
||||
static double ms_between(hipEvent_t a, hipEvent_t b) {
|
||||
float ms = 0; hipEventElapsedTime(&ms, a, b); return (double)ms;
|
||||
}
|
||||
|
||||
static int bench_size(size_t bytes) {
|
||||
const int iters = bytes >= (1UL<<30) ? 20
|
||||
: bytes >= (256UL<<20) ? 30
|
||||
: bytes >= (16UL<<20) ? 60 : 150;
|
||||
void *dev = NULL, *host = NULL, *pageable = NULL;
|
||||
hipStream_t s = NULL;
|
||||
CK(hipMalloc(&dev, bytes));
|
||||
CK(hipHostMalloc(&host, bytes, hipHostMallocDefault)); // PINNED, like the engine's slabs
|
||||
memset(host, 0x5a, bytes);
|
||||
CK(hipStreamCreate(&s));
|
||||
hipEvent_t a, b; CK(hipEventCreate(&a)); CK(hipEventCreate(&b));
|
||||
|
||||
for (int i = 0; i < 3; i++) CK(hipMemcpy(dev, host, bytes, hipMemcpyHostToDevice));
|
||||
CK(hipDeviceSynchronize());
|
||||
|
||||
// sync copy
|
||||
CK(hipEventRecord(a, 0));
|
||||
for (int i = 0; i < iters; i++) CK(hipMemcpy(dev, host, bytes, hipMemcpyHostToDevice));
|
||||
CK(hipEventRecord(b, 0)); CK(hipEventSynchronize(b));
|
||||
double h2d = (double)bytes * iters / (ms_between(a,b)/1000.0) / 1e9;
|
||||
|
||||
// async copy on a stream (the engine's fill pipeline shape)
|
||||
CK(hipEventRecord(a, 0));
|
||||
for (int i = 0; i < iters; i++) CK(hipMemcpyAsync(dev, host, bytes, hipMemcpyHostToDevice, s));
|
||||
CK(hipEventRecord(b, 0)); CK(hipEventSynchronize(b));
|
||||
double h2da = (double)bytes * iters / (ms_between(a,b)/1000.0) / 1e9;
|
||||
|
||||
// single-transfer latency at this size
|
||||
CK(hipMemcpy(dev, host, bytes, hipMemcpyHostToDevice));
|
||||
CK(hipDeviceSynchronize());
|
||||
CK(hipEventRecord(a, 0));
|
||||
CK(hipMemcpy(dev, host, bytes, hipMemcpyHostToDevice));
|
||||
CK(hipEventRecord(b, 0)); CK(hipEventSynchronize(b));
|
||||
double one_us = ms_between(a,b) * 1000.0;
|
||||
|
||||
// pageable, to see whether pinning is load-bearing here
|
||||
pageable = malloc(bytes); memset(pageable, 0x5a, bytes);
|
||||
CK(hipMemcpy(dev, pageable, bytes, hipMemcpyHostToDevice));
|
||||
CK(hipEventRecord(a, 0));
|
||||
for (int i = 0; i < iters; i++) CK(hipMemcpy(dev, pageable, bytes, hipMemcpyHostToDevice));
|
||||
CK(hipEventRecord(b, 0)); CK(hipEventSynchronize(b));
|
||||
double pag = (double)bytes * iters / (ms_between(a,b)/1000.0) / 1e9;
|
||||
|
||||
printf(" %8.3f MiB | %6.2f | %6.2f | %8.1f us | %6.2f\n",
|
||||
bytes/1048576.0, h2d, h2da, one_us, pag);
|
||||
|
||||
free(pageable); hipHostFree(host); hipFree(dev);
|
||||
hipEventDestroy(a); hipEventDestroy(b); hipStreamDestroy(s);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int main(int argc, char **argv) {
|
||||
int dev_id = 0;
|
||||
if (argc > 1) dev_id = atoi(argv[1]);
|
||||
// No hipGetDeviceProperties here: it segfaults on this ROCm build while hipGetDeviceCount is
|
||||
// fine, and the link topology these numbers must be read against comes from sysfs, which
|
||||
// scripts/pcie-probe.sh prints. Only the device index is needed to pick what to measure.
|
||||
printf("device %d (PCI bus from sysfs, see the topology block above)\n", dev_id);
|
||||
printf(" size | H2D | async | 1x xfer | pageable [GB/s]\n");
|
||||
printf(" %s\n", "-----------------------------------------------------------");
|
||||
size_t sz[] = {2459136UL, /* 2.344 MiB: the engine's exact expert slab */
|
||||
3UL<<20, 16UL<<20, 64UL<<20, 256UL<<20, 1UL<<30, 2UL<<30};
|
||||
for (int i = 0; i < 7; i++) if (bench_size(sz[i])) return 1;
|
||||
printf("\nGen5 x8 ceiling = 31.5 GB/s per direction\n");
|
||||
return 0;
|
||||
}
|
||||
Executable
+115
@@ -0,0 +1,115 @@
|
||||
#!/usr/bin/env bash
|
||||
# scripts/remote-job.sh — a detached job whose result outlives the session that started it
|
||||
#
|
||||
# Why this exists rather than `ssh host 'cmd | tail -f | sed -n /ready/q'`, which lied about success
|
||||
# repeatedly, for three structural reasons — all of them the pipe's fault: the broker aborts
|
||||
# long-lived ssh; a readiness pattern can match a log *header* before the work has begun; and
|
||||
# `sed -n /x/q` exits the instant it matches, so the supervisor reports green on a job that never
|
||||
# finished.
|
||||
#
|
||||
# Here the job runs detached on the box it is invoked on (over ssh, or locally). It writes NAME.out,
|
||||
# then NAME.rc, then touches NAME.done LAST. Status is then a cheap read of a directory: no
|
||||
# long-lived connection to lose, and the answer survives the session, the terminal, and the agent —
|
||||
# including the case where the job restarts the very service the agent is running on.
|
||||
#
|
||||
# remote-job.sh start NAME [--] CMD... launch detached, return at once
|
||||
# remote-job.sh status [NAME] one line per job
|
||||
# remote-job.sh wait NAME [SECONDS] [N] block until DONE or timeout, then tail N lines
|
||||
# remote-job.sh out NAME [LINES] print the tail of a job's output
|
||||
# remote-job.sh tail [NAME] follow one job, or every job at once
|
||||
# remote-job.sh watch refreshing status, for a spare pane
|
||||
#
|
||||
# Job directory: $REMOTE_JOBS_DIR, default $HOME/jobs. Ship this file to the host and run it there;
|
||||
# nothing about it is box-specific, which is the point.
|
||||
#
|
||||
# Exit status: 0 when every job reported is finished · 1 when something is unfinished or a wait
|
||||
# timed out · 2 usage. A job that FAILED still counts as finished for `status`: "is it done" and
|
||||
# "did it work" are different questions, and the second one is the rc= column.
|
||||
|
||||
set -uo pipefail
|
||||
|
||||
JOBS="${REMOTE_JOBS_DIR:-$HOME/jobs}"
|
||||
mkdir -p "$JOBS"
|
||||
|
||||
case "${1:-}" in
|
||||
start)
|
||||
[[ $# -ge 2 ]] || { echo "usage: remote-job.sh start NAME [--] CMD..." >&2; exit 2; }
|
||||
name="$2"; shift 2
|
||||
[[ "${1:-}" == "--" ]] && shift
|
||||
[[ $# -ge 1 ]] || { echo "remote-job: start needs a command for '$name'" >&2; exit 2; }
|
||||
# Clear the previous run entirely: a stale .out would make the new run's line count a lie.
|
||||
rm -f "$JOBS/$name.out" "$JOBS/$name.rc" "$JOBS/$name.done"
|
||||
# setsid leaves the invoking process group, so a dropped ssh or a killed agent takes nothing
|
||||
# down with it. rc lands BEFORE the sentinel: a waiter must never see DONE with no exit code.
|
||||
JOBS="$JOBS" setsid nohup bash -c '
|
||||
name="$1"; shift
|
||||
"$@" > "$JOBS/$name.out" 2>&1
|
||||
echo $? > "$JOBS/$name.rc"
|
||||
touch "$JOBS/$name.done"
|
||||
' _ "$name" "$@" < /dev/null > /dev/null 2>&1 &
|
||||
printf 'started %s\n' "$name"
|
||||
;;
|
||||
|
||||
status)
|
||||
if [[ -n "${2:-}" ]]; then
|
||||
names="$2"
|
||||
else
|
||||
names=$(cd "$JOBS" && ls *.out 2>/dev/null | sed 's/\.out$//' | sort)
|
||||
fi
|
||||
pending=0
|
||||
for n in $names; do
|
||||
if [[ -f "$JOBS/$n.done" ]]; then
|
||||
printf ' %-20s DONE rc=%s %s lines\n' "$n" \
|
||||
"$(cat "$JOBS/$n.rc" 2>/dev/null)" "$(wc -l < "$JOBS/$n.out" 2>/dev/null | tr -d ' ')"
|
||||
elif [[ -f "$JOBS/$n.out" ]]; then
|
||||
printf ' %-20s RUN %s lines so far\n' "$n" "$(wc -l < "$JOBS/$n.out" 2>/dev/null | tr -d ' ')"
|
||||
pending=1
|
||||
else
|
||||
printf ' %-20s PENDING (no output yet)\n' "$n"
|
||||
pending=1
|
||||
fi
|
||||
done
|
||||
if [[ -z "$names" ]]; then echo " (no jobs)"; exit 0; fi
|
||||
exit "$pending"
|
||||
;;
|
||||
|
||||
wait)
|
||||
[[ $# -ge 2 ]] || { echo "usage: remote-job.sh wait NAME [SECONDS] [TAIL]" >&2; exit 2; }
|
||||
name="$2"; limit="${3:-3600}"; lines="${4:-25}"; waited=0
|
||||
while [[ ! -e "$JOBS/$name.done" ]] && (( waited < limit )); do
|
||||
sleep 1; waited=$((waited + 1))
|
||||
done
|
||||
if [[ -e "$JOBS/$name.done" ]]; then
|
||||
printf 'DONE %s rc=%s\n' "$name" "$(cat "$JOBS/$name.rc" 2>/dev/null)"; rc=0
|
||||
else
|
||||
printf 'TIMEOUT %s after %ss\n' "$name" "$waited"; rc=1
|
||||
fi
|
||||
tail -n "$lines" "$JOBS/$name.out" 2>/dev/null
|
||||
exit "$rc"
|
||||
;;
|
||||
|
||||
out)
|
||||
[[ $# -ge 2 ]] || { echo "usage: remote-job.sh out NAME [LINES]" >&2; exit 2; }
|
||||
tail -n "${3:-40}" "$JOBS/$2.out" 2>/dev/null
|
||||
;;
|
||||
|
||||
tail)
|
||||
# Multiplexed follow: no NAME means every job at once, so one pane shows the whole harness.
|
||||
if [[ -n "${2:-}" ]]; then exec tail -f "$JOBS/$2.out"; fi
|
||||
exec tail -f "$JOBS"/*.out
|
||||
;;
|
||||
|
||||
watch)
|
||||
while true; do
|
||||
clear 2>/dev/null || true
|
||||
date '+%H:%M:%S'
|
||||
"$0" status
|
||||
sleep 5
|
||||
done
|
||||
;;
|
||||
|
||||
*)
|
||||
echo "usage: remote-job.sh {start|status|wait|out|tail|watch} ..." >&2
|
||||
exit 2
|
||||
;;
|
||||
esac
|
||||
Executable
+154
@@ -0,0 +1,154 @@
|
||||
#!/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
|
||||
|
||||
# systemctl stub: records every call and answers is-enabled from a file. The real
|
||||
# systemctl must never run here — on this box it would start a screen lock.
|
||||
mkdir -p "$TEST_TMP/bin"
|
||||
export STUB_LOG="$TEST_TMP/systemctl.log" UNIT_STATE="$TEST_TMP/unit-state"
|
||||
: > "$STUB_LOG"
|
||||
cat > "$TEST_TMP/bin/systemctl" <<'STUB'
|
||||
#!/usr/bin/env bash
|
||||
echo "systemctl $*" >> "$STUB_LOG"
|
||||
if [ "$2" = is-enabled ]; then
|
||||
state="$(cat "$UNIT_STATE" 2>/dev/null || echo disabled)"
|
||||
printf '%s\n' "$state"
|
||||
case "$state" in disabled | "") exit 1 ;; *) exit 0 ;; esac
|
||||
fi
|
||||
exit 0
|
||||
STUB
|
||||
chmod +x "$TEST_TMP/bin/systemctl"
|
||||
export PATH="$TEST_TMP/bin:$PATH"
|
||||
}
|
||||
|
||||
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"; }
|
||||
|
||||
# systemd_unit <name> <ExecStart> — a unit in the systemd-user package
|
||||
systemd_unit() {
|
||||
mkdir -p "$REPO/dotfiles/systemd-user/.config/systemd/user"
|
||||
printf '[Unit]\nDescription=%s\n[Service]\nExecStart=%s\n[Install]\nWantedBy=default.target\n' "$1" "$2" \
|
||||
> "$REPO/dotfiles/systemd-user/.config/systemd/user/$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" ]
|
||||
}
|
||||
|
||||
@test "a unit already enabled is enabled but not started" {
|
||||
systemd_unit lock.service /bin/sh
|
||||
printf 'enabled\n' > "$UNIT_STATE"
|
||||
run "$REPO/scripts/deploy.sh"
|
||||
[ "$status" -eq 0 ]
|
||||
[[ "$output" == *"not started"* ]]
|
||||
grep -q 'systemctl --user enable lock.service' "$STUB_LOG"
|
||||
! grep -q -- '--now' "$STUB_LOG"
|
||||
}
|
||||
|
||||
@test "a linked unit is not started either" {
|
||||
# `is-enabled` exits 1 for a linked unit, exactly as it does for a disabled one, so the
|
||||
# printed state is the signal — exit status cannot tell them apart.
|
||||
systemd_unit timer-driven.service /bin/sh
|
||||
printf 'linked\n' > "$UNIT_STATE"
|
||||
run "$REPO/scripts/deploy.sh"
|
||||
[ "$status" -eq 0 ]
|
||||
! grep -q -- '--now' "$STUB_LOG"
|
||||
}
|
||||
|
||||
@test "a disabled unit is enabled and started, so a fresh box still comes up" {
|
||||
systemd_unit fresh.service /bin/sh
|
||||
printf 'disabled\n' > "$UNIT_STATE"
|
||||
run "$REPO/scripts/deploy.sh"
|
||||
[ "$status" -eq 0 ]
|
||||
grep -q 'systemctl --user enable --now fresh.service' "$STUB_LOG"
|
||||
}
|
||||
|
||||
@test "a unit whose ExecStart is absent here stays inert" {
|
||||
systemd_unit otherbox.service /nonexistent/daemon
|
||||
printf 'disabled\n' > "$UNIT_STATE"
|
||||
run "$REPO/scripts/deploy.sh"
|
||||
[ "$status" -eq 0 ]
|
||||
[[ "$output" == *"SKIP"*otherbox.service* ]]
|
||||
! grep -q enable "$STUB_LOG"
|
||||
}
|
||||
|
||||
@test "a unit whose ExecStart uses a systemd specifier still resolves here" {
|
||||
# `%h` is expanded by systemd, not by the shell, so matching the literal string makes a
|
||||
# unit belonging to THIS box look foreign and skip it in silence. The victim is this repo's
|
||||
# own heartbeat: `ExecStart=%h/.omp/scripts/live-sync.sh`, skipped on every box. Refs #44.
|
||||
mkdir -p "$HOME/.local/bin"
|
||||
printf '#!/bin/sh\ntrue\n' > "$HOME/.local/bin/helper"
|
||||
chmod +x "$HOME/.local/bin/helper"
|
||||
systemd_unit heartbeat.service '%h/.local/bin/helper'
|
||||
printf 'linked\n' > "$UNIT_STATE"
|
||||
run "$REPO/scripts/deploy.sh"
|
||||
[ "$status" -eq 0 ]
|
||||
[[ "$output" == *"heartbeat.service (already linked"* ]]
|
||||
grep -q 'systemctl --user enable heartbeat.service' "$STUB_LOG"
|
||||
! grep -q -- '--now' "$STUB_LOG"
|
||||
}
|
||||
Executable
+144
@@ -0,0 +1,144 @@
|
||||
#!/usr/bin/env bats
|
||||
# tests/pcie-probe.bats — black-box tests for scripts/pcie-probe.sh (Refs #53)
|
||||
#
|
||||
# Contract: measure host→device bandwidth on an RDNA4 box, and refuse to measure while the GPU is
|
||||
# in use. Fleet law: never benchmark beside a live service — so the guard runs BEFORE anything is
|
||||
# built or run, and the tool stops nothing and starts nothing (auto-stopping a production service is
|
||||
# the thing it must not do unasked).
|
||||
#
|
||||
# Exit status: 0 measured · 2 usage · 3 refused (GPU busy) · 4 no toolchain / build failed.
|
||||
#
|
||||
# hipcc and the compiled probes are stubbed: this box has no HIP SDK (it is a client laptop) and a
|
||||
# real measurement belongs on a machine with the cards, beside no running model.
|
||||
|
||||
setup() {
|
||||
TEST_TMP="$(mktemp -d)"
|
||||
SCRIPT="$BATS_TEST_DIRNAME/../scripts/pcie-probe.sh"
|
||||
SRC="$TEST_TMP/src"
|
||||
BUILD="$TEST_TMP/build"
|
||||
SYSFS="$TEST_TMP/drm"
|
||||
BIN="$TEST_TMP/bin"
|
||||
mkdir -p "$SRC" "$BUILD" "$BIN" "$SYSFS/card0/device" "$SYSFS/card1/device"
|
||||
|
||||
# Fixtures stand in for the real sysfs files the driver reads.
|
||||
printf '8\n' > "$SYSFS/card0/device/current_link_width"
|
||||
printf '32.0 GT/s PCIe\n' > "$SYSFS/card0/device/current_link_speed"
|
||||
printf '16\n' > "$SYSFS/card0/device/max_link_width"
|
||||
printf '32.0 GT/s PCIe\n' > "$SYSFS/card0/device/max_link_speed"
|
||||
printf '0\n' > "$SYSFS/card0/device/gpu_busy_percent"
|
||||
printf '8\n' > "$SYSFS/card1/device/current_link_width"
|
||||
printf '32.0 GT/s PCIe\n' > "$SYSFS/card1/device/current_link_speed"
|
||||
printf '16\n' > "$SYSFS/card1/device/max_link_width"
|
||||
printf '32.0 GT/s PCIe\n' > "$SYSFS/card1/device/max_link_speed"
|
||||
printf '0\n' > "$SYSFS/card1/device/gpu_busy_percent"
|
||||
|
||||
# Probe sources: the real .hip files are shipped by the script's own directory, so the fixtures
|
||||
# only need to exist for the build step to be invoked.
|
||||
: > "$SRC/pcie_probe.hip"; : > "$SRC/pcie_bw.hip"; : > "$SRC/hip_smoke.hip"
|
||||
|
||||
# Stub toolchain: records that it was called, and emits a probe binary that records that IT ran.
|
||||
cat > "$BIN/hipcc" <<'STUB'
|
||||
#!/usr/bin/env bash
|
||||
[ "${STUB_HIPCC_FAILS:-0}" = 1 ] && { echo "stub: no matching GPU target" >&2; exit 1; }
|
||||
out=""
|
||||
while [ $# -gt 0 ]; do [ "$1" = "-o" ] && out="$2"; shift; done
|
||||
[ -n "$out" ] || exit 2
|
||||
: > "$PCIE_PROBE_BUILD/.built"
|
||||
{ echo '#!/usr/bin/env bash'
|
||||
echo ': > "$PCIE_PROBE_BUILD/.ran"'
|
||||
echo 'echo "PROBE RAN dev=${HIP_VISIBLE_DEVICES:-none} bin=$(basename "$0")"'
|
||||
echo 'echo " slab 2.344 MiB: 31.4 GB/s"'
|
||||
} > "$out"
|
||||
chmod +x "$out"
|
||||
STUB
|
||||
cat > "$BIN/fuser" <<'STUB'
|
||||
#!/usr/bin/env bash
|
||||
[ -n "${STUB_KFD_PIDS:-}" ] && printf '%s\n' $STUB_KFD_PIDS
|
||||
exit 0
|
||||
STUB
|
||||
chmod +x "$BIN/hipcc" "$BIN/fuser"
|
||||
|
||||
export PATH="$BIN:$PATH" PCIE_PROBE_SRC="$SRC" PCIE_PROBE_BUILD="$BUILD" \
|
||||
PCIE_PROBE_SYSFS="$SYSFS" PCIE_PROBE_KFD="$TEST_TMP/kfd" PCIE_PROBE_HIPCC="$BIN/hipcc"
|
||||
: > "$TEST_TMP/kfd"
|
||||
}
|
||||
|
||||
teardown() { rm -rf "$TEST_TMP"; }
|
||||
|
||||
@test "a missing HIP toolchain is a loud failure, not a silent skip" {
|
||||
run env PCIE_PROBE_HIPCC="$TEST_TMP/nope-hipcc" "$SCRIPT"
|
||||
[ "$status" -eq 4 ]
|
||||
[[ "$output" == *"hip"* ]]
|
||||
[ ! -e "$BUILD/.built" ]
|
||||
}
|
||||
|
||||
@test "a failed build is reported as a failure" {
|
||||
run env STUB_HIPCC_FAILS=1 "$SCRIPT"
|
||||
[ "$status" -eq 4 ]
|
||||
[[ "$output" == *"build"* ]]
|
||||
}
|
||||
|
||||
@test "a GPU held by another process refuses the measurement before anything is built" {
|
||||
# Order is the contract: build-and-then-ask would already have loaded the card beside a live
|
||||
# service. The .built marker proves the guard ran first.
|
||||
run env STUB_KFD_PIDS="4242" "$SCRIPT"
|
||||
[ "$status" -eq 3 ]
|
||||
[[ "$output" == *"busy"* ]]
|
||||
[ ! -e "$BUILD/.built" ]
|
||||
}
|
||||
|
||||
@test "a busy GPU percent refuses too" {
|
||||
printf '91\n' > "$SYSFS/card0/device/gpu_busy_percent"
|
||||
run "$SCRIPT"
|
||||
[ "$status" -eq 3 ]
|
||||
[[ "$output" == *"card0"* ]]
|
||||
[ ! -e "$BUILD/.built" ]
|
||||
}
|
||||
|
||||
@test "--force overrides the guard for a deliberately contended measurement" {
|
||||
run env STUB_KFD_PIDS="4242" "$SCRIPT" --force
|
||||
[ "$status" -eq 0 ]
|
||||
[ -e "$BUILD/.ran" ]
|
||||
}
|
||||
|
||||
@test "an idle GPU builds and runs the probe" {
|
||||
run "$SCRIPT"
|
||||
[ "$status" -eq 0 ]
|
||||
[ -e "$BUILD/.built" ]
|
||||
[ -e "$BUILD/.ran" ]
|
||||
[[ "$output" == *"PROBE RAN"* ]]
|
||||
[[ "$output" == *"31.4 GB/s"* ]] # probe output passes through untouched
|
||||
}
|
||||
|
||||
@test "--build-only compiles without touching the card" {
|
||||
run "$SCRIPT" --build-only
|
||||
[ "$status" -eq 0 ]
|
||||
[ -e "$BUILD/.built" ]
|
||||
[ ! -e "$BUILD/.ran" ]
|
||||
}
|
||||
|
||||
@test "the link topology is reported from sysfs, current against max" {
|
||||
run "$SCRIPT"
|
||||
[[ "$output" == *"card0"*"x8"*"32.0 GT/s"* ]]
|
||||
[[ "$output" == *"max x16"* ]]
|
||||
}
|
||||
|
||||
@test "each requested device is measured separately" {
|
||||
# One shared run cannot attribute bandwidth to a card; the whole point is per-card numbers.
|
||||
run "$SCRIPT" --device 0 --device 1
|
||||
[ "$status" -eq 0 ]
|
||||
[[ "$output" == *"dev=0"* ]]
|
||||
[[ "$output" == *"dev=1"* ]]
|
||||
}
|
||||
|
||||
@test "no probe sources is a usage-level error, not an empty success" {
|
||||
rm -f "$SRC"/*.hip
|
||||
run "$SCRIPT"
|
||||
[ "$status" -ne 0 ]
|
||||
[[ "$output" == *"probe"* ]]
|
||||
}
|
||||
|
||||
@test "an unknown flag is a usage error" {
|
||||
run "$SCRIPT" --frobnicate
|
||||
[ "$status" -eq 2 ]
|
||||
}
|
||||
Executable
+223
@@ -0,0 +1,223 @@
|
||||
#!/usr/bin/env bats
|
||||
# tests/pkg-inventory.bats — black-box tests for scripts/pkg-inventory.sh (Refs #51)
|
||||
#
|
||||
# Contract: `install.sh` installs desired-minus-installed from packages/*.pkgs. This tool walks
|
||||
# the other direction — installed-minus-declared — because a manifest the machine has quietly
|
||||
# outgrown cannot reproduce the machine on a rebuild. Only stdout, stderr and the exit status are
|
||||
# observed: 0 no drift, 1 drift (hand-installed packages undeclared), 2 usage, 3 sync databases
|
||||
# unavailable so repo/AUR classification is impossible.
|
||||
#
|
||||
# Every pacman/flatpak call hits a stub: nothing here may query the real package database, and a
|
||||
# real pacman on PATH would make the fixture answers depend on the workstation.
|
||||
|
||||
setup() {
|
||||
TEST_TMP="$(mktemp -d)"
|
||||
SCRIPT="$BATS_TEST_DIRNAME/../scripts/pkg-inventory.sh"
|
||||
REPO="$TEST_TMP/repo"
|
||||
mkdir -p "$REPO/packages" "$TEST_TMP/bin"
|
||||
|
||||
# --- manifests: common + host(box) + aur ------------------------------------
|
||||
cat > "$REPO/packages/common.pkgs" <<'EOF'
|
||||
# common desired state
|
||||
ghostty
|
||||
zoxide
|
||||
vlc # declared, deliberately NOT installed below
|
||||
EOF
|
||||
cat > "$REPO/packages/box.pkgs" <<'EOF'
|
||||
neovim
|
||||
EOF
|
||||
cat > "$REPO/packages/aur.pkgs" <<'EOF'
|
||||
typora # AUR-only desired state
|
||||
EOF
|
||||
|
||||
# --- pacman log: install day 2026-01-01, hand installs from 2026-03 ----------
|
||||
cat > "$TEST_TMP/pacman.log" <<'EOF'
|
||||
[2026-01-01T10:00:00-0800] [PACMAN] Running 'pacman --sysroot /tmp/calamares-root-x -S base'
|
||||
[2026-01-01T10:00:01-0800] [ALPM] installed filesystem (2026.01.01-1)
|
||||
[2026-01-01T10:00:02-0800] [ALPM] installed base (3-1)
|
||||
[2026-01-01T10:00:03-0800] [ALPM] installed libfoo (1.0-1)
|
||||
[2026-03-04T09:00:00-0800] [ALPM] installed bun (1.2-1)
|
||||
[2026-03-04T09:30:00-0800] [ALPM] upgraded filesystem (2026.01.01-1 -> 2026.03.01-1)
|
||||
[2026-03-05T09:00:00-0800] [ALPM] installed nirimod-git (r7-1)
|
||||
[2026-03-06T09:00:00-0800] [ALPM] removed base (3-1)
|
||||
EOF
|
||||
|
||||
# --- installed state --------------------------------------------------------
|
||||
# ghostty/zoxide/neovim/typora declared. libfoo: explicit, arrived with the installer.
|
||||
# bun/nirimod-git: installed by hand afterwards, undeclared. python-hatchling: orphan.
|
||||
printf '%s\n' base bun filesystem ghostty libfoo neovim nirimod-git typora zoxide > "$TEST_TMP/local-all"
|
||||
printf '%s\n' bun ghostty libfoo neovim nirimod-git typora zoxide > "$TEST_TMP/local-explicit"
|
||||
printf '%s\n' python-hatchling > "$TEST_TMP/orphans"
|
||||
# nirimod-git is deliberately ABSENT here: no sync db can serve it, which is the fact that
|
||||
# routes it to aur.pkgs in the test below.
|
||||
printf '%s\n' bun ghostty neovim typora zoxide libfoo vlc python-hatchling base filesystem > "$TEST_TMP/sync-all"
|
||||
|
||||
# Four flag combinations are the script's whole interface to the package manager; the stub
|
||||
# answers those and refuses anything else, so an accidental new query fails loudly.
|
||||
cat > "$TEST_TMP/bin/pacman" <<'STUB'
|
||||
#!/usr/bin/env bash
|
||||
case "$1" in
|
||||
-Sl) [ "${STUB_SYNC_EMPTY:-0}" = 1 ] && exit 1
|
||||
sed 's/^/syncdb /' "$SYNC_ALL" ;;
|
||||
-Qqe) cat "$LOCAL_EXPLICIT" ;;
|
||||
-Qtdq) cat "$ORPHANS"; [ -s "$ORPHANS" ] ;;
|
||||
-Qq) cat "$LOCAL_ALL" ;;
|
||||
*) echo "stub pacman: unhandled args: $*" >&2; exit 64 ;;
|
||||
esac
|
||||
STUB
|
||||
cat > "$TEST_TMP/bin/flatpak" <<'STUB'
|
||||
#!/usr/bin/env bash
|
||||
[ "${STUB_NO_FLATPAK:-0}" = 1 ] && exit 127
|
||||
printf '%s\n' com.discordapp.Discord
|
||||
STUB
|
||||
chmod +x "$TEST_TMP/bin/pacman" "$TEST_TMP/bin/flatpak"
|
||||
|
||||
export PATH="$TEST_TMP/bin:$PATH"
|
||||
export PKGINV_REPO="$REPO" PKGINV_HOST=box PKGINV_LOG="$TEST_TMP/pacman.log"
|
||||
export LOCAL_ALL="$TEST_TMP/local-all" LOCAL_EXPLICIT="$TEST_TMP/local-explicit"
|
||||
export ORPHANS="$TEST_TMP/orphans" SYNC_ALL="$TEST_TMP/sync-all"
|
||||
}
|
||||
|
||||
teardown() { rm -rf "$TEST_TMP"; }
|
||||
|
||||
# section RE_START RE_END — the report from RE_START's line up to (not including) RE_END, so an
|
||||
# assertion proves a name landed in a specific section instead of merely appearing somewhere in the
|
||||
# report. Start inclusive (the count and the names sit on the label line), terminator exclusive (a
|
||||
# plain sed range would leak the next section's first line). Anchoring matters: an unanchored start
|
||||
# runs the range to end-of-output, silently turning every absence assertion into a vacuous pass —
|
||||
# exactly how four of these tests were passing before this helper was fixed.
|
||||
section() { printf '%s\n' "$output" | awk -v s="$1" -v e="$2" '$0 ~ e {on=0} $0 ~ s {on=1} on'; }
|
||||
ADDITIONS() { section "^undeclared additions" "^installer-provided"; }
|
||||
|
||||
@test "header names the host and takes the baseline from the oldest install line" {
|
||||
run "$SCRIPT"
|
||||
[ "$status" -eq 1 ] # the fixture has drift by design
|
||||
[[ "$output" == *"pkg-inventory host=box"* ]]
|
||||
[[ "$output" == *"baseline=2026-01-01"* ]]
|
||||
}
|
||||
|
||||
@test "hand-installed packages are reported as additions" {
|
||||
run "$SCRIPT"
|
||||
[[ "$output" == *"undeclared additions (declare these): 2"* ]]
|
||||
s="$(ADDITIONS)"
|
||||
[[ "$s" == *bun* ]]
|
||||
[[ "$s" == *nirimod-git* ]]
|
||||
}
|
||||
|
||||
@test "a package the installer provided is not reported as an addition" {
|
||||
run "$SCRIPT"
|
||||
additions="$(ADDITIONS)"
|
||||
[[ "$additions" != *libfoo* ]]
|
||||
[[ "$(section "^installer-provided" "^declared,")" == *libfoo* ]]
|
||||
# The folded block must end in a newline. Without one, the following label is glued onto the
|
||||
# last folded line and every section after it vanishes from a sliced report.
|
||||
[[ "$(section "^installer-provided" "^declared,")" != *declared,* ]]
|
||||
}
|
||||
|
||||
@test "AUR-only packages are separated from sync-repo packages" {
|
||||
run "$SCRIPT"
|
||||
# nirimod-git is missing from the sync-DB fixture, so only a helper can install it: it must be
|
||||
# routed to aur.pkgs, not to a pacman line that would fail on a fresh box. Bash `*` crosses
|
||||
# newlines, so the absence check runs on one line rather than on the whole section.
|
||||
repo_line="$(section '^ repo ' '^ aur ')"
|
||||
[[ "$repo_line" == *"repo (sync dbs): bun"* ]]
|
||||
[[ "$repo_line" != *nirimod-git* ]]
|
||||
[[ "$(section '^ aur ' '^installer-provided')" == *"aur (needs a helper): nirimod-git"* ]]
|
||||
}
|
||||
|
||||
@test "declared-but-missing packages are listed for install.sh" {
|
||||
run "$SCRIPT"
|
||||
[[ "$output" == *"declared, not installed"* ]]
|
||||
[[ "$(section "^declared," "^orphans")" == *vlc* ]]
|
||||
}
|
||||
|
||||
@test "orphans are reported as ignored and never emitted" {
|
||||
run "$SCRIPT"
|
||||
[[ "$output" == *orphans*python-hatchling* ]]
|
||||
run "$SCRIPT" --emit
|
||||
[[ "$output" != *python-hatchling* ]]
|
||||
}
|
||||
|
||||
@test "--baseline moves packages installed on that date into the installer set" {
|
||||
# Inclusive date semantics: pushing the baseline to bun's install date makes bun installer
|
||||
# furniture, while nirimod-git (the next day) is still a hand install.
|
||||
run env PKGINV_BASELINE=2026-03-04 "$SCRIPT"
|
||||
[[ "$output" == *"baseline=2026-03-04"* ]]
|
||||
s="$(ADDITIONS)"
|
||||
[[ "$s" != *bun* ]]
|
||||
[[ "$s" == *nirimod-git* ]]
|
||||
}
|
||||
|
||||
@test "--emit prints lines that survive the manifest parser" {
|
||||
run "$SCRIPT" --emit
|
||||
[ "$status" -eq 0 ]
|
||||
# install.sh reads manifests as `sed 's/#.*//'` then the first field, so an emitted name must
|
||||
# be a bare token: a stray path or trailing note would be handed to pacman as a package name.
|
||||
bad="$(printf '%s\n' "$output" | grep -v '^#' | grep -vE '^[A-Za-z0-9@._+-]+$' || true)"
|
||||
[ -z "$bad" ]
|
||||
printf '%s\n' "$output" > "$REPO/packages/emitted.pkgs"
|
||||
names="$(sed 's/#.*//' "$REPO/packages/emitted.pkgs" | awk '{if ($1) print $1}' | sort | tr '\n' ' ')"
|
||||
[[ "$names" == *bun* ]]
|
||||
[[ "$names" == *nirimod-git* ]]
|
||||
[[ "$names" != *ghostty* ]]
|
||||
[[ "$names" != *libfoo* ]]
|
||||
[[ "$names" != *vlc* ]]
|
||||
}
|
||||
|
||||
@test "an unknown host gets the common manifest only, so host packages look undeclared" {
|
||||
# Mirrors install.sh: wrong-host runs are a no-op, not a hazard. Here that means neovim,
|
||||
# declared only in box.pkgs, counts as an addition when the host is not box.
|
||||
run env PKGINV_HOST=otherbox "$SCRIPT"
|
||||
[[ "$(ADDITIONS)" == *neovim* ]]
|
||||
}
|
||||
|
||||
@test "no drift exits 0" {
|
||||
printf '%s\n' ghostty libfoo zoxide > "$TEST_TMP/local-explicit"
|
||||
run "$SCRIPT"
|
||||
[ "$status" -eq 0 ]
|
||||
[[ "$output" == *"undeclared additions (declare these): 0"* ]]
|
||||
}
|
||||
|
||||
@test "missing sync databases fail loudly rather than misclassifying everything as AUR" {
|
||||
run env STUB_SYNC_EMPTY=1 "$SCRIPT"
|
||||
[ "$status" -eq 3 ]
|
||||
[[ "$output" == *"sync"* ]]
|
||||
}
|
||||
|
||||
@test "flatpak apps are reported separately from pacman packages" {
|
||||
run "$SCRIPT"
|
||||
[[ "$output" == *"flatpak"*com.discordapp.Discord* ]]
|
||||
}
|
||||
|
||||
@test "a box without flatpak still reports the pacman inventory" {
|
||||
run env STUB_NO_FLATPAK=1 "$SCRIPT"
|
||||
[ "$status" -eq 1 ]
|
||||
[[ "$output" == *"flatpak"* ]]
|
||||
[[ "$(ADDITIONS)" == *bun* ]]
|
||||
}
|
||||
|
||||
@test "the baseline is found in a rotated log when the install day has aged out of the live one" {
|
||||
# pacman rotates its log; without reading the rotated file the whole base system would be
|
||||
# reported as hand-installed additions on any machine older than the rotation window.
|
||||
mv "$TEST_TMP/pacman.log" "$TEST_TMP/pacman.log.1"
|
||||
printf '[2026-10-01T09:00:00-0700] [ALPM] upgraded zoxide (0.9-1 -> 0.10-1)\n' > "$TEST_TMP/pacman.log"
|
||||
run env PKGINV_LOG="$TEST_TMP/pacman.log $TEST_TMP/pacman.log.1" "$SCRIPT"
|
||||
[[ "$output" == *"baseline=2026-01-01"* ]]
|
||||
[[ "$(ADDITIONS)" != *libfoo* ]]
|
||||
}
|
||||
|
||||
@test "a log larger than the pipe buffer still yields the baseline" {
|
||||
# A real install day writes thousands of install lines. `sort | head -1` under pipefail dies of
|
||||
# SIGPIPE once that output exceeds the pipe buffer, and no small fixture can show it — the first
|
||||
# run against a real log is where it surfaced. seq, not yes: yes would SIGPIPE in the fixture.
|
||||
seq 1 20000 | awk '{print "[2025-05-05T10:00:00-0700] [ALPM] installed pkgfiller (1-1)"}' > "$TEST_TMP/big.log"
|
||||
cat "$TEST_TMP/big.log" "$TEST_TMP/pacman.log" > "$TEST_TMP/combined.log"
|
||||
run env PKGINV_LOG="$TEST_TMP/combined.log" "$SCRIPT"
|
||||
[ "$status" -eq 1 ]
|
||||
[[ "$output" == *"baseline=2025-05-05"* ]]
|
||||
}
|
||||
|
||||
@test "usage is a distinct exit status from drift" {
|
||||
run "$SCRIPT" --nope
|
||||
[ "$status" -eq 2 ]
|
||||
}
|
||||
Executable
+135
@@ -0,0 +1,135 @@
|
||||
#!/usr/bin/env bats
|
||||
# tests/remote-job.bats — black-box tests for scripts/remote-job.sh (Refs #53)
|
||||
#
|
||||
# Contract: a job runs detached, writes its output, then its exit code, then touches .done LAST.
|
||||
# Status is then a cheap independent read that survives the session that started the job — the
|
||||
# whole point is replacing `ssh host 'tail -f | sed'`, which reported success on jobs that had not
|
||||
# finished. Exit status answers "is everything finished"; the rc= field answers "did it work".
|
||||
#
|
||||
# Jobs really run here, against a temp job dir. No fake filesystem state is pre-seeded, because a
|
||||
# fixture that pre-creates .done proves nothing about the code that is supposed to create it.
|
||||
|
||||
setup() {
|
||||
TEST_TMP="$(mktemp -d)"
|
||||
SCRIPT="$BATS_TEST_DIRNAME/../scripts/remote-job.sh"
|
||||
export REMOTE_JOBS_DIR="$TEST_TMP/jobs"
|
||||
}
|
||||
|
||||
teardown() {
|
||||
# Jobs are detached on purpose; leave nothing behind that could outlive the test run.
|
||||
pkill -P 1 -f "$TEST_TMP" 2>/dev/null || true
|
||||
rm -rf "$TEST_TMP"
|
||||
}
|
||||
|
||||
# until_done NAME SECONDS — poll instead of sleeping a fixed amount, so the suite is neither slow
|
||||
# nor flaky on a loaded machine.
|
||||
until_done() {
|
||||
local name="$1" limit="${2:-10}" waited=0
|
||||
while [[ ! -e "$REMOTE_JOBS_DIR/$name.done" && $waited -lt $((limit * 10)) ]]; do
|
||||
sleep 0.1; waited=$((waited + 1))
|
||||
done
|
||||
[[ -e "$REMOTE_JOBS_DIR/$name.done" ]]
|
||||
}
|
||||
|
||||
@test "start returns immediately while the job is still running" {
|
||||
run timeout 10 "$SCRIPT" start slow -- sleep 2
|
||||
[ "$status" -eq 0 ]
|
||||
[[ "$output" == "started slow" ]]
|
||||
# A 2-second job must not have blocked a start that is supposed to return at once, and while it
|
||||
# is still in flight the report must say unfinished — never DONE.
|
||||
run "$SCRIPT" status slow
|
||||
[[ "$output" == *RUN* || "$output" == *PENDING* ]]
|
||||
[ "$status" -eq 1 ]
|
||||
}
|
||||
|
||||
@test "a successful job is reported DONE with its output" {
|
||||
"$SCRIPT" start ok -- bash -c 'echo marker-4711'
|
||||
until_done ok
|
||||
run "$SCRIPT" status ok
|
||||
[[ "$output" == *"ok"*DONE*rc=0* ]]
|
||||
[ "$status" -eq 0 ]
|
||||
run "$SCRIPT" out ok
|
||||
[[ "$output" == *marker-4711* ]]
|
||||
}
|
||||
|
||||
@test "a failing job's exit code survives into the report" {
|
||||
# This is the reason the .rc file exists: a supervisor that cannot see the failure code is the
|
||||
# bug the runner was written to fix.
|
||||
"$SCRIPT" start boom -- bash -c 'echo it failed here >&2; exit 7'
|
||||
until_done boom
|
||||
run "$SCRIPT" status boom
|
||||
[[ "$output" == *"boom"*DONE*rc=7* ]]
|
||||
run "$SCRIPT" out boom
|
||||
[[ "$output" == *"it failed here"* ]]
|
||||
}
|
||||
|
||||
@test "the .done sentinel is never written before the exit code" {
|
||||
# If .done landed first, a waiter could read an empty rc and call the job successful.
|
||||
"$SCRIPT" start order -- bash -c 'echo x'
|
||||
until_done order
|
||||
[ -f "$REMOTE_JOBS_DIR/order.rc" ]
|
||||
[ -s "$REMOTE_JOBS_DIR/order.rc" ]
|
||||
run cat "$REMOTE_JOBS_DIR/order.rc"
|
||||
[[ "$output" =~ ^[0-9]+$ ]]
|
||||
}
|
||||
|
||||
@test "wait prints DONE with the exit code and a tail of the output" {
|
||||
"$SCRIPT" start fin -- bash -c 'echo one; echo two'
|
||||
run "$SCRIPT" wait fin 15
|
||||
[ "$status" -eq 0 ]
|
||||
[[ "$output" == *"DONE fin rc=0"* ]]
|
||||
[[ "$output" == *two* ]]
|
||||
}
|
||||
|
||||
@test "wait gives up on its own and says so" {
|
||||
"$SCRIPT" start never -- sleep 30
|
||||
run "$SCRIPT" wait never 1
|
||||
[ "$status" -eq 1 ]
|
||||
[[ "$output" == *TIMEOUT*never* ]]
|
||||
"$SCRIPT" status never | grep -q . # the job is still there, unfinished
|
||||
}
|
||||
|
||||
@test "status distinguishes running from pending from done" {
|
||||
run "$SCRIPT" status
|
||||
[[ "$output" == *"(no jobs)"* ]]
|
||||
[ "$status" -eq 0 ]
|
||||
|
||||
"$SCRIPT" start later -- sleep 3
|
||||
run "$SCRIPT" status later
|
||||
[[ "$output" == *later* ]]
|
||||
[[ "$output" == *RUN* || "$output" == *PENDING* ]]
|
||||
[ "$status" -eq 1 ] # unfinished: a supervisor must be able to branch on this
|
||||
}
|
||||
|
||||
@test "two jobs in flight keep separate state" {
|
||||
"$SCRIPT" start a -- bash -c 'echo A; exit 0'
|
||||
"$SCRIPT" start b -- bash -c 'echo B; exit 2'
|
||||
until_done a && until_done b
|
||||
run "$SCRIPT" status
|
||||
[[ "$output" == *"a"*DONE*rc=0* ]]
|
||||
[[ "$output" == *"b"*DONE*rc=2* ]]
|
||||
run "$SCRIPT" out a
|
||||
[[ "$output" == A ]]
|
||||
run "$SCRIPT" out b
|
||||
[[ "$output" == B ]]
|
||||
}
|
||||
|
||||
@test "out honours a line limit" {
|
||||
"$SCRIPT" start many -- bash -c 'seq 1 40'
|
||||
until_done many
|
||||
run "$SCRIPT" out many 3
|
||||
[ "$(printf '%s\n' "$output" | grep -c .)" -eq 3 ]
|
||||
[[ "$output" == *38* ]] # the tail, not the head
|
||||
}
|
||||
|
||||
@test "an unknown verb is a usage error" {
|
||||
run "$SCRIPT" frobnicate
|
||||
[ "$status" -eq 2 ]
|
||||
[[ "$output" == *usage* ]]
|
||||
}
|
||||
|
||||
@test "the job directory is created on first use" {
|
||||
[ ! -d "$REMOTE_JOBS_DIR" ]
|
||||
"$SCRIPT" status
|
||||
[ -d "$REMOTE_JOBS_DIR" ]
|
||||
}
|
||||
Executable
+159
@@ -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"* ]]
|
||||
}
|
||||
Reference in New Issue
Block a user