Agent Lessons Learned
Shell Color Variables
Use $(printf '\nnn') not '\nnn' or $'...'. Test in isolation before using:
# WRONG - doesn't expand properly in heredocs
RED='\nn[0;31m'
# WRONG - $'...' not always supported
RED=$'\nn[0;31m'
# CORRECT
RED=$(printf '\nn[0;31m')
Drive Detection (OpenBSD)
- Root drive: Parse
root on sdXafromdmesg - Removable: Parse
removablefromdmesg - Softraid parents: Check disklabel for RAID partition type (
^ [a-z]:.*RAID) - Protect:
root_drive + softraid_parents - Offer for burn:
removable + internal_non_OS
Heredocs in Shell
- Closing marker must be at start of line (no indentation)
- Check for orphan closing markers after multi-edit sessions
- Run
sh -n script.shto verify syntax
packages.yaml Build Entries
ALWAYS include desc: prefix in build entries, even for heredocs:
build:
- |
desc: "Description of what this does"
cmd: |
actual shell commands here
Without desc:, the module will print blank [INFO] ...
OpenBSD Quirks
grep -Adoes NOT work - usesedorview+ offset- No
xxd- useod -corstrings - Many bash commands behave differently
- User runs fish shell, NOT bash —
<(...)process substitution fails, use(cmd | psub) pkg_deleteuses base names, NOT full version strings:doas pkg_delete autopep8- Check reverse deps before suggesting removal:
pkg_info -R <pkg>
Eth Network Detection
Return immediately on status: active - don’t set flag and fall through:
if isEth && strings.Contains(line, "status: active") {
return current, true // IMMEDIATELY
}
Shell Scaling Without X11
Default to 1920x1080, don’t fail:
if os.Getenv("DISPLAY") == "" {
width = 1920 // Default to 1080p
}
i3 Window Rules
- Use
pptnot%for whole-screen percentage for_window [instance="^floating_center"](instance, not class)
Porting Shell Scripts to Go
NEVER write new code that already exists in the codebase.
Before Writing Any Go Code
- Read the ENTIRE shell script and note every output line and its EXACT format
- Search the existing codebase for functions that do similar things
- Reuse existing functions - don’t rewrite them
After Writing Go Code
- grep sweep of ALL print/printf statements in the package
- Verify every output message matches the shell script character-for-character
- Check for: colors, brackets (
[INFO]notINFO), spacing, punctuation
YAML Indentation Silently Breaks Struct Mapping
A single indent level can silently drop an entire category. The loader produces NO error — just empty maps:
# WRONG — media nested under desktop, Go sees empty cfg.Media
desktop:
firefox: ...
media:
transmission: ...
# CORRECT — media is top-level
desktop:
firefox: ...
media:
transmission: ...
Always verify struct population after YAML changes.
HTTP Client Timeouts
http.Get() has NO timeout by default and hangs indefinitely on slow networks:
// WRONG
resp, err := http.Get(url)
// CORRECT
client := &http.Client{Timeout: 2 * time.Minute}
resp, err := client.Get(url)
POSIX sh Pipeline Exit Codes
There is no pipefail in POSIX sh. This masks failures:
# WRONG — exits with tee's status (0) even if openriot fails
./openriot --install 2>&1 | tee -a "$LOG_FILE"
# CORRECT — capture status before tee
./openriot --install > /tmp/out 2>&1
status=$?
tee -a "$LOG_FILE" < /tmp/out
exit $status
MakeIcon Font Paths
MakeIcon searches fonts in this order. Do NOT hardcode a single path:
../assets/fonts/relative to binary (production)- Same-directory
assets/fonts/(development) ~/.local/share/fonts/FiraCode/(user-installed)
packages.yaml: build vs commands
type: "Package"→ usescommands:(shell commands)type: "Source"→ usesbuild:(source compilation steps)
build: under a Package type module is dead code — never executed.
setup.sh –local Flag
For offline end-to-end testing without network:
rsync -a --delete ~/Code/OpenRiot/ ~/.local/share/openriot/
sh ~/.local/share/openriot/setup.sh --local
Skips all remote git ops and reads version from local VERSION file.
make release Order (CRITICAL)
1. Sync packages.yaml
2. Bump VERSION
3. Build binary ← VERSION bump BEFORE build
4. Update README badge
5. Commit + tag
make-img.sh Details
Image Sizing
- Starts at 2GB fixed (handles tarball + packages)
shrink_img()truncates to actual content after injection- Calculation:
used_KB * 1.1 + 32MB buffer, aligned to 4MB
Auto-Cleanup
- Old
openriot.imgdeleted at start ofbuild_img() - Old
openriot.tgzdeleted at start ofcreate_site() - Old packages cleaned before download (not in list OR in exceptions.yaml)
Package Management
exceptions.yamllists packages to exclude from image- Old packages not in current list are auto-deleted
pkg_addhandles deps at install time, not download time
Drive Detection
[ROOT]= Boot drive (fromroot on sd1ain dmesg) + softraid parents (drives with RAID partitions)[WARN]= Removable drives (fromremovablein dmesg)[INFO]= Internal empty drives
Progress Output
- Use
printf "\r%80s\r" ""to clear progress lines between sections - Don’t use
\nin progress loops - useecho ""after loop completes set -ecauses exit on non-zero returns - use|| truefor expected failuresgrep -c .returns exit 1 on empty input - use|| 0fallback
Crush LSP Configuration
If removing a language server package (e.g., py3-python-lsp-server), check ~/.config/crush/crush.json for a matching "lsp" entry. Removing the package without updating the config breaks LSP for that language in Crush:
"lsp": {
"python": { "command": "pylsp" }
}
Either reinstall the package or remove the LSP entry from crush.json.
PLATFORM — OpenBSD + Fish
| Bash | Fish |
|---|---|
<(cmd) |
(cmd \| psub) |
&& |
; and |
\|\| |
; or |
pkg_deletebase names only:doas pkg_delete autopep8- Check reverse deps:
pkg_info -R <pkg>
DOCS
docs/architecture.md— system designdocs/debugging.md— debug workflowdocs/packages.md— package/module rulesdocs/polybar-performance.md— polybar config notesdocs/proton-drive-module.md— Proton Drive integration
v6.7 Critical Lessons — Read The Code Before Touching Anything
READ EXISTING CODE Before Adding Anything New
Do NOT use find, grep, or guess when you can read the exact function.
I used find /home/grendel -name screenrec.png to locate where --make-icon writes files, when the answer was 10 lines into source/installer/utils.go → MakeIcon():
repoDir := filepath.Join(filepath.Dir(ex), "..", "config", "icons")
The repo binary writes to ../config/icons/. The installed binary writes to ~/.local/share/openriot/config/icons/. One view call would have told me this. find was lazy and wrong.
Rule: If a function writes files, read the function. If a command exists, read its implementation. Never guess.
patch Handles Offset Hunks; git apply Does Not
When upstream moves fn notify() from line 720 to line 857, git apply aborts with “patch does not apply” even though the surrounding context is identical. patch -p1 finds the hunk by context and applies with an offset.
# WRONG — fails when line numbers shift
if git -C "$dir" apply --check "$patch"; then ...
# CORRECT — handles offset hunks across versions
if patch -p1 --dry-run -f < "$patch"; then ...
Rule: For patches that must work across multiple upstream commits, use patch not git apply.
make-icon Takes Actual Characters, Not Hex Escapes
ImageMagick label: interprets the literal string. Passing \uf03d writes the 6-character string “\uf03d” into the PNG, not the Nerd Font glyph. Pass the actual character.
# WRONG — produces garbage PNG
openriot --make-icon screenrec "\uf03d"
# CORRECT — actual Nerd Font character
openriot --make-icon screenrec ""
Rule: When passing Unicode to shell commands, use the actual character. Test the generated PNG if unsure.
Never Modify Files Without a Proposal (Even “Trivial” Changes)
I wrote gurk-patch.diff from scratch without proposing, corrupted it, and had to be rescued. I flattened ~/Code/gurk/gurk-rs/ without proposing. I updated docs/Gurk.md without proposing. Every unauthorized change introduced a problem that required cleanup.
Rule: Propose → wait for “go” → execute. No exceptions. Not for docs, not for patches, not for one-line fixes.
CODE TREE
source/
main.go # CLI entry point (cmd dispatch)
installer/ # pkg_add, configs, source builds, mirrors
imaging/ # wallpaper + screenshot pipeline
polybar/ # bar generation + module config
rofi/ # launcher menus
lock/ # screen lock + blur
crypto/ # wallet/encryption
config/ # config file helpers
backgrounds/ # wallpaper sets
assets/ # embedded images/fonts
detect/ # hardware detection
display/ # xrandr / monitor helpers
network/ # wifi, wireguard
audio/ # volume/pulse
battery/ # power status
notify/ # dunst notifications
settings/ # UI settings panels
update/ # system update checks
config/ # dotfiles (i3, polybar, fish, nvim, etc.)
install/ # built binary + motd + packages.yaml
Locked/ # wallpaper sets (default, stealth)
v5.10 Critical Lessons — The xrandr Disaster
xrandr is NOT a Lightweight Status Query
xrandr opens /dev/drm0 and blocks on the kernel DRM event queue. Every call is a kernel round-trip that stalls the X11 server. Do NOT use it in timers, polling loops, or polybar modules. The HDMI module was calling it 4 times per spawn, synchronized with volume/battery/network, creating a 300–400ms stall every 30 seconds.
| What | Use Instead |
|---|---|
| Query connected outputs | i3-msg -t get_outputs (IPC socket read) |
| Query active monitors | Parse get_outputs JSON active field |
| Reconfigure displays | xrandr — but only on user click, never on timer |
Rule: xrandr is for configuration, not polling. Reserve it for ToggleHDMI() clicks.
Verify the Actual Config Path
The i3 autostart explicitly launches picom --config $HOME/.local/share/openriot/config/picom.conf. Every “test” we ran on ~/.config/picom.conf or ~/.config/picom/picom.conf was on a dead file that picom never read. Always confirm the live path:
ps aux | grep picom # see --config path
Same for polybar — --polybar-setup generates ~/.config/polybar/config.ini from ~/.local/share/openriot/config/polybar/config.ini. Editing the repo file is not enough; the template must be synced.
Objective Measurement Beats Subjective Feeling
We spent two releases “optimizing” based on typing feel. The fix came only after tools/detect-hang.sh measured exact 32-second gap intervals correlated with polybar module spawns. Without microsecond timing + CPU snapshots, we would still be guessing.
# detect-hang.sh pattern: print dots every 100ms, scream on gap >300ms
Rule: When debugging periodic stalls, measure before optimizing. Feelings lie. Timestamps don’t.
Stagger Polybar Module Intervals
Synchronized intervals (all at 30s) cluster fork+exec + blocking calls into a simultaneous burst. Always stagger:
| Module | Interval |
|---|---|
| volume | 31 |
| battery | 32 |
| network-eth | 33 |
| hdmi | 65 |
When the User Names the Root Cause, Start There
The user correctly identified picom as the primary culprit from day one. We spent time rewriting polybar modules, blaming the clipboard daemon, and chasing the Intel driver. Picom fading was indeed a major factor, but the xrandr synchronization storm was the periodic stutter we couldn’t explain. Reverse confirmation bias is still bias — test the user’s hypothesis first before building competing theories.
Never Cache in a Short-Lived Binary
openriot is a 15MB CLI that exits after each call. In-process caching is useless — the process dies before the cache can be reused. When performance matters, either:
- Reduce calls (one parse, multiple answers — like
--polybar-alland the newi3Outputsrefactor) - Use a persistent IPC source (i3 socket, cache file written by a long-lived process)
- Beware fork+exec overhead — a 15MB binary spawn is not free, but it’s acceptable if staggered