Files
Linux_post_install/AGENT_TODO.md
T
Your Name 010e067935
gates / consistency-and-conventions (push) Successful in 2m34s
fix: ytsync — classify youtu.be/<id> short links as videos; sync tools-docs thumbnail flag
2026-08-23 07:46:51 -04:00

61 KiB
Raw Blame History

AGENT_TODO — Worklist & Idea Backlog

Living list of what we are doing, what is next, and what we might do later. Deep history lives in git: git log --follow AGENT_TODO.md, git blame, and the individual feature commits — the Done section below is just a readable summary (newest last).

Conventions

  • Now — items actively being worked on this session (only a few).
  • Next — queued, well-scoped items.
  • Later — idea backlog. Ideas marked NOT NOW were evaluated and rejected for the stated reason; revisit only if circumstances change.
  • When a task is completed: move it from Now/Next into Done (dated one-line) in the same commit that finishes the work.

Now

Next

    • Wire alerting into more tools as they are added (default: source lib/notify.sh, call notify_send on success/failure).

Later

  • Tier 2: pos health extras — temperature/fan/load average thresholds, ss -tln port checks for known services, SMART status for disks.
  • Tier 3: backup rotation + remote target — keep-N rotations, upload to rclone remote after verify, --remote flag, digest reports rotation age.
  • Tier 3: pos secret vault — gpg/age-encrypted key-value store; backend for future tools that need stored tokens.
  • Tier 3: pos inventory — machine manifest (OS, packages, services, mounted disks, USB devices) exportable as markdown/JSON.
  • Tier 4: pos self update — pull repo, make gen && make check, re-run install.sh to refresh /usr/local/bin.
  • Tier 4: pos new — scaffold a new tool from templates/pos-tool.sh (category, name, POS header, exec bit, doc stubs).
  • NOT NOW: per-category bin/ subdirectories — flat bin/ + filename dispatch scales fine; revisit only if bin/ passes ~40 files.
  • NOT NOW: split lib/entertainment-lib.sh — fine under 600 lines; revisit if it grows.

Done

  • 2026-08-21 — Refreshed AGENTS.md against the codebase: HOWTO category list corrected to match DOC/howto/* (ai/share/schedule, no bare "usb"); CI bullet now states only verifiable facts (lint.yml job gates, push-to-main/PR, ci-ok/<sha>/ci-fail/<sha> result tags) instead of the uncheckable act-runner naming; new Doc conflicts bullet encoding the MAINTENANCE.md → Phase 0 authority order and templates/*.sh as required starting points. Every other claim re-verified against scripts/{gen-docs,check-sync,lint-conventions}.sh, bin/pos (dispatch loop, INTERACTIVE_CMDS), .gitignore/.gitmodules, lib/config-ui.sh; gates green before and after.
  • 2026-08-15pos docker stack (bin/pos-docker-stack) — containers grouped by their Docker Compose project. Each stack is a section (project name, sorted) with lines container-name status ports; containers with no compose project land in a Standalone section at the end; ends with Stacks: N containers: N standalone: N. Running only by default, -a|--all includes stopped/exited (like docker ps -a). Status colored on a terminal (Up* green, Exited*/Dead*/Created* red, Paused*/Restarting* yellow); exit 0 also when no containers. Data via docker ps with --format '{{.Names}}{{"\u001f"}}{{.Label "com.docker.compose.project"}}{{"\u001f"}}{{.Status}}{{"\u001f"}}{{.Ports}}' (compose v2 sets the project label; {{"\u001f"}} escapes in the Go template), parsed with awk -F'\x1f' + IFS=$'\x1f' read everywhere — tab/pipe delimiters are IFS whitespace or inside values, so \x1f (DEV.md:213 gotcha); dash padding via sed not tr (tr corrupts multi-byte ). Deps guard (docker) before --help; no stdin → not in INTERACTIVE_CMDS; # POS_FLAGS: -a --all. Docs: POS.md docker row + detail, howto/docker.md table + section, bin/pos usage EXAMPLES, AGENT_Context §14 row. Verified: stub-PATH suite /tmp/opencode/docker-stack-test/run-tests.sh 23/23 (grouping, sorted stacks, -a shows exited, standalone, empty daemon rc=0, colored status, missing docker rc=1, --help after deps guard); live runs against the real daemon (affine/audiobookshelf/convertx/gitea stacks, affine_migration_job Exited (0) + lab1 Exited (137) under -a); dispatch via pos docker stack; make gen && make check, make lint 0 FAIL / 0 WARN.
  • 2026-08-15 — Fix pos media sync offering a Ventoy stick's EFI partition as the sync target: with the data partition unmounted, the 32 MB VTOYEFI ESP was the only mounted USB partition, usb_detect offered it with no context, and cp died mid-copy with No space left on device (live-box report). usb_detect now fetches FSTYPE/PARTTYPENAME and excludes EFI system partitions (Ventoy VTOYEFI, /boot/efi) from both the mounted list and the mount-offer list; USB_MOUNTED entries carry mp|label|size|model|fs and usb_pick_root shows that in the single-stick confirm and the multi-stick/partition picker (1) /media/Ventoy (1.1T, Ventoy, exfat)), while USB_ROOT stays a bare mountpoint (${root%|*}) so pos system backup (${root%/}/backups) is unaffected. pos-media-sync gained a pre-flight space check (measures exactly what needs_copy would copy vs df -Pk, err/warn before any copy) — no more mid-copy ENOSPC. Docs: howto/media.md target-picking note, SCRIPTS.md usb-lib paragraph, AGENT_Context hand-maintained lib row (194→205). Verified: new stub harness /tmp/opencode/vtoyefi-run.sh (ESP filtered from mounted + mount-offer, multi-pick shows only the data partition, space fit/too-small/dry-run-warn) green; /tmp/opencode/backup-test still green; live check printf 'n\ns\n' | bash bin/pos-media-sync --mp3 no longer offers VTOYEFI (offers unmounted sda1 Ventoy instead); make gen && make check, make lint 0 FAIL / 0 WARN.
  • 2026-08-15 — Fix pos media sync reporting success with 0 files when the source is a symlink: it enumerated with plain find "$SRC", and GNU find (default -P) does not descend a command-line symlink to a directory — ~/Music -> /mnt/hdd/…/music therefore yielded zero matches, the loop never ran, and the tool printed 0 added, 0 updated, 0 unchanged without creating the target dir (live-box report). Switched to find -H "$SRC" (follows only command-line symlinks; inner-symlink semantics unchanged). howto/media.md sync section notes symlinked sources are followed. Caught live, not by the 46-case stub suite (which used a real temp dir source — lesson: add a symlink-root fixture). Verified: printf 'y\n' | bash bin/pos-media-sync --mp3 --dry-run now lists all 31 mp3s as "would copy"; make gen && make check green.
  • 2026-08-14pos system backup — smart USB detection: lsblk TRAN (lsusb/by-id cross-check), mount offer for plugged-in-but-unmounted sticks, sha256-verified copy (stub-suite 54/54).
  • 2026-08-05pos communication telegram--parse-mode (plain/markdown/html).
  • 2026-08-05 — doc/code sync gate — make gen + make check + pre-commit hook.
  • 2026-08-05pos usb server — USB Redirector control tool (494eae2).
  • 2026-08-05pos <category> --help auto-discovery in the dispatcher.
  • 2026-08-05 — AGENTS.md with lazy-loaded DOC references.
  • 2026-08-06 — Fix entertainment timer 1h not firing — interval_to_oncalendar emitted invalid OnCalendar=*-*-* */N:00:00 (systemd rejects */N in the hour field); now *-*-* 00/N:00:00. Dropped the cron fallback entirely: scheduling is systemd user timers only (sync_cron/interval_to_cron/cron_block removed), status simplified, Nd intervals rejected with a clear error.
  • 2026-08-06 — Nested pos subcommands — # POS_SUBCMDS: header annotation (telegram, docker-compose, docker-vbox) + make gen emits a _pos_subcmds completion map; nested tools (telegram listener) auto-list under their parent instead of as a flat sibling (telegram-listener) in pos <category> and tab-completion; generic tool-level completion (subcommands + flags + --help).
  • 2026-08-06 — Telegram listenerpos communication telegram listener: interactive /command → bash map editor + owner-only polling daemon as a systemd user service (map in ~/.config/linux_post_install/telegram_commands.env, re-read per message; /help, unknown-command reply, 60s timeout, stdout reply).
  • 2026-08-06 — NFS in pos systempos system nfs-server (status/share/ unshare/list/reload/enable/disable, idempotent /etc/exports edits, generic default with Tailscale/WireGuard/LAN examples) + pos system nfs-client (mount/unmount/list + persistent mounts as systemd .mount units ordered after network-online.target, no fstab); nfs-kernel-server + nfs-common added to preinstall PACKAGES.
  • 2026-08-06pos HOW-TO guide set — DOC/HOWTO.md index + per-category DOC/howto/*.md (network, docker, media, system, ssh, usb, communication, entertainment) with flags, recipes, config, and troubleshooting; wired into DOC/README, root README, AGENTS.md.
  • 2026-08-06 — Multi-platform alerting — lib/notify.sh routes via NOTIFY_PLATFORM (notify.env, default telegram; sender contract for Matrix/Synapse later), system.env shared config for health/backup, dynamic effective values in --help, telegram --markdown alias.
  • 2026-08-06 — Tier 1 — pos system health (dashboard + --send), lib/notify.sh (wired into backup + firewall), daily digest timer via postinstall.
  • 2026-08-06 — Document Map index + Entertainment section in AGENT_Context (cf36780).
  • 2026-08-06 — Entertainment module — plugins (weather/joke/gold), pos entertainment config/enable/disable/send/status, auto-trigger + Telegram send.
  • 2026-08-07pos system health --send notification-only; listener @quiet prefix (run mapped command without replying, for commands that self-notify). /status=@quiet pos system health --send = exactly one digest.
  • 2026-08-09 — Matrix/Synapse communication tools — pos communication matrix sender + listener, completing the second notify platform lib/notify.sh was designed for (NOTIFY_PLATFORM=telegram,matrix fan-out; the sender implements the send <value> [--markdown] contract via notify_sender_name()'s default key→tool mapping, no lib changes). Sender (bin/pos-communication-matrix-sender): send <value> [--markdown] [--room <id|alias>] PUTs m.room.message (m.text) to the client-server API v3 — room ids/aliases URL-encoded (#pos:example.org%23pos%3A…), unique per-message txn id, --markdown sends org.matrix.custom.html via a best-effort markdown→HTML converter (bold/italic/code/fences/strike/links/headers/lists, escapes HTML, never fails the send); login --user <@id> (masked password prompt → m.login.password → saves access_token+user_id); test. Config scope matrix (~/.config/linux_post_install/matrix.env, MATRIX_HOMESERVER/MATRIX_ACCESS_TOKEN/MATRIX_USER_ID/MATRIX_ROOM_ID, secret masked) registered via # POS_CONFIG:pos config matrix + tab-completion scope. Listener (bin/pos-communication-matrix-listener): systemd user daemon (pos-matrix-listener.service) long-polling /sync (30s timeout, per-sync since token, compact filter dropping presence/account_data/device noise, m.room.message only); reacts to MATRIX_USER_ID's own messages (resolved via /account/whoami if unset), MATRIX_ROOM_ID restricts to one room; / and ! both resolve; replies threaded m.in_reply_to; @quiet no-reply marker; /cmd::desc=… map descriptions; ai … bridge (pos ai gemini ask, per-room session matrix-<room>, ai /reset clears, markdown stripped); interactive editor (--status/--enable/--disable/--run), 60s command timeout, exit-code prefix, ~3800-char truncation. communication-matrix-listener added to INTERACTIVE_CMDS (stdin editor + forever-loop daemon). Docs: POS.md rows + "in detail" sections + ai bridge note, howto/communication.md rewritten Matrix sections, HOWTO.md index + config table + platform note, bin/pos usage EXAMPLES; make gen && make check green. Verified against a mock homeserver: send plain/markdown/--room/test request shape (URL-encoding, Bearer auth, JSON body), login token save, listener owner-filter + /status reply + /help + @quiet silence + non-zero exit reply + interactive editor add. — state-based threshold rule monitors (eventer). Each line of ~/.config/linux_post_install/event.env is an independent rule: ["<msg>" if ] <check-command> <op> <threshold> (op > < >= <= == !=, unit suffix ok 60c/80%). The check command is run on every pass and its first numeric output compared float-safe; operator detected as the rightmost op threshold pair so checks containing their own >/< (awk, redirection) parse fine. Alerts once on false→true plus one recovery message on true→false (no repeats while a condition holds); per-rule state in ~/.local/share/linux_post_install/eventer/state/ keyed by rule-line hash (editing a rule resets its state). Subcommands: run (timer entrypoint), config (interactive add/remove/edit with validation by test-running the check), list (rules + live values), enable [interval] (systemd user timer pos-event-trigger.timer + oneshot service; 5m…weekly or OnCalendar=…; graceful warnings when no user systemd manager, loginctl enable-linger attempt), disable, status. --dry-run honors the DEV.md dry-run convention. Alerts via lib/notify.sh (Telegram default; other platforms via NOTIFY_PLATFORM). New: bin/pos-system-event-trigger, lib/eventer-lib.sh, config/event.env template (installed no-clobber by postinstall), lib/eventer-lib.sh installed by install.sh, system-event-trigger added to INTERACTIVE_CMDS, usage EXAMPLES row. Docs: POS.md system row, HOWTO.md index row, howto/event-trigger.md; make gen && make check green; functional tests covered trigger/recovery/no-repeat, float + unit parsing, editor add/remove/edit + validation + dry-run, timer enable/disable/status (graceful), dispatcher routing.
  • 2026-08-09pos media mp3/mp4 hardened + smart format selection. Both tools: yt-dlp calls go through spawn (honor $DRY_RUN; --dry-run prints the exact command and skips dep checks), -o/--output, --no-playlist, --cookies (file existence check), clean ffmpeg/yt-dlp guards, # POS_FLAGS: for completion, full metadata (--embed-metadata --embed-chapters --embed-thumbnail --no-overwrites, mp3 also --convert-thumbnails jpg + --parse-metadata "%(artist,uploader)s:%(artist)s" so the uploader fills the artist tag). mp3 gains --by-artist (~/Music/<artist>/<title>.mp3). mp4 gains -f <id> / --best / --worst (no prompt), conflict validation, and an interactive picker that shows a curated -F table ([audio]/[video]/[combo] grouping, raw clutter dropped) on stderr — stdout carries only the chosen id (ui_pick lesson) — with id validation against the real table and empty/best default. Docs: howto/media.md rewritten (flags, metadata, by-artist, troubleshooting); make gen && make check green.
  • 2026-08-09 — Telegram ai … now answers about a message you reply to: the listener extracts reply_to_message.text (falls back to caption) from each update and passes it to handle_message; the AI bridge prefixes the prompt with [Reply context — the message you are replying to]. So replying to a /status output and asking ai check this details gives the model the actual output. Applies only to the AI bridge (mapped /commands untouched); reply context rides in the user turn so the session records what was analyzed. Docs: howto/ai.md bridge section.
  • 2026-08-09pos ai gemini sessions + Telegram-friendly replies. --session <name> gives ask/chat persistent memory (~/.local/share/linux_post_install/ai/<name>.json, capped at 40 turns, pruning keeps the first user turn as scene); new sessions subcommand (list / reset <name>). Telegram listener now keeps one session per chat (telegram-<chat_id>) with ai /reset to clear. New --system "<text>" flag injects a Gemini systemInstruction (via jq merge) sent every turn but never stored in the session file; the listener passes a Telegram-voice prompt ("reply like a friendly Telegram chat, use emojis") and strips markdown (**, *, backticks, #, links, lists, blockquotes) from replies before sendMessage, since messages go out as plain text. Docs: howto/ai.md (flags, sessions, bridge memory/formatting), make gen && make check green.
  • 2026-08-09 — Fixed pos config secret-value corruption: cfg_read_secret's cursor-advance echo went to stdout and, since the function is called via $(...), a leading \n ended up inside every secret value → the env file got AI_GEMINI_API_KEY="\n<key>", unreadable by cfg_value/load_config (menu showed (not set), pos ai gemini demanded a key). The newline now goes to the terminal (echo >&2). Defense in depth: cfg_write/write_config_key strip CR and truncate multi-line pastes (warn), cfg_value and the ai/telegram load_configs strip CR on read. Verified on a real PTY (piped tests couldn't reproduce — non-TTY stdin skips the echo path).
  • 2026-08-09ai category — pos ai gemini (ask/chat/models) via Google Gemini REST API. ask prints only the answer (pipe/script/Telegram-friendly), chat is a multi-turn REPL (q/quit/Ctrl+C, /reset, empty input re-prompts), models lists generateContent-capable ids and flags the default; --model override; default gemini-2.5-flash. Config scope ai (AI_GEMINI_API_KEY secret + AI_GEMINI_MODEL) in ~/.config/linux_post_install/ai.env, edited via pos config ai; config/ai.env template installed no-clobber by postinstall; ai-gemini added to INTERACTIVE_CMDS. Telegram listener now answers non-command messages starting with ai via pos ai gemini ask (owner chat only; error replies carry the pos config ai hint) — future intents (reminders) slot in as more case arms in handle_message. Docs: POS.md ai section + listener bridge, howto/ai.md, HOWTO/README index rows, bin/pos usage example.
  • 2026-08-09 — Entertainment plugins gold + weather now emit emoji-visualized Telegram messages. Gold: headline is USD/gram (XAU/oz ÷ 31.1034768), ounce as reference, bid/ask, cleaned timestamp (+00:00/fractional seconds stripped). Weather: per-WMO-code emoji (☀️/🌙 day-night aware for clear sky), °C + feels-like, humidity, wind with unit spacing. Both verified live; emojis are safe in the default plain send mode.
  • 2026-08-09pos tree: prints the live pos command tree (categories → commands → subcommands) by deriving the hierarchy from bin/pos-* filenames + # POS: / # POS_SUBCMDS: headers, so it always matches what the dispatcher can run. Category-less like pos-config; --depth N limit; pos help tree works. Docs: POS.md tree section, bin/pos usage example, make gen regenerated the AGENT_Context tree/dispatch/filetable + _pos_flags[tree].
  • 2026-08-09 — Telegram sender config / config set removed — redundant with pos config telegram (same # POS_CONFIG: registry, masked token display + input, chat-id validation, chmod 600); sender/listener error hints now point there. Deep-review bugfixes in the same commit: mapped /command values containing | are no longer truncated (load_map switched from a | to a \x1f delimiter — previously /up=echo hi | head silently ran echo hi ); pos entertainment send <plugin> [args…] actually forwards the extra args (every arg was shifted in the flag loop, so $@ was empty) and passes -- before the message so leading-- plugin output isn't parsed as an option; write_config_key (entertainment-lib) and cfg_write (config-ui) replaced unescaped sed -i "s|^K=.*|K=\"$v\"|" with grep-v+append so values with &/|/\ no longer mangle (also the path all telegram config now flows through); sync_systemd daemon-reloads after removing timer units; digits config validation accepts negative group/supergroup chat ids (-100…).
  • 2026-08-09 — Fixed telegram listener editor crash on remove/edit/test: ui_pick printed its menu listing to stdout, so idx="$(ui_pick)" captured the menu and the number, and MAP_CMDS[$idx] (arithmetic array subscript) blew up with "syntax error in expression". Menu decoration now goes to stderr; only the picked index is emitted on stdout. Pre-existing bug (before the ::desc work), exposed by the description column.
  • 2026-08-09 — Telegram listener pushes its mapped /commands to the bot's / menu via setMyCommands (auto after every map edit, on --enable, and at daemon start; manual --sync-commands flag). Map lines may carry a menu description: /cmd::short description=bash command (falls back to the bash command, ~40 chars). Names are validated against Telegram's lowercase [a-z0-9_] rule — invalid ones are skipped from the menu with a warning but still resolve when typed; empty map clears the menu. Fixed latent bugs found by the sync work: map_has (awk END{exit 1} overrode the match), and warn() went to stdout so it leaked into the generated JSON (now stderr).
  • 2026-08-09pos config <TAB> scope completion is now cached at gen time (_pos_config_scopes array emitted by make gen from the # POS_CONFIG: registry) instead of scanning ~40 tools per TAB — a per-keypress subshell storm that wedged interactive shells for minutes on the loaded homelab box. Two stuck -bash sessions (69%/38% CPU) killed. plugin_marker/plugin_keys hardened with || true so config_keys no longer aborts mid-scan under set -euo pipefail on mixed lib/plugin dirs (installed layout) — fixes missing plugin keys in pos config entertainment.
  • 2026-08-09pos config <scope> interactive config editor: reads the # POS_CONFIG: registry across tools into a single runtime config (~/.config/linux_post_install/*.env, one file per scope, chmod 600); secret masking with show/hide toggle, digits:/num:/url: validation, - to clear, blank keeps; *plugins marker expands plugin vars (entertainment) from entertainment-lib.sh; desc::example value-format hints shown in the editor; gen-docs now handles category-less tools (pos-config), fixed a set -e+pipefail bug that truncated the header registry.
  • 2026-08-09pos-communication-telegrampos-communication-telegram-sender: one canonical send (dropped the legacy --send flag, which duplicated the send subcommand in completion). pos communication telegram <TAB> now completes to just sender listener. lib/notify.sh maps platform telegramtelegram-sender via notify_sender_name(); entertainment-send + health --send check updated. Removed phantom subcommands from howto/communication.md (webhook/logs/broadcast/file never existed).
  • 2026-08-09 — Structure/convention audit fix: --dry-run now truly dry (spawn() honors DRY_RUN, install.sh exports it to child phases, postinstall mutations run-wrapped); gen-docs.sh no longer chmods regenerated files to 0600; make check now syntax-checks apps/entertainment/features/templates; .gitignore protects config/authorized_keys + config/rclone.conf; honest --send confirmation; docs refreshed (notify.sh in lib lists, pos-health systemd units, tsui, scripts/, INTERACTIVE_CMDS).
  • 2026-08-11 — Docs: DEV.md / AGENTS.md / AGENT_Context improved from the SMB session's lessons. DEV.md: new "Testing tools that need root / systemd / missing deps" (env-override test seams — FLAGS_DIR/SMB_CONF/SMB_CREDS_DIR/UNIT_DIR precedents — + stub-PATH fakes + PTY prompt driving via script); new Best Practice "Managed Config Blocks" (start/end marker idiom incl. the inblock == 1 awk guard, validate-then-apply, hot reload); deps-guards-run-before---help made explicit (previously only inferable by reading the NFS tools); "Update the docs" checklist completed (howto index/section, Common Tasks row, AGENTS.md Quick facts, AGENT_TODO Done move). AGENTS.md: clarified which filetable line-count rows are hand-maintained (non-pos-* files above the marker) + when to bump them; deps-guard clause added to Quick facts. AGENT_Context "Adding a New Tool" steps 67 mirror the above. make gen && make check green.
  • 2026-08-11share category grows SMB: pos share smb server (bin/pos-share-smb-server) + pos share smb client (bin/pos-share-smb-client), completing the share trio (usb/nfs/smb). Server: status/share/unshare/list/adduser/deluser/reload/enable/disable; idempotent marker blocks in /etc/samba/smb.conf (# >>> pos-managed share: <name># <<< end pos-managed share — hand edits outside markers survive; inblock==1-guarded awk so removing one block never eats another's end marker), testparm validation before apply + smbcontrol smbd reload-config hot reload; --read-only/--guest/--users u1,u2 flags with unrestricted-share warnings; smbpasswd user management (prompts, requires system user first). Client: mount/unmount/list/persist/unpersist; password prompt via /dev/tty, throwaway chmod-600 credentials for one-shot mounts, persistent creds at /etc/samba/credentials/<name> (chmod 600); persist writes a systemd .mount unit (systemd-escape) with x-systemd.automount + _netdev — mounts on first access, never blocks boot. Both source lib/notify.sh for mutations; added to INTERACTIVE_CMDS (prompting subcommands). Deps: samba + cifs-utils added to preinstall PACKAGES. SMB_CONF/SMB_CREDS_DIR/UNIT_DIR env-overridable for tests (FLAGS_DIR precedent). Docs: POS.md share rows, howto/share.md SMB sections, HOWTO index row, AGENT_Context Common Tasks, AGENTS.md categories. make gen && make check green; logic tested via stubbed PATH + temp config (marker idempotency, guest + user persist flows).
  • 2026-08-11pos network checkport nmap overhaul: two-pass engine — pass 1 = fast -Pn -T4 --max-retries 1 scan of only the asked ports (was: all 65535) with per-port state + nmap service names; pass 2 (--versions, opt-in) = -sV --version-light on open ports only (generous host-timeout — version probing a silent service otherwise made nmap skip the host entirely), fallback fast banner probe for open TCP with no version info; TCP fast path ~2s for 3 ports. Unprivileged UDP now falls back to the nc engine (Debian nmap -sU requires root and quit outright); IPv6 hosts get -6; no output/filtered states set rc=1; --timeout scales nmap host-timeouts. New --versions flag in # POS_FLAGS: (completions regenerated) + usage text; port-metadata fallback retained. make gen && make check green.
  • 2026-08-11pos communication matrix sender login error reporting: captures HTTP status + Matrix errcode/error from the JSON body (temp file, not stdout) instead of a generic "wrong credentials?" message — distinguishes unreachable homeserver from rejected credentials; auto-prepends @ when --user is bare (e.g. --user alice:example.org@alice:example.org).
  • 2026-08-11 — New share category — usb and nfs moved out of pos usb / pos system into pos share: pos share usb server (was pos-usb-server), pos share nfs server + pos share nfs client (were pos-system-nfs-*). Renamed the three tools (bin/pos-share-*), updated # POS: headers/usage strings, INTERACTIVE_CMDS (usb-servershare-usb-server), bin/pos usage() EXAMPLES, and the notify-scope comment in pos-system-backup. Docs: new DOC/howto/share.md (USB + NFS consolidated; howto/usb.md deleted, NFS sections stripped from howto/system.md), POS.md ### share section (replaces ### usb, nfs rows moved out of ### system), HOWTO/README indices, AGENT_Context hand-written spots, root README, DEV.md INTERACTIVE_CMDS example, AGENTS.md categories. Category is the home for future smb. make gen && make check green; /usr/local/bin refreshed.
  • 2026-08-12pos network download (bin/pos-network-download) — aria2 JSON-RPC daemon + queue control. Daemon: persistent aria2c as a systemd user service (pos-aria2.service, ${XDG_CONFIG_HOME:-$HOME/.config}/systemd/user, enable --now + linger warning on headless boxes), --rpc-listen-port=6800, generated RPC_SECRET in ~/.config/linux_post_install/download.env (chmod 600, env override), unit flags --continue=true --max-connection-per-server=16 --split=16 --seed-time=0 --dir=$HOME/Downloads. Commands (18): start/stop/status (+ bare overview = status+list), add <url…> [--dir --out --split --tmux], torrent <file|magnet…> [--dir --seed --tmux] (base64 addTorrent), metalink <file|url> [--tmux], list (active/waiting/stopped table), info/files/peers <gid>, pause|resume|remove [gid|all] (--forceforce*), purge, move <gid> <pos>, limit [gid] <speed> (--upload, 0=unlimited, 2M/512K), set <k=v…> [--gid], watch [gid] (2s live repoll; exits when that gid completes). --tmux opens a detached dl-<name> session running watch <gid> (name from --out/URL basename, sanitized, 40-char truncate, -2 on collision; closes itself on completion). Deps: aria2c/jq/curl guards before --help; aria2 added to preinstall PACKAGES. No stdin → not in INTERACTIVE_CMDS. JSON built with jq -nc --arg (never string interpolation — fixes JSON-quote bugs); # POS_SUBCMDS: (18) + # POS_FLAGS: → completions. Test seams RPC_PORT/RPC_SECRET/DOWNLOAD_DIR/USER_SYSTEMD_DIR/ACTIVE_MARKER; 76-case stub-PATH behavior suite green (unit content, secret 600, add→gid, tables, queue ops, error paths). Docs: POS.md network row+detail, howto/network.md section, HOWTO index, AGENT_Context Common Tasks row. make gen && make check green.
  • 2026-08-12pos network download grows outage resilience: restart <gid> (re-queue from history — torrents via rebuilt magnet urn:btih: + &tr= trackers, HTTP via original URIs with dir/out preserved, --continue=true resumes partials; options --dir/--seed/--split/--tmux), retry <gid|all> (smart retry — waits out internet outages via NET_PROBE seam, re-queues, retry_verify polls the new gid; aria2 error 3 = real problem → diagnosed + marked permanent in ~/.config/linux_post_install/download.retry as url:<uri>/bt:<infohash>, retry all skips them, manual restart overrides; --once/--quiet timer mode; --interval/--max-wait), and the retry healer systemd user pair (pos-aria2-retry.service oneshot retry all --once --quiet + pos-aria2-retry.timer 2min, Persistent) that arms on download start (add/torrent/metalink/restart) and disables itself when nothing is left; watch <gid> now auto-restarts its download after an outage. Fixes from stub-suite review: ensure_healer was missing from the three submit paths; RESTART_NAME was lost across do_restart's process-substitution subshell (now a download_name() helper); restart exited 1 because the [ tmux -eq 1 ] && tmux_watch test was the function's last statement. Verification: stub-based test harness (/tmp/opencode/dl-test — curl/systemctl stubs with tellStatus fixtures, NET_PROBE file-flip, unit enable/disable logging) 119/119 green, incl. new restart/retry/healer/watch-heal cases. Docs: POS.md download rows + outage-resilience paragraph, howto/network.md outage recipe, SYSTEMD.md per-user units section, AGENT_Context + completions regenerated. make gen && make check green.
  • 2026-08-12pos network download replace <gid> <url> + fresh-link status advisory. status now flags stopped errored downloads whose source is marked permanently failing in download.retry (needs fresh link: <name> (<gid>) — pos network download replace … <new-url>; one tellStopped RPC, id-match in jq). replace <gid> <url> re-queues a dead single-file HTTP/FTP download with a new URL keeping the same dir + file name (partial resumes via --continue=true), unmarks the old source (retry_unmark, literal grep -vxF — URL-safe), and reuses retry_verify so a dead replacement link is diagnosed + marked permanent; torrents/active/multi-file are rejected with hints; --dir/--split/--tmux supported. retry_verify hardened to ${quiet:-0} so it works outside cmd_retry. Stub suite grew a replace section (advisory match, success + unmark + advisory-clear, dead new link marked, torrent/active/arg errors, prefix gid) — tellStopped fixtures gained uris (real aria2 includes them). 141/141 green; docs: POS.md row + outage paragraph, howto/network.md dead-link recipe. make gen && make check green.
  • 2026-08-12pos system event-trigger (eventer) generalized into pos system schedule — the scheduler replaces the single-timer threshold monitor with per-job systemd user timers (pos-schedule-<name>.{timer,service}, Persistent, ExecStart run <name>, reconciled on enable/disable — orphan units + the legacy pos-event-trigger timer auto-removed). Each job is a chmod-600 file ~/.config/linux_post_install/schedule.d/<name>.env: INTERVAL (5m..59m/1h..23h/hourly/daily/weekly/OnCalendar=…), NOTIFY policy, optional MSG, RULE (threshold only), and COMMAND = literal remainder of the line (pipes/quotes/sudo need no escaping). Policies: always (full output every run), onchange (diff vs last run, first run always sends), onerror (non-zero exit or empty output), threshold (old event-trigger behavior: first numeric vs RULE, alert on false→true + recovery, per-job firing state), never (silent side-effect jobs — no notify; run log + last-run record still kept). Per-run logs/state in ~/.local/share/linux_post_install/schedule/{logs,state}/. Subcommands: run [name|all], list, config (interactive add/edit/remove/enable/disable with validation), enable [name|all], disable [name|all], status, migrate (converts legacy event.env rules → schedule.d/rule-N.env threshold jobs, adopts the legacy timer's OnCalendar or 5m, removes the old timer). Files: bin/pos-system-event-triggerbin/pos-system-schedule, lib/eventer-lib.shlib/scheduler-lib.sh (git mv; installed by install.sh), config/event.env + config/event-rules.templateconfig/schedule.d/ starter jobs (nvme-health via sudo -n smartctl with the user's exact grep — sudoers NOPASSWD documented; cpu-temp + disk-root thresholds; silent log-cleanup), postinstall installs them no-clobber into an empty schedule.d/ (legacy event.env users get a migrate hint instead). bin/pos EXAMPLES + INTERACTIVE_CMDS (system-schedule config) updated. Supersedes the "Tier 2: watch plugins" backlog idea. Docs: POS.md system row rewritten, howto/event-trigger.md → howto/schedule.md (job syntax, policies, NVMe recipe, migration), HOWTO.md index row + config table + scheduling bullet, AGENT_Context lib row + Common Tasks row. make gen && make check green; stub-harness suite (fake systemctl/sudo/smartctl/sensors/df + fake telegram sender logging, env seams SCHEDULE_DIR/SCHEDULE_STATE_DIR/SCHEDULE_LOG_DIR/USER_SYSTEMD_DIR/SCHED_LEGACY_ENV) covers all 5 policies (threshold cross/recover/no-repeat, onchange first/diff/same, onerror, always, never-silent), COMMAND literal-pipe parsing, enable/disable/status + orphan/legacy cleanup, migrate (incl. skip-existing + dry-run), and dispatch.
  • 2026-08-13pos communication scrcpy audio control: scrcpy already forwards device audio to the desktop by default (answer: yes, default is sound-to-desktop). Added SCRCPY_AUDIO config key (pos config scrcpy, default true): false/no/0--no-audio, true/yes/1 → nothing (default), anything else → error. Docs: POS_CONFIG header, POS.md config table, howto/communication.md Mirror section, HOWTO.md env row. Verified: harness +7 tests (47/47 green — false/true/yes/0/invalid/combined-order), bash -n, make gen && make check green.
  • 2026-08-13pos communication scrcpy --new-display support: new SCRCPY_NEW_DISPLAY config key (pos config scrcpy) — true/yes → bare --new-display (default size/dpi), 1920x1080, 1920x1080/420 or /240--new-display=<value>; inline validation in _mirror (err runs in the main shell, not a process-substitution subshell) rejects anything else with the accepted forms. Docs: POS_CONFIG header, POS.md command+config tables, howto/communication.md Mirror section, HOWTO.md env row, usage() example. CLI pass-through pos communication scrcpy --new-display=1920x1080 also works verbatim. Verified: harness +8 tests (40/40 green — WxH, true, WxH/DPI, /DPI, invalid-rejected, env>config, combined order), bash -n, make gen && make check green.
  • 2026-08-13 — Fix pos communication scrcpy mirror failure on the real box (ERROR: Unexpected additional argument: on every mirror, bare or with flags): _extra_flags() ran printf '%s\n' with an empty array expansion, which prints one blank line; _mirror()'s while read turned that into an empty-string arg passed to scrcpy. Fix: _extra_flags now returns early when SCRCPY_EXTRA_FLAGS is empty (and printf '%s\n' "${extra[@]}" when set), and _mirror defensively skips blank entries ([ -n "$f" ] && cmd+=("$f")). Rebuilt the stub-PATH suite (/tmp/scrcpy-run-test.sh, outside the wiped $TEST_DIR) — 32/32 green incl. the regression (bare mirror → zero args to scrcpy) and EXTRA_FLAGS + passthrough mixed. bash -n, make gen && make check green.
  • 2026-08-13 — Fix scrcpy apt install on the live box: preinstall apt install failed with Unable to locate package scrcpy (Debian/Ubuntu need contrib/universe for scrcpy, and the apt build is older anyway). Removed scrcpy from preinstall.sh PACKAGES (kept adb); scrcpy now installs via the existing optional app apps/media/scrcpy.sh (GitHub latest, bundles adb) — docs (POS.md, howto/communication.md) and the tool's deps-guard error reworded to lead with that path. Re-verified: bash -n, stub suite 21/21, make gen && make check green.
  • 2026-08-13pos communication scrcpy (bin/pos-communication-scrcpy): wrapper over scrcpy+adb for Android mirroring/control. Subcommands: bare scrcpy (mirror — config defaults + verbatim pass-through of any scrcpy flag; no device → friendly error + hints), devices (adb devices -l), record [file] [--headless] (default $SCRCPY_RECORD_DIR/<device>_<date>.mp4, --headless = --no-playback for headless servers), tcpip [port] (USB→wireless switch + prints connect with the auto-detected device IP), connect <ip[:port]> (adb connect + mirror -s), push (default /sdcard/Download = scrcpy's own default), pull, screenshot (adb exec-out screencap -p → PNG in RECORD_DIR), info (model/android/sdk/serial via getprop). Config scope scrcpy (~/.config/linux_post_install/scrcpy.env, pos config scrcpy): SCRCPY_SERIAL/MAX_SIZE/MAX_FPS/BIT_RATE/FULLSCREEN/RECORD_DIR/PUSH_TARGET/EXTRA_FLAGS, env-var precedence. Deps scrcpy + adb added to preinstall PACKAGES; docs note the apt build is older and point to the existing apps/media/scrcpy.sh app installer (GitHub latest, bundles adb) — researched 2026 releases (current v4.1). Conventions: # POS:/# POS_SUBCMDS:/# POS_CONFIG: headers, deps guards before -h|--help, no stdin → no INTERACTIVE_CMDS. Verified: bash -n, stub-PATH suite /tmp/opencode/scrcpy-run-test.sh 21/21 green (fake adb/scrcpy echo-args, HOME isolation, env/file precedence, rc paths, screenshot bytes), make gen && make check green, dispatch via pos communication scrcpy --help. Docs: POS.md communication table + detail block, howto/communication.md section, HOWTO.md index + env row, AGENT_Context Common Tasks row + gen'd tree/dispatch/filetable.
  • 2026-08-13 — Entertainment-module hardening (approved Tier 1 + Tier 2): delivery moved to notify_send (platform follows NOTIFY_PLATFORM, default Telegram) via lib/notify.sh sourced by bin/pos-entertainment-send; a last-run state is recorded per plugin (~/.local/share/linux_post_install/entertainment/last/<plugin> — rc + timestamp) on every non---print run and shown by pos entertainment status, which also lists installed-but-not-enabled plugins; a send that fails while fired by a timer (gated on $INVOCATION_ID) additionally notifies the configured platforms. New message-safe plugin lib lib/entertainment-plugin-lib.sh (defines only plugin_*, never writes stdout — the stdout contract stays "message only"): plugin_load_config (entertainment.env + env precedence), plugin_have, plugin_require, plugin_err, plugin_http_json <url> [--key <jq>] [-H <header>] (curl --max-time 20 --retry 2); weather/joke/gold refactored onto it. pos entertainment config gains get|unset|ls|edit (edit via the shared pos config UI — added to INTERACTIVE_CMDS). Tier 2: new shared lib lib/user-timers-lib.sh (only ut_*: ut_interval_to_oncalendar, ut_interval_label, ut_unit_name, ut_write_unit_pair incl. TimeoutStopSec=5s + Persistent + network-online deps, ut_ensure_linger, USER_SYSTEMD_DIR) dedupes the systemd user-timer machinery between lib/entertainment-lib.sh and lib/scheduler-lib.sh (the latter's sched_* duplicates deleted; both source it; collides-with-nothing). install.sh Phase 2 lib list += the two new libs; SCRIPTS.md/DEV.md/POS.md/howto/entertainment.md/AGENT_Context updated (hand-maintained lib rows: entertainment-lib 354→311, scheduler-lib 830→760, +112 user-timers-lib, +67 plugin lib). Verified: bash -n everywhere; smoke-tested in an isolated HOME=/tmp/enttest (status, config get/set/unset/ls, send path rc=0, failing plugin records rc=1, error-case message hygiene); make gen && make check green.
  • 2026-08-13 — Fast pos-unit shutdown: every systemd unit a pos tool writes (or systemd/ ships) now sets TimeoutStopSec=5s (+ KillMode=control-group on the daemons) so a stuck process can't stall a reboot for the 90s systemd default. Applied at all 7 template sites: pos-communication-telegram-listener, pos-communication-matrix-listener (also gained a trap 'kill $(jobs -p) 2>/dev/null; exit 0' TERM INT in run_daemon so stop returns sub-second), pos-network-download (aria2 + retry-healer units), lib/scheduler-lib.sh sched_write_units, lib/entertainment-lib.sh write_units, and systemd/{ssh-agent,autostart,usb-automount}.service. Legacy-unit cleanup: the repo no longer ships pos-health.{service,timer} / pos-entertainment.service (they were documented but postinstall never created them — found stale only on the live box, FAILED); removed their stale references from SYSTEMD.md (deleted the pos-health.service section + gating special-case, added a new Stop behavior section), POS.md, HOWTO.md, howto/system.md (now documents the pos system schedule job replacement + removal commands), AGENT_Context (tree, phase description, selfcontained table). DEV.md Best Practices gains a Systemd units convention (TimeoutStopSec=5s + TERM trap + regeneration caveat). Verified: bash -n on all edited scripts; make gen && make check green (filetable rows for the two listeners + network-download auto-regenerated, hand-maintained lib rows bumped 350→354 / 822→830). Live-box application is manual (this session was a Google Cloud Shell, not the real machine): regenerate units via pos network download start, pos communication telegram listener --enable, pos system schedule enable <job>, pos entertainment enable <plugin>, then sudo systemctl disable --now pos-health.timer pos-health.service 2>/dev/null; sudo rm -f /etc/systemd/system/pos-health.{service,timer} && sudo systemctl daemon-reload.
  • 2026-08-13 — Bootstrap output transparency (install.sh / preinstall.sh / postinstall.sh): removed the redundant apt update (preinstall.sh owns it — install.sh previously ran it twice, showing two identical OK apt update lines); Phase 2 now names what it installs — libs line (libs -> /usr/local/bin (644): common.sh flags.sh …), plugin names in the count line, x64_bin names, per-feature feature installed/overwritten + feature flag set logs with a N features installed: … summary — and the misleading "47 scripts + libs" label is fixed to 47 scripts + 6 libs (the 6 libs were outside the counter); preinstall prints Installing N packages (apt install -y): with the 40-name list wrapped at 80 cols; postinstall now logs silent skips — config/authorized_keys is empty — nothing to add (empty file previously looped zero times with no message), schedule.d already exists, keeping it (restructured the condition so the message is accurate when the dest exists vs config/schedule.d absent), and a per-service service enabled: <name> line. No output-layer changes (no --verbose, no log file — decided scope). Verified: bash -n + --dry-run smokes of phases 1/2/3 showing every new line (learned: install.sh:19 hardcodes export DRY_RUN=0, so an env DRY_RUN=1 is ignored — the flag --dry-run is required), hand-maintained filetable count rows bumped (install.sh 206→223, preinstall.sh 73→75, postinstall.sh 163→168), make gen && make check green. usb-automount left live (user choice).
  • 2026-08-13usb-automount feature, integrated exactly like autostart: features/usb-automount.sh (root-guard re-exec via sudo; first-root-run self-install of udev rule /etc/udev/rules.d/99-usb-automount.rulesACTION=="add", KERNEL=="sd[a-z]*", SUBSYSTEM=="block", ENV{ID_BUS}=="usb", TAG+="systemd", SYSTEMD_WANTS="usb-automount.service" — + udevadm control --reload + trigger --subsystem-match=block; an existing/edited rule is never overwritten; scans lsblk -J for unmounted removable partitions/raw whole-disk filesystems, mounts each at /media/<label> — vfat/exfat/ntfs world-writable via -o umask=000, fallback plain mount, label-collision bump -2/-3, no label → usb-<name>, logs ${HOME:-/root}/.usb-automount.log) + systemd/usb-automount.service (Type=oneshot, WantedBy=multi-user.target — boot + hotplug + manual systemctl start usb-automount), gated in postinstall.sh's systemd loop exactly like autostart (flag_is_set usb-automount → skip with hint). Purpose: a plugged-in stick is auto-mounted world-writable, ready for pos system backup's post-verify USB copy. Docs: SYSTEMD.md (service section + gating code block), SCRIPTS.md (feature section + systemd bullet + TOC), AGENT_Context tree + filetable rows (postinstall.sh count corrected 152→163 — it was already 6 lines stale), README index rows. Verified with a stub suite (/tmp/opencode/usb-automount-test — lsblk JSON fixtures, mount/mountpoint/udevadm/sudo stubs, MOUNT_BASE/UDEV_RULES_DIR seams, HOME isolation): 47/47 green. make gen && make check green. Gotcha learned: ${VAR:-{...}} with a { inside the parameter-expansion default mis-parses in bash (emits a stray } — printf of a multi-line value showed }}); avoid braces in :- defaults.
  • 2026-08-13pos system backup copies the finished backup to a USB stick. Detection runs after the archive verifies (so a stick plugged in while the backup ran is found; if none is mounted, one re-scan prompt before giving up — s skips, EOF from cron skips silently, rc stays 0). Single stick → y/N confirm; several → numbered pick (0 = skip). Copy lands in <usb>/backups/ (mkdir -p; chmod 600 best-effort — vfat chmod failures warn, never fail), and the transfer is proven 100% by sha256 source-vs-copy before any success is announced: mismatch → warn with both hashes + notify_send "USB copy FAILED…" + rc=1 (the ERR trap is re-armed mid-script so a USB-phase failure no longer notifies "Backup FAILED"). Detection: lsblk -J → recursive jq filter (rm==true && mounted && type part|disk, space-safe via JSON) or pinned BACKUP_USB_ROOT seam (= <root>/backups/, skips detection — also the test seam). Docs: usage() Environment, POS.md backup row, howto/system.md (USB section + env table + mismatch troubleshooting), DEV.md system.env list. Verified with a stub suite (/tmp/opencode/backup-test — sudo/gpg/lsblk/sender stubs, HOME isolation, per-test lsblk JSON fixtures, corrupting-cp + vfat-chmod override stubs): 40/40 green (skip s/EOF, seam y/n, detect single, multi pick 2/0, re-scan after replug, corrupt copy rc=1 + honest notify, vfat tolerance). make gen && make check green.
  • 2026-08-13pos share smb-server share now guards the two common NT_STATUS_ACCESS_DENIED causes at share time (warnings only): --users entries missing from the Samba passdb (pdbedit -L, cut to user column, grep -qxF per user — pointer to pos share smb-server adduser <user>), and ancestors of the share path lacking other:+x traversal (sticky dirs like /tmp count as traversable via the t slot; fix hint chmod o+x <dir>). Both wired into the share case after require_root_dir; howto/share.md SMB section + troubleshooting updated. Rooted in reports/bug-report-smb-server-access-denied.md (committed as the spec). Verified with a stub-PATH suite (/tmp/opencode/smb-test — pdbedit/systemctl/smbcontrol/testparm/smbpasswd stubs, SMB_CONF seam): 16/16 green.
  • 2026-08-13 — Docs hardening from the schedule-session review (sole-developer call: terse, session-learned). DEV.md §7 env-seam registry now lists USER_SYSTEMD_DIR (bin/pos-network-download, bin/pos-communication-{telegram,matrix}-listener, lib/scheduler-lib.sh) + the scheduler's SCHEDULE_* seams, and documents the missing-:--guard gotcha (a VAR="${XDG…:-…}" without leading VAR:- overrides the seam — stub runs then silently write to the real $HOME; fix: USER_SYSTEMD_DIR="${USER_SYSTEMD_DIR:-…}"). 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 documents that migrate copies the rule LHS verbatim as COMMAND (old tool never had disk root/loadavg shorthands — rewrite those jobs with real commands). make check green.
  • 2026-08-14 — Gitea Actions gate is now live and green end-to-end: act_runner (v0.6.1, labels ubuntu-latest) registered on 100.100.1.2 (~/srv/gitea/runner/, standalone compose next to the ScaleTail gitea; CONFIG_FILE=/config.yaml env required or run.sh never reads the config; --add-host gitea.skink-platy.ts.net:100.111.241.54 so the job container reaches gitea). First real runs caught a deterministic gen-drift: plain sort in scripts/gen-docs.sh is locale-dependent (category-less tool keys start with |, which collates after letters under the CI container's locale → pos-config/pos-tree reordered), so the git diff --exit-code step failed. Fixed with export LC_ALL=C in gen-docs.sh (byte-order sort) + regenerated DOC/AGENT_Context_Project.md (config/tree now sort after the letter categories); make check OK, make lint 0 FAIL / 0 WARN. Live CI verdicts: the run for e0b5b11 (workflow commit) and the empty trigger 98a767c both FAILED on the drift; the run for 9d058b7 (the fix) SUCCEEDED (🏁 Job succeeded).
  • 2026-08-14 — Gitea Actions gate added: .gitea/workflows/lint.yml runs make gen + git diff --exit-code (gen-drift) + make check + make lint on every push/PR. Verified locally the exact four steps pass (gen idempotent, check OK, lint 0 FAIL / 0 WARN). "no CI" lines updated in AGENTS.md (Quick facts → CI bullet, notes a registered act_runner is required) and DEV.md (stub harnesses note: CI runs static gates only, not behaviour suites). Gitea 1.26.4 confirmed reachable; runner registration completed the same day (see the entry above).
  • 2026-08-14 — Convention-drift maintenance fix session (completed the audit backlog MAINTENANCE.md, M-001..M-023, all VERIFIED; gate scripts/lint-conventions.sh + make lint now 0 FAIL / 0 WARN; make gen && make check green). P0 bugs: M-002/003/004 added docker-compose docker-vbox network-hotspot to INTERACTIVE_CMDS (stdin/log-pipe prompt swallow); M-005 install.sh --steps now expands documented N-M ranges via normalize_steps_spec() (dry-run verified); M-006 feature-vs-docs decision: --send/--markdown not restored (health is a console-only reporter by design since fe7708f; scheduler NOTIFY=always covers delivery) — 5 docs corrected instead; M-007 lib/notify.sh:57 fallback routed to stderr (stdout-leak on standalone source). P1: M-008..M-014 deps guards moved before -h|--help in docker-health/docker-ps (converted to command -v X || err), network-scan, share-usb-server, media-mp3/mp4 (guards before help with a --dry-run pre-scan preserving the documented no-deps preview); system-health documented as the sanctioned graceful-degradation no-guard pattern in DEV.md — lint refined accordingly (first_guard_line only matches real guards; first_line skips comments; precision fixes, not weakenings); M-015 system-firewall gained usage()+-h|--help (root-gated first; verified via sudo); M-016 ffmpeg added to preinstall PACKAGES. P2: M-017/M-018 autostart + usb-automount gained the feature-template preamble (flags.sh load, usage); M-019 chmod +x apps/media/scrcpy.sh; M-020 SCALE_DIR/CONFIG_ENV :- seams in pos-docker-compose (verified via overrides; follow-on fix: DIM color var missing from common.sh crashed pos docker compose config — added it); M-021 CONFIG_DIR centralized as the canonical XDG-aware seam in common.sh, per-file duplicates dropped (standalone-sourced notify.sh/config-ui.sh/matrix+telegram tools keep an identical guarded copy — "no shared lib? inline fallbacks"); M-022 plugin_* prefix collision resolved by renaming the internal registry helpers to ent_plugin_* (the documented plugin-authoring API plugin_have/plugin_require/plugin_load_config/plugin_http_json kept for user plugins); M-023 six tools (pos-config, pos-tree, pos-entertainment-{config,enable,disable,status}) now filename-referenced in DOC/POS.md. Hand-maintained AGENT_Context line-count rows bumped (install.sh 223→248, preinstall 75→76, common.sh 144→151, notify.sh 76→87 stale-corrected, autostart 14→50, usb-automount 134→138); make lint target wired in the Makefile. MAINTENANCE.md kept as the working record (uncommitted by design).
  • 2026-08-14pos system backup optional encryption (--no-encrypt flag + BACKUP_ENCRYPT=0 env, flag-or-env — user chose "Flag + env only"): plain path keeps a verified .tar.gz with no password prompt (headless/cron safe); encrypt path unchanged (prompt → gpg AES-256 → decrypt-verify; the gpg dep-guard moved into the encrypt branch so plain backups no longer require gnupg). Arg parsing rewritten as a loop over "$@" so pos system backup <folder> --no-encrypt works with the flag after the folder; usage() documents all three forms + the plain artifact name; # POS_FLAGS: --service --no-encrypt; config/system.env template gains #BACKUP_ENCRYPT=0; POS.md row + howto/system.md section updated. Verified: stub suite +2 cases (T18 flag / T19 env: plain .tar.gz artifact, gpg never called via $GPG_CALLED, USB copy + sha256 of the plain archive, notify wording) — 65/65 green; bash -n, make gen && make check, make lint 0 FAIL / 0 WARN.
  • 2026-08-14 — CI green-check via plain git (no SSH to the runner, no API tokens — user chose "CI tags + git ls-remote" + "scripts/ci-status.sh helper"): .gitea/workflows/lint.yml scoped to on: push: branches: [main] (tag pushes no longer re-trigger it) and the gate step now reports its own outcome as a lightweight tag — ci-ok/$GITHUB_SHA on success / ci-fail/$GITHUB_SHA on failure, pushed over HTTP with the jobs automatic GITEA_TOKEN to http://oauth2:${GITEA_TOKEN}@gitea.skink-platy.ts.net:3000/admin/Linux_post_install.git (runner container already host-maps that hostname to 100.111.241.54); steps.gates.conclusion decides ok/fail, if: always() (guarded to push events) covers failed gate runs, and an existing-tag guard makes re-runs idempotent. New executable scripts/ci-status.sh [--wait] [<sha>] reads the tags via git ls-remote (origin, CI_STATUS_REMOTE override): GREEN (0) / RED (1) / PENDING (2); --wait polls every 10s up to 10 min. DEV.md §CI gains a "Checking green without SSH" bullet. Verified: bash -n, yaml-parse OK, make gen && make check, make lint 0 FAIL / 0 WARN; first live-tag verification pending the push (fallback if Gitea clamps token-push: PAT as workflow secret).
  • 2026-08-14pos media sync (bin/pos-media-sync) — incremental Music → USB sync, plus the shared USB layer it builds on. New lib lib/usb-lib.sh (194 lines, installed by install.sh): usb_detect (lsblk JSON, TRAN + lsusb/by-id cross-check → USB_MOUNTED/USB_UNMOUNTED), usb_related_present, usb_mount_offer (/media/<label> mount-offer, usb-automount scheme), usb_pick_root <prefix> <subfolder> <giveup-msg> (detect → mount-offer → single/multi picker → USB_ROOT); seams USB_MOUNT_BASE/USB_BYID with BACKUP_MOUNT_BASE/BACKUP_USB_BYID aliases so existing system.env lines keep working; TRAN-fallback warning deduped to once per scan. pos-system-backup refactored onto it (216 lines, was 364) — re-ran the backup stub suite: 65/65 green. Sync tool: add/update only, never deletes (user choice); --mp3/--mp4 filter (neither = both), --source <dir> (default MEDIA_SYNC_SOURCE/$HOME/Music), --dry-run preview with counts; copies missing/changed (size/mtime) files into <usb>/Music/ (MEDIA_SYNC_DEST) preserving the tree via cp --preserve=timestamps; result notified via lib/notify.sh; media-sync added to INTERACTIVE_CMDS; deps guards (lsblk/jq) before -h|--help. Docs: POS.md media row, howto/media.md section, SCRIPTS.md lib section + Phase-2 lib list, system.env seams, DEV.md env-seam registry, AGENT_Context Common Tasks + hand-maintained lib row (+usb-lib 194) + gen'd tree/dispatch/filetable/flags. Verified: new stub suite /tmp/opencode/msync-run.sh 46/46 green (fresh/no-op/update/filter/dry-run/multi-stick/mount-offer/no-USB skip/never-delete/--source/TRAN-fallback/notify) — caught and fixed an inverted needs_copy return; make gen && make check, make lint 0 FAIL / 0 WARN; dispatch via pos media sync --help + pos media listing.
  • 2026-08-22pos media ytsync (bin/pos-media-ytsync) — incremental YouTube channel/playlist sync into ~/Videos, implemented per the Architect decisions D1D9 + Designer UX contract (reportAgents/2026-08-22-*.md). Subcommands add [url] / sync [name] / list / remove <name> + --dry-run; bare invocation = interactive menu (/dev/tty reads, EOF-safe, NOT in INTERACTIVE_CMDS so dispatcher tee logging is kept; empty state goes straight to the URL prompt). One yt-dlp call per new video (bestvideo*+bestaudio/best → MP4, metadata/chapters/thumbnail, --no-overwrites, --windows-filenames --trim-filenames 120, retries 3), per-video [n/N] title heartbeat lines, LF-only logs (spinner TTY-gated); probe = yt-dlp --flat-playlist -J parsed with jq, new-list diffed against the per-source --download-archive BEFORE downloads (exact counts, exact dry-run plans, zero speculative downloads). State machine-owned outside ~/Videos: $YTSYNC_STATE_DIR/{registry(\x1f-delimited slug⇥type⇥url⇥subdir⇥playlist_title⇥added_ts), archive/<slug>.txt, history.log}, atomic temp+mv writes; remove keeps files AND archive (re-add resumes incrementally); ?v=+&list= URLs download the single video only. Exit codes: 0 incl. no-op/cancel/non-tty-guard; 1 reserved for missing deps, invalid explicit URL, unknown/ambiguous name, wholesale source failure. Notify digest only when new>0 or failed>0 (+ ERR-trap alarm around download passes) via opt-in lib/notify.sh. Config scope ytsync: YTSYNC_VIDEOS_DIR / YTSYNC_EXTRA_ARGS (pos config ytsync); automation documented as a pos system schedule job (COMMAND=pos media ytsync sync, NOTIFY=never). Deps guards before -h|--help with yt-dlp+jq active under --dry-run (the preview IS the probe; ffmpeg skipped there). Docs: tools-docs/ytsync.md (new dir), POS.md media row + notes, HOWTO.md row, howto/media.md section + troubleshooting, AGENT_Context Common-Tasks row, bin/pos EXAMPLES line. Verified: make gen && make check && make lint 0 FAIL / 0 WARN; PATH-stub yt-dlp suite (add happy path, incremental 0-new idempotency, playlist NNN numbering, dry-run zero writes, non-tty guard rc0, remove-keeps-archive) with real $HOME byte-untouched via seams.
  • 2026-08-23 — ytsync post-review fixes (from reportAgents/2026-08-23-reviewer-ytsync.md, ACCEPT_WITH_NITS): classify_url now treats youtu.be/<id> short links (with or without &list=, incl. scheme-less + ?si= forms) as single videos — same path as ?v= — so they get type video + --no-playlist instead of being misfiled as playlists; usage() watch-link note reworded; tools-docs classification table gains the short-link row and the invocation block gains the previously undocumented --convert-thumbnails jpg; POS.md/howto media wording extended. Verified: classify_url matrix (6 URL shapes) + stub-PATH end-to-end add (registry type=video, download call carries --no-playlist + canonical watch URL); make gen && make check && make lint 0 FAIL / 0 WARN.