Once the archive verifies, USB detection runs (so a stick plugged in while
the backup ran is found): mounted removable storage is auto-detected via
lsblk -J + a recursive jq filter (rm, mounted, type part|disk — JSON makes
spacey mountpoints safe), or BACKUP_USB_ROOT pins a fixed stick and skips
detection. None mounted → one re-scan prompt ('s' skips, EOF from cron
skips silently, rc stays 0); one stick → y/N confirm; several → numbered
pick (0 = skip). The copy lands in <usb>/backups/ (mkdir -p, chmod 600
best-effort — a vfat chmod failure warns, never fails the copy) and the
transfer is proven 100% by sha256 source-vs-copy before any success is
announced; a mismatch warns with both hashes, notifies 'USB copy FAILED',
and exits 1. The ERR trap is re-armed before the USB phase so a copy
failure no longer notifies 'Backup FAILED'. Docs: usage() Environment,
POS.md backup row, howto/system.md (USB section + env table + mismatch
troubleshooting), DEV.md system.env list. Stub suite
(/tmp/opencode/backup-test, HOME-isolated, sudo/gpg/lsblk/sender stubs,
corrupting-cp + vfat-chmod overrides, per-test lsblk JSON fixtures):
40/40 green.
Session-learned hardening (sole-developer project — terse, actionable):
- §7 env-override precedent list gains USER_SYSTEMD_DIR (network-download,
communication listeners, scheduler-lib) + the scheduler's SCHEDULE_* seams,
plus the missing-:-guard gotcha that silently writes to the real $HOME
under stub runs.
- New-tool test checklist gains an env-seam review step (grep for unguarded
config writes, prove with VAR=/tmp/x).
- §7 notes stub harnesses are throwaway by design: build in
/tmp/opencode/<tool>-test/, leave there, keep only the pattern.
- howto/schedule.md: migrate copies the rule LHS verbatim as COMMAND — the
old tool never had disk root/loadavg shorthands; rewrite those jobs.
pos tree derives the hierarchy live from bin/pos-* filenames + # POS:
/# POS_SUBCMDS: headers, so it always matches what the dispatcher can
run. Category-less tool (like pos-config); --depth N limits nesting.
Also sharpen the agent-facing docs that the work exposed:
- AGENTS.md: drop the stale "one category-less exception" claim; document
the category-less pattern (pos-config, pos-tree) and that gen only reads
the text after the first em-dash in # POS: headers
- DEV.md: category-less vs categorized rule + header extraction note
- AGENT_Context "Adding a New Tool": new step 0 — pin the exact CLI verb
and runtime context (dev/repo-only vs installed) before writing code
- POS.md: new config + tree sections; category-less tools get their own
sections
- README: pos tree bullet following the pos X convention
- Drop sender 'config'/'config set' subcommand — redundant with 'pos config
telegram' (same POS_CONFIG registry: masked token input/display, chat-id
validation, chmod 600); sender/listener error hints now point there.
- Listener: load_map now uses a \x1f delimiter, so mapped /commands
containing '|' are no longer truncated at the first pipe.
- entertainment send: forward extra plugin args (previously dropped); pass
'--' before the message so leading '-' plugin output isn't parsed as an
option.
- write_config_key/cfg_write: replace unescaped sed writes with grep-v+append
so values with &, |, \ round-trip safely.
- sync_systemd: daemon-reload after removing timer units.
- config-ui: 'digits' validation accepts negative group/supergroup chat ids.
The --send flag duplicated the send subcommand — completion suggested both.
Now 'pos communication telegram <TAB>' completes to just 'sender' and
'listener'. send is the single canonical action.
- git mv bin/pos-communication-telegram bin/pos-communication-telegram-sender
- remove --send branch + POS_FLAGS entry; add --markdown to completion
- lib/notify.sh: notify_sender_name() maps platform telegram -> telegram-sender
(notify_send otherwise looks for bin/pos-communication-telegram)
- pos-system-health --send 'sent:' check + pos-entertainment-send use the new
name and the send subcommand
- completion: keys with nested tools but no direct tool complete to the group
suffixes (sender/listener); --type/--parse-mode value completion moved to
the new word positions
- docs updated (POS.md, AGENT_Context, DEV.md, SCRIPTS.md, notify.env,
HOWTO.md, postinstall.sh); removed phantom webhook/log/broadcast subcommands
from howto/communication.md
- spawn() now respects DRY_RUN, install.sh exports it to child phases, and
postinstall.sh wraps every user-home mutation in run() — '--dry-run' no
longer runs apt/install/clone or edits dotfiles for real
- gen-docs.sh chmods regenerated files to 644 (mktemp mv left them at 0600)
- make check now syntax-checks apps/, entertainment/, features/, templates/
- .gitignore protects config/authorized_keys + config/rclone.conf; drop the
tracked empty authorized_keys and the stray 6 MB session file
- pos-system-health --send prints 'sent:' only when a platform sender exists,
otherwise warns on stderr (notify_send is silent-fail)
- standardize sourced libs (no shebang); refresh AGENT_Context/DEV/APPS/SCRIPTS
doc drift: notify.sh in lib lists, pos-health systemd units, tsui, scripts/,
INTERACTIVE_CMDS list, entertainment scheduler (systemd timers only)
- lib/notify.sh: route notify_send to every platform in NOTIFY_PLATFORM
(notify.env, default telegram; comma-separated = send to all). New
platforms need only a bin/pos-communication-<p> sender implementing
'send <value> [--markdown]' (Matrix/Synapse ready)
- pos-communication-telegram: add --markdown as alias for --parse-mode
markdown to match the sender contract
- lib/common.sh: load_system_env() — shared ~/.config/linux_post_install/
system.env for pos-system-* tools (env exported > file > default)
- pos-system-health/backup: load system.env and show effective dynamic
values (NOTIFY_PLATFORM, HEALTH_BACKUP_MAX_AGE_DAYS, BACKUP_SERVICE_ROOTS)
in --help
- config/system.env + config/notify.env templates copied by postinstall
- systemd/pos-health.service: EnvironmentFile for both configs so the
daily digest honors them