62 KiB
62 KiB
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, callnotify_sendon success/failure).
- Wire alerting into more tools as they are added (default: source
Later
- Tier 2:
pos healthextras — temperature/fan/load average thresholds,ss -tlnport checks for known services, SMART status for disks. - Tier 3: backup rotation + remote target — keep-N rotations, upload to
rclone remote after verify,
--remoteflag, digest reports rotation age. - Tier 3:
pos secretvault — 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 fromtemplates/pos-tool.sh(category, name, POS header, exec bit, doc stubs). - NOT NOW: per-category
bin/subdirectories — flatbin/+ filename dispatch scales fine; revisit only ifbin/passes ~40 files. - NOT NOW: split
lib/entertainment-lib.sh— fine under 600 lines; revisit if it grows.
Done
- 2026-08-16 —
pos media syncreview follow-up (onba12a41): (1) strip ALL trailing slashes —SRC="${SRC%/}"removed only ONE slash, and GNU find preserves a doubled one on the starting point (find -H /x// -type femits/x//Album/a.mp3), so--source /x///MEDIA_SYNC_SOURCE=…//still hit the original nesting bug (prefix/x//never matched); nowwhile [[ "$SRC" == */ ]]; do SRC="${SRC%/}"; done, which also collapses a lone/or//to empty → guard errs instead of mirroring the filesystem root. (2) partial-sync marker — the "find reported problems" condition is computed once (find_ok=1/0at the warn block) and reused: when set, the finalok "Sync complete: …"andnotify_send "Music sync completed: …"both append(partial — find reported problems)so the success signal can't contradict the warning (dry-run line untouched). (3) clear empty-source error — the post-normalization guard now saysSource path is emptyinstead ofSource not found:with a blank value; the-dguard keepsSource not found: $SRCfor non-empty missing paths. Docs untouched (ba12a41wording kept). Verified: harness extended (NOTIFY_LOGseam: telegram-sender stub appends its argv, run_sync passesNOTIFY_LOG; new §7--source …///…///+ env…//→ correct rel placement, noMusic/tmpnesting; §8--source /,//,''→ exit 1 + "Source path is empty"; §3 now asserts the partial marker on both the ok line and the notify log; §1 negative: no marker on clean runs) — full suite 51/51 green on the fixed tool, and against aba12a41snapshot it fails ONLY the follow-up assertions (double/triple-slash nesting ×5, partial markers ×2, empty-source message ×2; 8b--source //excluded from the pre-run because the old code would have mirrored/).make gen && make checkgreen,make lint0 FAIL / 0 WARN. - 2026-08-16 —
pos media synchardening (3 approved fixes): (1) trailing-slash source — with--source /x/(orMEDIA_SYNC_SOURCE=…/) GNU find normalizes the slash on the starting point, so the rel prefix"${f#"$SRC/"}"became/x//which never matched and every file silently nested under<stick>/Music//data/Music/...each run;SRCis now%/-normalized with a non-empty guard right after option parsing, before the-dcheck (covers flag + env forms). (2) silent find failure → false "Sync complete" — bothfind -Hinvocations ran through process substitution, which hides find's exit code and stderr fromset -e/pipefail: an unreadable subdir made find exit 1 with "Permission denied" yet the tool announced a full success on a partial tree. find now runs ONCE into a temp list + temp stderr (|| find_rc=$?, sorted in place withsort -o, temp files removed viatrap EXIT); rc != 0 or non-empty stderr prints a visiblewarn("results may be incomplete") with the find stderr lines indented (spawn-style) instead of continuing silently; both the space scan and the copy loop read the same captured list, so the double find scan is gone and the file set is identical. (3) inner symlinks silently skipped —find -Hfollows only the command-line source symlink, so symlinks inside the tree never synced; an inner-symlink count (find -H "$SRC" -mindepth 1 -type l, root symlink excluded) now warns "N symlink(s) inside the source are not followed (find -H) — their targets will not be synced".find -Hkept (no-L);needs_copymtime logic untouched (FAT32 granularity deferred). Docs: howto/media.md symlink paragraph notes inner symlinks are skipped with a count warning; AGENT_Context filetable regenerated (pos-media-sync 164→202). Verified: stub harness/tmp/opencode/media-sync-fix-test/(lsblk JSON fixture with rm=true/type=part/mountpoint/TRAN=usb,yconfirm,MEDIA_SYNC_SOURCE/MEDIA_SYNC_DEST/USB_BYIDseams, HOME isolation, telegram-sender stub) 32/32 green — trailing-slash--sourceand env forms land under<stick>/Music/<rel>with no absolute-path nesting, unreadable subdir → visible "results may be incomplete" warn + indented find stderr + exit 0 + accessible files still copied, inner symlinks → count warning, symlink root still works with NO inner-symlink warning (23d69b7regression incl. trailing-slash combo), re-run after success →0 added, 0 updated, 2 unchanged; the same suite against the pre-fix script fails exactly those assertions (24/32).make gen && make checkgreen,make lint0 FAIL / 0 WARN. - 2026-08-15 —
pos docker stack(bin/pos-docker-stack) — containers grouped by their Docker Compose project. Each stack is a section (project name, sorted) with linescontainer-name status ports; containers with no compose project land in aStandalonesection at the end; ends withStacks: N containers: N standalone: N. Running only by default,-a|--allincludes stopped/exited (likedocker ps -a). Status colored on a terminal (Up*green,Exited*/Dead*/Created*red,Paused*/Restarting*yellow); exit 0 also when no containers. Data viadocker pswith--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 withawk -F'\x1f'+IFS=$'\x1f' readeverywhere — tab/pipe delimiters are IFS whitespace or inside values, so\x1f(DEV.md:213 gotcha); dash padding viasednottr(tr corrupts multi-byte─). Deps guard (docker) before--help; no stdin → not inINTERACTIVE_CMDS;# POS_FLAGS: -a --all. Docs: POS.md docker row + detail, howto/docker.md table + section,bin/posusage EXAMPLES, AGENT_Context §14 row. Verified: stub-PATH suite/tmp/opencode/docker-stack-test/run-tests.sh23/23 (grouping, sorted stacks,-ashows exited, standalone, empty daemon rc=0, colored status, missing docker rc=1,--helpafter deps guard); live runs against the real daemon (affine/audiobookshelf/convertx/gitea stacks,affine_migration_job Exited (0)+lab1 Exited (137)under-a); dispatch viapos docker stack;make gen && make check,make lint0 FAIL / 0 WARN. - 2026-08-15 — Fix
pos media syncoffering a Ventoy stick's EFI partition as the sync target: with the data partition unmounted, the 32 MBVTOYEFIESP was the only mounted USB partition,usb_detectoffered it with no context, andcpdied mid-copy withNo space left on device(live-box report).usb_detectnow fetchesFSTYPE/PARTTYPENAMEand excludes EFI system partitions (VentoyVTOYEFI,/boot/efi) from both the mounted list and the mount-offer list;USB_MOUNTEDentries carrymp|label|size|model|fsandusb_pick_rootshows that in the single-stick confirm and the multi-stick/partition picker (1) /media/Ventoy (1.1T, Ventoy, exfat)), whileUSB_ROOTstays a bare mountpoint (${root%|*}) sopos system backup(${root%/}/backups) is unaffected.pos-media-syncgained a pre-flight space check (measures exactly whatneeds_copywould copy vsdf -Pk,err/warnbefore 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-teststill green; live checkprintf 'n\ns\n' | bash bin/pos-media-sync --mp3no longer offers VTOYEFI (offers unmountedsda1Ventoy instead);make gen && make check,make lint0 FAIL / 0 WARN. - 2026-08-15 — Fix
pos media syncreporting success with 0 files when the source is a symlink: it enumerated with plainfind "$SRC", and GNU find (default-P) does not descend a command-line symlink to a directory —~/Music -> /mnt/hdd/…/musictherefore yielded zero matches, the loop never ran, and the tool printed0 added, 0 updated, 0 unchangedwithout creating the target dir (live-box report). Switched tofind -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-runnow lists all 31 mp3s as "would copy";make gen && make checkgreen. - 2026-08-14 —
pos 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-05 —
pos communication telegram—--parse-mode(plain/markdown/html). - 2026-08-05 — doc/code sync gate —
make gen+make check+ pre-commit hook. - 2026-08-05 —
pos usb server— USB Redirector control tool (494eae2). - 2026-08-05 —
pos <category> --helpauto-discovery in the dispatcher. - 2026-08-05 — AGENTS.md with lazy-loaded DOC references.
- 2026-08-06 — Fix entertainment timer
1hnot firing —interval_to_oncalendaremitted invalidOnCalendar=*-*-* */N:00:00(systemd rejects*/Nin 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_blockremoved),statussimplified,Ndintervals rejected with a clear error. - 2026-08-06 — Nested
possubcommands —# POS_SUBCMDS:header annotation (telegram, docker-compose, docker-vbox) +make genemits a_pos_subcmdscompletion map; nested tools (telegram listener) auto-list under their parent instead of as a flat sibling (telegram-listener) inpos <category>and tab-completion; generic tool-level completion (subcommands + flags +--help). - 2026-08-06 — Telegram listener —
pos 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 system—pos 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.mountunits ordered after network-online.target, no fstab);nfs-kernel-server+nfs-commonadded to preinstall PACKAGES. - 2026-08-06 —
posHOW-TO guide set —DOC/HOWTO.mdindex + per-categoryDOC/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.shroutes viaNOTIFY_PLATFORM(notify.env, default telegram; sender contract for Matrix/Synapse later),system.envshared config for health/backup, dynamic effective values in--help, telegram--markdownalias. - 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-07 —
pos system health --sendnotification-only; listener@quietprefix (run mapped command without replying, for commands that self-notify)./status=@quiet pos system health --send= exactly one digest. - 2026-08-09 — Matrix/Synapse
communicationtools —pos communication matrix sender+listener, completing the second notify platformlib/notify.shwas designed for (NOTIFY_PLATFORM=telegram,matrixfan-out; the sender implements thesend <value> [--markdown]contract vianotify_sender_name()'s default key→tool mapping, no lib changes). Sender (bin/pos-communication-matrix-sender):send <value> [--markdown] [--room <id|alias>]PUTsm.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,--markdownsendsorg.matrix.custom.htmlvia 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→ savesaccess_token+user_id);test. Config scopematrix(~/.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-syncsincetoken, compact filter dropping presence/account_data/device noise,m.room.messageonly); reacts toMATRIX_USER_ID's own messages (resolved via/account/whoamiif unset),MATRIX_ROOM_IDrestricts to one room;/and!both resolve; replies threadedm.in_reply_to;@quietno-reply marker;/cmd::desc=…map descriptions;ai …bridge (pos ai gemini ask, per-room sessionmatrix-<room>,ai /resetclears, markdown stripped); interactive editor (--status/--enable/--disable/--run), 60s command timeout, exit-code prefix, ~3800-char truncation.communication-matrix-listeneradded toINTERACTIVE_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/posusage EXAMPLES;make gen && make checkgreen. Verified against a mock homeserver: send plain/markdown/--room/test request shape (URL-encoding, Bearer auth, JSON body), login token save, listener owner-filter +/statusreply +/help+@quietsilence + non-zero exit reply + interactive editor add. — state-based threshold rule monitors (eventer). Each line of~/.config/linux_post_install/event.envis an independent rule:["<msg>" if ] <check-command> <op> <threshold>(op> < >= <= == !=, unit suffix ok60c/80%). The check command is run on every pass and its first numeric output compared float-safe; operator detected as the rightmostop thresholdpair 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 timerpos-event-trigger.timer+ oneshot service;5m…weeklyorOnCalendar=…; graceful warnings when no user systemd manager,loginctl enable-lingerattempt),disable,status.--dry-runhonors the DEV.md dry-run convention. Alerts vialib/notify.sh(Telegram default; other platforms viaNOTIFY_PLATFORM). New:bin/pos-system-event-trigger,lib/eventer-lib.sh,config/event.envtemplate (installed no-clobber by postinstall),lib/eventer-lib.shinstalled by install.sh,system-event-triggeradded toINTERACTIVE_CMDS, usage EXAMPLES row. Docs: POS.md system row, HOWTO.md index row, howto/event-trigger.md;make gen && make checkgreen; 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-09 —
pos media mp3/mp4hardened + smart format selection. Both tools: yt-dlp calls go throughspawn(honor$DRY_RUN;--dry-runprints 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-Ftable ([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 checkgreen. - 2026-08-09 — Telegram
ai …now answers about a message you reply to: the listener extractsreply_to_message.text(falls back tocaption) from each update and passes it tohandle_message; the AI bridge prefixes the prompt with[Reply context — the message you are replying to]. So replying to a/statusoutput and askingai check this detailsgives the model the actual output. Applies only to the AI bridge (mapped/commandsuntouched); reply context rides in the user turn so the session records what was analyzed. Docs: howto/ai.md bridge section. - 2026-08-09 —
pos ai geminisessions + Telegram-friendly replies.--session <name>givesask/chatpersistent memory (~/.local/share/linux_post_install/ai/<name>.json, capped at 40 turns, pruning keeps the first user turn as scene); newsessionssubcommand (list /reset <name>). Telegram listener now keeps one session per chat (telegram-<chat_id>) withai /resetto clear. New--system "<text>"flag injects a GeminisystemInstruction(viajqmerge) 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 beforesendMessage, since messages go out as plain text. Docs: howto/ai.md (flags, sessions, bridge memory/formatting),make gen && make checkgreen. - 2026-08-09 — Fixed
pos configsecret-value corruption:cfg_read_secret's cursor-advanceechowent to stdout and, since the function is called via$(...), a leading\nended up inside every secret value → the env file gotAI_GEMINI_API_KEY="\n<key>", unreadable bycfg_value/load_config(menu showed(not set),pos ai geminidemanded a key). The newline now goes to the terminal (echo >&2). Defense in depth:cfg_write/write_config_keystrip CR and truncate multi-line pastes (warn),cfg_valueand the ai/telegramload_configs strip CR on read. Verified on a real PTY (piped tests couldn't reproduce — non-TTY stdin skips the echo path). - 2026-08-09 —
aicategory —pos ai gemini(ask/chat/models) via Google Gemini REST API.askprints only the answer (pipe/script/Telegram-friendly),chatis a multi-turn REPL (q/quit/Ctrl+C,/reset, empty input re-prompts),modelslists generateContent-capable ids and flags the default;--modeloverride; defaultgemini-2.5-flash. Config scopeai(AI_GEMINI_API_KEYsecret +AI_GEMINI_MODEL) in~/.config/linux_post_install/ai.env, edited viapos config ai;config/ai.envtemplate installed no-clobber by postinstall;ai-geminiadded toINTERACTIVE_CMDS. Telegram listener now answers non-command messages starting withaiviapos ai gemini ask(owner chat only; error replies carry thepos config aihint) — future intents (reminders) slot in as more case arms inhandle_message. Docs: POS.mdaisection + listener bridge, howto/ai.md, HOWTO/README index rows,bin/posusage example. - 2026-08-09 — Entertainment plugins
gold+weathernow 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-09 —
pos tree: prints the liveposcommand tree (categories → commands → subcommands) by deriving the hierarchy frombin/pos-*filenames +# POS:/# POS_SUBCMDS:headers, so it always matches what the dispatcher can run. Category-less likepos-config;--depth Nlimit;pos help treeworks. Docs: POS.mdtreesection,bin/posusage example,make genregenerated the AGENT_Context tree/dispatch/filetable +_pos_flags[tree]. - 2026-08-09 — Telegram sender
config/config setremoved — redundant withpos 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/commandvalues containing|are no longer truncated (load_mapswitched from a|to a\x1fdelimiter — previously/up=echo hi | headsilently ranecho hi);pos entertainment send <plugin> [args…]actually forwards the extra args (every arg wasshifted 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) andcfg_write(config-ui) replaced unescapedsed -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_systemddaemon-reloads after removing timer units;digitsconfig validation accepts negative group/supergroup chat ids (-100…). - 2026-08-09 — Fixed telegram listener editor crash on remove/edit/test:
ui_pickprinted its menu listing to stdout, soidx="$(ui_pick)"captured the menu and the number, andMAP_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::descwork), exposed by the description column. - 2026-08-09 — Telegram listener pushes its mapped
/commandsto the bot's/menu viasetMyCommands(auto after every map edit, on--enable, and at daemon start; manual--sync-commandsflag). 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(awkEND{exit 1}overrode the match), andwarn()went to stdout so it leaked into the generated JSON (now stderr). - 2026-08-09 —
pos config <TAB>scope completion is now cached at gen time (_pos_config_scopesarray emitted bymake genfrom 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-bashsessions (69%/38% CPU) killed.plugin_marker/plugin_keyshardened with|| truesoconfig_keysno longer aborts mid-scan underset -euo pipefailon mixed lib/plugin dirs (installed layout) — fixes missing plugin keys inpos config entertainment. - 2026-08-09 —
pos 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;*pluginsmarker expands plugin vars (entertainment) fromentertainment-lib.sh;desc::examplevalue-format hints shown in the editor; gen-docs now handles category-less tools (pos-config), fixed aset -e+pipefailbug that truncated the header registry. - 2026-08-09 —
pos-communication-telegram→pos-communication-telegram-sender: one canonicalsend(dropped the legacy--sendflag, which duplicated thesendsubcommand in completion).pos communication telegram <TAB>now completes to justsender listener.lib/notify.shmaps platformtelegram→telegram-sendervianotify_sender_name(); entertainment-send + health--sendcheck updated. Removed phantom subcommands from howto/communication.md (webhook/logs/broadcast/file never existed). - 2026-08-09 — Structure/convention audit fix:
--dry-runnow truly dry (spawn()honorsDRY_RUN, install.sh exports it to child phases, postinstall mutations run-wrapped);gen-docs.shno longer chmods regenerated files to 0600;make checknow syntax-checks apps/entertainment/features/templates;.gitignoreprotectsconfig/authorized_keys+config/rclone.conf; honest--sendconfirmation; 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_DIRprecedents — + stub-PATH fakes + PTY prompt driving viascript); new Best Practice "Managed Config Blocks" (start/end marker idiom incl. theinblock == 1awk guard, validate-then-apply, hot reload); deps-guards-run-before---helpmade 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 6–7 mirror the above.make gen && make checkgreen. - 2026-08-11 —
sharecategory 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),testparmvalidation before apply +smbcontrol smbd reload-confighot reload;--read-only/--guest/--users u1,u2flags with unrestricted-share warnings;smbpasswduser 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);persistwrites a systemd.mountunit (systemd-escape) withx-systemd.automount+_netdev— mounts on first access, never blocks boot. Both sourcelib/notify.shfor mutations; added toINTERACTIVE_CMDS(prompting subcommands). Deps:samba+cifs-utilsadded to preinstall PACKAGES.SMB_CONF/SMB_CREDS_DIR/UNIT_DIRenv-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 checkgreen; logic tested via stubbed PATH + temp config (marker idempotency, guest + user persist flows). - 2026-08-11 —
pos network checkportnmap overhaul: two-pass engine — pass 1 = fast-Pn -T4 --max-retries 1scan of only the asked ports (was: all 65535) with per-port state + nmap service names; pass 2 (--versions, opt-in) =-sV --version-lighton 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-sUrequires root and quit outright); IPv6 hosts get-6;no output/filtered states set rc=1;--timeoutscales nmap host-timeouts. New--versionsflag in# POS_FLAGS:(completions regenerated) + usage text; port-metadata fallback retained.make gen && make checkgreen. - 2026-08-11 —
pos communication matrix sender loginerror reporting: captures HTTP status + Matrixerrcode/errorfrom the JSON body (temp file, not stdout) instead of a generic "wrong credentials?" message — distinguishes unreachable homeserver from rejected credentials; auto-prepends@when--useris bare (e.g.--user alice:example.org→@alice:example.org). - 2026-08-11 — New
sharecategory —usbandnfsmoved out ofpos usb/pos systemintopos share:pos share usb server(waspos-usb-server),pos share nfs server+pos share nfs client(werepos-system-nfs-*). Renamed the three tools (bin/pos-share-*), updated# POS:headers/usage strings,INTERACTIVE_CMDS(usb-server→share-usb-server),bin/posusage() EXAMPLES, and the notify-scope comment inpos-system-backup. Docs: newDOC/howto/share.md(USB + NFS consolidated;howto/usb.mddeleted, NFS sections stripped fromhowto/system.md), POS.md### sharesection (replaces### usb, nfs rows moved out of### system), HOWTO/README indices, AGENT_Context hand-written spots, root README, DEV.mdINTERACTIVE_CMDSexample, AGENTS.md categories. Category is the home for futuresmb.make gen && make checkgreen;/usr/local/binrefreshed. - 2026-08-12 —
pos network download(bin/pos-network-download) — aria2 JSON-RPC daemon + queue control. Daemon: persistentaria2cas 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, generatedRPC_SECRETin~/.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](base64addTorrent),metalink <file|url> [--tmux],list(active/waiting/stopped table),info/files/peers <gid>,pause|resume|remove [gid|all](--force→force*),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).--tmuxopens a detacheddl-<name>session runningwatch <gid>(name from--out/URL basename, sanitized, 40-char truncate,-2on collision; closes itself on completion). Deps:aria2c/jq/curlguards before--help;aria2added to preinstall PACKAGES. No stdin → not inINTERACTIVE_CMDS. JSON built withjq -nc --arg(never string interpolation — fixes JSON-quote bugs);# POS_SUBCMDS:(18) +# POS_FLAGS:→ completions. Test seamsRPC_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 checkgreen. - 2026-08-12 —
pos network downloadgrows outage resilience:restart <gid>(re-queue from history — torrents via rebuilt magneturn:btih:+&tr=trackers, HTTP via original URIs withdir/outpreserved,--continue=trueresumes partials; options--dir/--seed/--split/--tmux),retry <gid|all>(smart retry — waits out internet outages viaNET_PROBEseam, re-queues,retry_verifypolls the new gid; aria2 error 3 = real problem → diagnosed + marked permanent in~/.config/linux_post_install/download.retryasurl:<uri>/bt:<infohash>,retry allskips them, manual restart overrides;--once/--quiettimer mode;--interval/--max-wait), and the retry healer systemd user pair (pos-aria2-retry.serviceoneshotretry all --once --quiet+pos-aria2-retry.timer2min,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_healerwas missing from the three submit paths;RESTART_NAMEwas lost acrossdo_restart's process-substitution subshell (now adownload_name()helper);restartexited 1 because the[ tmux -eq 1 ] && tmux_watchtest was the function's last statement. Verification: stub-based test harness (/tmp/opencode/dl-test— curl/systemctl stubs with tellStatus fixtures,NET_PROBEfile-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 checkgreen. - 2026-08-12 —
pos network download replace <gid> <url>+ fresh-link status advisory.statusnow flags stopped errored downloads whose source is marked permanently failing indownload.retry(needs fresh link: <name> (<gid>) — pos network download replace … <new-url>; onetellStoppedRPC, id-match in jq).replace <gid> <url>re-queues a dead single-file HTTP/FTP download with a new URL keeping the samedir+ file name (partial resumes via--continue=true), unmarks the old source (retry_unmark, literalgrep -vxF— URL-safe), and reusesretry_verifyso a dead replacement link is diagnosed + marked permanent; torrents/active/multi-file are rejected with hints;--dir/--split/--tmuxsupported.retry_verifyhardened to${quiet:-0}so it works outsidecmd_retry. Stub suite grew areplacesection (advisory match, success + unmark + advisory-clear, dead new link marked, torrent/active/arg errors, prefix gid) — tellStopped fixtures gaineduris(real aria2 includes them). 141/141 green; docs: POS.md row + outage paragraph, howto/network.md dead-link recipe.make gen && make checkgreen. - 2026-08-12 —
pos system event-trigger(eventer) generalized intopos system schedule— the scheduler replaces the single-timer threshold monitor with per-job systemd user timers (pos-schedule-<name>.{timer,service},Persistent, ExecStartrun <name>, reconciled onenable/disable— orphan units + the legacypos-event-triggertimer 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=…),NOTIFYpolicy, optionalMSG,RULE(threshold only), andCOMMAND= literal remainder of the line (pipes/quotes/sudoneed 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 vsRULE, 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 legacyevent.envrules →schedule.d/rule-N.envthreshold jobs, adopts the legacy timer's OnCalendar or 5m, removes the old timer). Files:bin/pos-system-event-trigger→bin/pos-system-schedule,lib/eventer-lib.sh→lib/scheduler-lib.sh(git mv; installed by install.sh),config/event.env+config/event-rules.template→config/schedule.d/starter jobs (nvme-health viasudo -n smartctlwith the user's exact grep — sudoers NOPASSWD documented; cpu-temp + disk-root thresholds; silent log-cleanup), postinstall installs them no-clobber into an emptyschedule.d/(legacyevent.envusers get a migrate hint instead).bin/posEXAMPLES + 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 checkgreen; stub-harness suite (fakesystemctl/sudo/smartctl/sensors/df+ fake telegram sender logging, env seamsSCHEDULE_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-13 —
pos communication scrcpyaudio control: scrcpy already forwards device audio to the desktop by default (answer: yes, default is sound-to-desktop). AddedSCRCPY_AUDIOconfig key (pos config scrcpy, defaulttrue):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 checkgreen. - 2026-08-13 —
pos communication scrcpy --new-displaysupport: newSCRCPY_NEW_DISPLAYconfig key (pos config scrcpy) —true/yes→ bare--new-display(default size/dpi),1920x1080,1920x1080/420or/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-throughpos communication scrcpy --new-display=1920x1080also 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 checkgreen. - 2026-08-13 — Fix
pos communication scrcpymirror failure on the real box (ERROR: Unexpected additional argument:on every mirror, bare or with flags):_extra_flags()ranprintf '%s\n'with an empty array expansion, which prints one blank line;_mirror()'swhile readturned that into an empty-string arg passed to scrcpy. Fix:_extra_flagsnow returns early whenSCRCPY_EXTRA_FLAGSis empty (andprintf '%s\n' "${extra[@]}"when set), and_mirrordefensively 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 checkgreen. - 2026-08-13 — Fix scrcpy apt install on the live box: preinstall
apt installfailed withUnable to locate package scrcpy(Debian/Ubuntu need contrib/universe forscrcpy, and the apt build is older anyway). Removedscrcpyfrompreinstall.shPACKAGES (keptadb);scrcpynow installs via the existing optional appapps/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 checkgreen. - 2026-08-13 —
pos communication scrcpy(bin/pos-communication-scrcpy): wrapper over scrcpy+adb for Android mirroring/control. Subcommands: barescrcpy(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-playbackfor headless servers),tcpip [port](USB→wireless switch + printsconnectwith 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 scopescrcpy(~/.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. Depsscrcpy+adbadded to preinstall PACKAGES; docs note the apt build is older and point to the existingapps/media/scrcpy.shapp 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.sh21/21 green (fake adb/scrcpy echo-args, HOME isolation, env/file precedence, rc paths, screenshot bytes),make gen && make checkgreen, dispatch viapos 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 followsNOTIFY_PLATFORM, default Telegram) vialib/notify.shsourced bybin/pos-entertainment-send; a last-run state is recorded per plugin (~/.local/share/linux_post_install/entertainment/last/<plugin>— rc + timestamp) on every non---printrun and shown bypos 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 liblib/entertainment-plugin-lib.sh(defines onlyplugin_*, 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/goldrefactored onto it.pos entertainment configgainsget|unset|ls|edit(edit via the sharedpos configUI — added toINTERACTIVE_CMDS). Tier 2: new shared liblib/user-timers-lib.sh(onlyut_*:ut_interval_to_oncalendar,ut_interval_label,ut_unit_name,ut_write_unit_pairincl.TimeoutStopSec=5s+Persistent+ network-online deps,ut_ensure_linger,USER_SYSTEMD_DIR) dedupes the systemd user-timer machinery betweenlib/entertainment-lib.shandlib/scheduler-lib.sh(the latter'ssched_*duplicates deleted; both source it; collides-with-nothing).install.shPhase 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 -neverywhere; smoke-tested in an isolatedHOME=/tmp/enttest(status, config get/set/unset/ls, send path rc=0, failing plugin records rc=1, error-case message hygiene);make gen && make checkgreen. - 2026-08-13 — Fast pos-unit shutdown: every systemd unit a pos tool writes (or
systemd/ships) now setsTimeoutStopSec=5s(+KillMode=control-groupon 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 atrap 'kill $(jobs -p) 2>/dev/null; exit 0' TERM INTinrun_daemonso stop returns sub-second),pos-network-download(aria2 + retry-healer units),lib/scheduler-lib.shsched_write_units,lib/entertainment-lib.shwrite_units, andsystemd/{ssh-agent,autostart,usb-automount}.service. Legacy-unit cleanup: the repo no longer shipspos-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 thepos-health.servicesection + gating special-case, added a new Stop behavior section), POS.md, HOWTO.md, howto/system.md (now documents thepos system schedulejob 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 -non all edited scripts;make gen && make checkgreen (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 viapos network download start,pos communication telegram listener --enable,pos system schedule enable <job>,pos entertainment enable <plugin>, thensudo 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 redundantapt update(preinstall.sh owns it — install.sh previously ran it twice, showing two identicalOK apt updatelines); 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-featurefeature installed/overwritten+feature flag setlogs with aN features installed: …summary — and the misleading"47 scripts + libs"label is fixed to47 scripts + 6 libs(the 6 libs were outside the counter); preinstall printsInstalling 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-serviceservice enabled: <name>line. No output-layer changes (no--verbose, no log file — decided scope). Verified:bash -n+--dry-runsmokes of phases 1/2/3 showing every new line (learned:install.sh:19hardcodesexport DRY_RUN=0, so an envDRY_RUN=1is ignored — the flag--dry-runis required), hand-maintained filetable count rows bumped (install.sh 206→223, preinstall.sh 73→75, postinstall.sh 163→168),make gen && make checkgreen. usb-automount left live (user choice). - 2026-08-13 —
usb-automountfeature, integrated exactly likeautostart: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.rules—ACTION=="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; scanslsblk -Jfor 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 + manualsystemctl 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 forpos 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_DIRseams, HOME isolation): 47/47 green.make gen && make checkgreen. 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-13 —
pos system backupcopies 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 —sskips, 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 600best-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 pinnedBACKUP_USB_ROOTseam (=<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 checkgreen. - 2026-08-13 —
pos share smb-server sharenow guards the two commonNT_STATUS_ACCESS_DENIEDcauses at share time (warnings only):--usersentries missing from the Samba passdb (pdbedit -L, cut to user column,grep -qxFper user — pointer topos share smb-server adduser <user>), and ancestors of the share path lackingother:+xtraversal (sticky dirs like/tmpcount as traversable via thetslot; fix hintchmod o+x <dir>). Both wired into thesharecase afterrequire_root_dir; howto/share.md SMB section + troubleshooting updated. Rooted inreports/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_CONFseam): 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'sSCHEDULE_*seams, and documents the missing-:--guard gotcha (aVAR="${XDG…:-…}"without leadingVAR:-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 withVAR=/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 thatmigratecopies the rule LHS verbatim asCOMMAND(old tool never haddisk root/loadavgshorthands — rewrite those jobs with real commands).make checkgreen. - 2026-08-14 — Gitea Actions gate is now live and green end-to-end: act_runner (v0.6.1, labels
ubuntu-latest) registered on100.100.1.2(~/srv/gitea/runner/, standalone compose next to the ScaleTail gitea;CONFIG_FILE=/config.yamlenv required orrun.shnever reads the config;--add-host gitea.skink-platy.ts.net:100.111.241.54so the job container reaches gitea). First real runs caught a deterministic gen-drift: plainsortinscripts/gen-docs.shis locale-dependent (category-less tool keys start with|, which collates after letters under the CI container's locale →pos-config/pos-treereordered), so thegit diff --exit-codestep failed. Fixed withexport LC_ALL=Cin gen-docs.sh (byte-order sort) + regeneratedDOC/AGENT_Context_Project.md(config/tree now sort after the letter categories);make checkOK,make lint0 FAIL / 0 WARN. Live CI verdicts: the run fore0b5b11(workflow commit) and the empty trigger98a767cboth FAILED on the drift; the run for9d058b7(the fix) SUCCEEDED (🏁 Job succeeded). - 2026-08-14 — Gitea Actions gate added:
.gitea/workflows/lint.ymlrunsmake gen+git diff --exit-code(gen-drift) +make check+make linton 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; gatescripts/lint-conventions.sh+make lintnow 0 FAIL / 0 WARN;make gen && make checkgreen). P0 bugs: M-002/003/004 addeddocker-compose docker-vbox network-hotspottoINTERACTIVE_CMDS(stdin/log-pipe prompt swallow); M-005install.sh --stepsnow expands documentedN-Mranges vianormalize_steps_spec()(dry-run verified); M-006 feature-vs-docs decision:--send/--markdownnot restored (health is a console-only reporter by design since fe7708f; schedulerNOTIFY=alwayscovers delivery) — 5 docs corrected instead; M-007lib/notify.sh:57fallback routed to stderr (stdout-leak on standalone source). P1: M-008..M-014 deps guards moved before-h|--helpin docker-health/docker-ps (converted tocommand -v X || err), network-scan, share-usb-server, media-mp3/mp4 (guards before help with a--dry-runpre-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_lineonly matches real guards;first_lineskips comments; precision fixes, not weakenings); M-015 system-firewall gainedusage()+-h|--help(root-gated first; verified via sudo); M-016ffmpegadded to preinstall PACKAGES. P2: M-017/M-018 autostart + usb-automount gained the feature-template preamble (flags.sh load, usage); M-019chmod +x apps/media/scrcpy.sh; M-020SCALE_DIR/CONFIG_ENV:-seams in pos-docker-compose (verified via overrides; follow-on fix:DIMcolor var missing from common.sh crashedpos docker compose config— added it); M-021CONFIG_DIRcentralized 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-022plugin_*prefix collision resolved by renaming the internal registry helpers toent_plugin_*(the documented plugin-authoring APIplugin_have/plugin_require/plugin_load_config/plugin_http_jsonkept 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 linttarget wired in the Makefile.MAINTENANCE.mdkept as the working record (uncommitted by design). - 2026-08-14 —
pos system backupoptional encryption (--no-encryptflag +BACKUP_ENCRYPT=0env, flag-or-env — user chose "Flag + env only"): plain path keeps a verified.tar.gzwith 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 requiregnupg). Arg parsing rewritten as a loop over"$@"sopos system backup <folder> --no-encryptworks with the flag after the folder; usage() documents all three forms + the plain artifact name;# POS_FLAGS: --service --no-encrypt;config/system.envtemplate 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 lint0 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.ymlscoped toon: 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_SHAon success /ci-fail/$GITHUB_SHAon failure, pushed over HTTP with the jobs automaticGITEA_TOKENtohttp://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.conclusiondecides ok/fail,if: always()(guarded topushevents) covers failed gate runs, and an existing-tag guard makes re-runs idempotent. New executablescripts/ci-status.sh [--wait] [<sha>]reads the tags viagit ls-remote(origin,CI_STATUS_REMOTEoverride): GREEN (0) / RED (1) / PENDING (2);--waitpolls 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 lint0 FAIL / 0 WARN; first live-tag verification pending the push (fallback if Gitea clamps token-push: PAT as workflow secret). - 2026-08-14 —
pos media sync(bin/pos-media-sync) — incremental Music → USB sync, plus the shared USB layer it builds on. New liblib/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-automountscheme),usb_pick_root <prefix> <subfolder> <giveup-msg>(detect → mount-offer → single/multi picker →USB_ROOT); seamsUSB_MOUNT_BASE/USB_BYIDwithBACKUP_MOUNT_BASE/BACKUP_USB_BYIDaliases so existingsystem.envlines keep working; TRAN-fallback warning deduped to once per scan.pos-system-backuprefactored 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/--mp4filter (neither = both),--source <dir>(defaultMEDIA_SYNC_SOURCE/$HOME/Music),--dry-runpreview with counts; copies missing/changed (size/mtime) files into<usb>/Music/(MEDIA_SYNC_DEST) preserving the tree viacp --preserve=timestamps; result notified vialib/notify.sh;media-syncadded toINTERACTIVE_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.sh46/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 invertedneeds_copyreturn;make gen && make check,make lint0 FAIL / 0 WARN; dispatch viapos media sync --help+pos medialisting.