Files
Linux_post_install/AGENT_TODO.md
T

42 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.

Done

  • 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-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-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 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-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-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-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-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.

Now

  • (none — Tier 1 shipped: pos system health, lib/notify.sh, digest timer)

Next

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

Later — idea backlog

  • 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 (summary, newest last)

  • 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-06: pos 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-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> --help auto-discovery in the dispatcher.
  • 2026-08-05: AGENTS.md with lazy-loaded DOC references.