Files
Linux_post_install/AGENT_TODO.md
T
Your Name df1cca478d
gates / consistency-and-conventions (push) Successful in 23s
fix: Telegram listener — async command execution + singleton guard
Root cause: run_and_reply() blocked the entire listener synchronously.
FFmpeg hung because child processes inherited stdin (waiting for 'q').
Long-running commands froze the listener for up to 120s.

Fix:
- Commands run in background with stdin=/dev/null, output to temp file
- reap_commands() collects output non-blocking after each getUpdates cycle
- SIGCHLD handler pre-caches exit codes via wait -n
- TERM/INT trap kills background processes and cleans temp files
- Singleton guard (flock) prevents duplicate listeners racing getUpdates

Tests:
- t-telegram-listener-exec.sh: 12 hermetic checks (echo, pipes, stderr,
  compound commands, long-running, quiet mode)
- t-telegram-listener-singleton.sh: 8 checks (lock acquire/release/status)

Architect verdict: accepted as-is, no re-architecture needed.
2026-09-09 17:17:56 -04:00

102 KiB
Raw Blame History

AGENT_TODO — Worklist & Idea Backlog

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

Conventions

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

Now

Next

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

Later

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

Done

  • 2026-09-09 — Telegram listener single-instance guard (Toolsmith): bin/pos-communication-telegram-listener --run now takes a flock(1) on ${XDG_RUNTIME_DIR:-/tmp}/pos-telegram-listener.lock inside run_daemon() (before config load/sync/poll loop) — a second --run on the same token fails fast (exit 1, ERROR: listener already running (single instance) — check: systemctl --user status pos-telegram-listener), never racing getUpdates (Telegram 409/command stealing). Kernel auto-release → no stale-lock bookkeeping, systemd Restart=always restarts clean. --status first line now reports listener: running (single instance lock held) / listener: not running via the same lock_held() probe. flock dep guard added (util-linux). New regression tests/t-telegram-listener-singleton.sh (7 checks: first acquires+loops, second exits 1 with exact message, lock releases → third starts clean, status reports both states; stubbed curl/systemctl, sandboxed XDG_RUNTIME_DIR — hermetic, no network). Verified: bash -n, make gen ×2 byte-idempotent, make check OK, make lint 0 FAIL / 0 WARN, make test green, git diff --check clean.

  • 2026-09-09 — Unified YouTube tools into pos media yt + new subtitles (Architect POS--9). New bin/pos-media-yt dispatcher (mp3/mp4/grab/ytsync/subtitles) + bin/pos-media-yt-{mp3,mp4,grab,subtitles,ytsync}; the ytsync file is a forwarder to the existing pos media ytsync; legacy bin/pos-media-{mp3,mp4,grab} became thin forwarders to the yt forms. New lib/yt-lib.sh (deps/URL-validation/echo/classify helpers; classify_url migrated from grab, yt_validate_url is a return-1 checker — never exits, so callers can prefix errors). bin/pos INTERACTIVE_CMDS += media-yt-mp4 (interactive format pick reads stdin). pos-media-yt-subtitles extracts captions via --write-subs --write-auto-subs --sub-langs best, --lang en,ar (one --sub-langs arg), srt|vtt|txt (txt = srt→txt conversion stripping timestamps/HTML), --auto-only, --list-subs probe, --output, no-ffmpeg dep (yt-dlp only; dry-run skips deps entirely). Docs: DOC/POS.md media section rewritten (yt group + forwarder rows), DOC/howto/media.md yt commands + subtitles section, AGENT_Context hand-maintained lib/yt-lib.sh row, tests/README row. New tests/t-pos-media-yt.sh (72 checks: dispatcher/forwarder resolution, full pos media mp3 dispatch chain, yt-lib helpers, per-tool flags/dry-run/YT_OUT_DIR seam/GRAB_DEFAULT config, 3 mandated negative controls — unsafe-URL no-expansion, --lang en,ar single arg, txt timestamp-stripping, unavailable-subs detection). tests/t-config-precedence.sh Part D config-consumer list updated pos-media-grabpos-media-yt-grab. Verified: bash -n all; make gen ×2 byte-idempotent; make check OK; make lint 0 FAIL / 0 WARN; make test 21 files / 533 checks / 0 fail / 0 skip; git diff --check clean; smokes — pos media yt --help, yt mp3/mp4/subtitles --help, yt ytsync --help (reaches pos media ytsync), pos media mp3 --help forwarder, pos tree shows the yt subtree (with repo-first PATH; system /usr/local/bin has a stale pre-POS--9 install that shadows it otherwise).

  • 2026-09-08install.sh version gate (Architect→Builder): skip+abort when installed version == current version, --force to bypass, version scheme 0.0c<git commit count> (auto-bumps per commit). install_version() derives 0.0c$(git rev-list --count HEAD); empty when .git absent → gate skipped (silently); INSTALL_VERSION_OVERRIDE env var (presence-check) = test seam. Gate after arg-parse, before phases, numeric comparison (strip 0.0c, -eq); log "Already installed ($CURRENT_VERSION). Use --force to re-install." / --dry-run(dry-run) Would skip install: already at version $CURRENT_VERSION, both exit 0. FORCE=0 init, --force parse + usage. flag_set installed_version "$CURRENT_VERSION" after "Bootstrap complete" banner (only when DRY_RUN≠1 and version non-empty; even under --force). New tests/t-install-version.sh (21 checks / 9 cases). Docs: README/SCRIPTS/AGENT_Context (flags, flow, line count 248→301, tests/README row). Verified: bash -n clean; make gen idempotent; make check OK; make lint 0 FAIL / 0 WARN; make test suite green.

  • 2026-09-07pos ai API-key contract mismatch fixed (Architect→Builder→Reviewer; docs/history evidence): docs claimed AI_API_KEY was the required primary key, but resolve_key() read only provider-specific keys (7ae2e77 had removed shared-key priority to fix cross-provider leakage; docs never updated). Decision C: provider key stays primary (leakage guard intact), legacy AI_API_KEY honored as backward-compat fallback when the provider's own key is empty; cmd_providers() "configured" mirrors it; require_key() messages byte-stable; AI_API_KEY NOT re-added to # POS_CONFIG:/# PROVIDER_CONFIG: registry. Docs reworded (POS.md rows 91/96/98 + precedence, howto/ai.md first-run hints, HOWTO.md, AGENT_Context 2 prose spots, config/ai.env comment). New regression tests/t-ai-key-resolution.sh (24 checks / 10 cases: gemini+openrouter via provider key only, via AI_API_KEY only, both→provider wins, env-wins, llamacpp no-key, missing-key message, providers configured status). Verified: suite 19 files / 440 checks / 0 fail / 0 skip; make gen byte-idempotent; make check OK; make lint 0 FAIL, 0 WARN; Reviewer APPROVE_WITH_NOTES with mutation-based disproof (inverted precedence → C3/C6 fail).

  • 2026-09-07 — opencode project skill: created .opencode/skills/linux-post-install/SKILL.md (repo had no .opencode/). Skill encodes the repo's operational playbook for agents: repo shape, pos tool model (# POS: header system, exec-bit, deps-guard-before-help, INTERACTIVE_CMDS, determinism), doc authority order (MAINTENANCE Phase 0: templates → DEV.md → AGENTS.md → code), Definition of Done gates (make gen ×2 idempotent → make checkmake lint 0/0 → make test 18 files/416 checks), test conventions (hard-skip contract, negative controls), and repo commands (ci-status, gitea API, pos tree/config). Frontmatter validated (name matches folder, description with trigger keywords); auto-discovered at .opencode/skills/ — no opencode.json change needed; restart opencode to load.

  • 2026-09-06 — Repo cleanup: removed 114 temp/process files (AgentsReport/ 86 + reportAgents/ 28 agent reports), stale task/plan/audit docs (tmp_request.md, FINAL_SUMMARY.md, IMPLEMENTATION_PLAN.md, AUDIT.md, AUDIT_TABLE.md, registry-design doc), stray To/.n/reports/ artifacts; gitignored AgentsReport/ so agent process reports stay local-only. DOC/, tools-docs/ytsync.md, AGENT_TODO.md, bin/lib/apps/tests/config/scripts etc. kept untouched.

  • 2026-09-06pos ai OpenRouter 402 + unbounded session — Architect→Builder→Reviewer. User hit OpenRouter 402 on the assist alias: "You requested up to 131072 tokens, but can only afford 4511" — no provider sent max_tokens, so OpenRouter pre-bills the routed model's full worst-case output (131072 on openrouter/auto); user also asked to bound session history to last-5 requests. Architect decisions: AI_MAX_TOKENS (num, default 2048, real cost cap) sent as max_tokens on OpenRouter and generationConfig.maxOutputTokens on Gemini (llamacpp skipped — local/free); AI_SESSION_TURNS (messages, 2 per exchange; default 40 kept back-compat; 10 = last 5 conversations) resolved lazily in session_push because config loads after the hardcoded line-25 default; both registered in the bin/pos-ai # POS_CONFIG: @General section → visible in pos config ai. Reviewer hardening (CHANGES_REQUIRED → fixed): unguarded env input could reach jq tonumber (0/-5/010/abc) — both providers + session_push now guard with ^[1-9][0-9]*$ fallback-to-default. Verified: fake-curl shim smoke (16 provider-body + 12 session-window checks incl. 010-regression proof), make gen idempotent, make check OK, make lint 0/0, make test 17 files/299 checks green; Reviewer ACCEPT (twice). Tester regression round not run this cycle (user's call); permanent coverage remains a follow-up.

  • 2026-09-06pos ai alias create-flow silent abort + bogus step labels. User hit: pressing Enter on "System prompt (empty = use built-in)" silently returned to the menu (no alias created); step counters showed [1/4] [2/4] in a 5-step flow. Detective: menu_ask_value (lib/menu-lib.sh) contract returns rc 1 for empty+no-default, collapsing "empty" with "cancel"; the Step 4 call at :410 passed "" default so the advertised empty answer triggered || return 0 → silent abort; same latent trap at alias-name :353 (re-prompt dead code). Pre-existing (introduced with the alias feature f61766b0/9f289ba3/300b742a), NOT a 2026-09-06 regression; 11 other menu_ask_value callers correct (6 external rely on empty=cancel, 4 pass defaults) → no global semantic change allowed. Architect: opt-in --allow-empty flag on menu_ask_value (backward-compatible; empty+no-default → rc 0 empty value, genuine cancel/EOF → rc 1, default wins) + step labels fixed to /5. Builder: implemented (lib/menu-lib.sh flag+docs, bin/pos-ai-alias :353/:410 + step counters), 7-case smoke matrix PASS, gen idempotent, check OK, lint 0 FAIL/0 WARN. User chose to commit without the Tester regression round (report: AgentsReport/tester/2026-09-06_alias-menu-tests.md — pty feasibility proven, steps 2-6 pending).

  • 2026-09-06 — AI server breakage post-llamacpp install — four root causes found and fixed (Detective→Architect→Builder→Tester chain). (1) llama-server --version prints to STDERR — detect_llama_version's 2>/dev/null swallowed it → "installed llama.cpp unknown"; (2) printf|grep -q under pipefail → SIGPIPE rc=141 race randomly rejecting valid flags from the 59 KB --help; (3) resolve_model expected flat files but the HF downloader creates <models>/<repo>/file.gguf dirs → Model not found; (4) llama.cpp default port 8080 vs tool's 8088. Architect DQ1-DQ6: help-gated validation stays; dir-expansion never silently picks; ensure_user_bus in lib/common.sh pre-flights all three tools; --no-unit direct-run escape hatch for SSH/headless; candidates narrowed to llama-server/llama-server-cuda; port pinned 8088; installer post-install sanity. Builder F1-F7 (stderr version capture, pipefail-safe flag validation, model dir expansion, port hardcoding, --no-unit, user-bus pre-flight, installer sanity + F1 regex edge: build 1.2.31 misparse). Tester: 4 new regression files (version-from-stderr, 20× flag-validation determinism, model dir expansion, bus pre-flight + E2E) + 3 fixture updates; suite now 16 files / 269 checks. Verified: make gen idempotent, make check OK, make lint 0 FAIL / 0 WARN, make test 269/269 (~49 s), bash -n clean, git diff --check clean. Post-fix: user's machine needed only export XDG_RUNTIME_DIR=/run/user/1000 (linger already on) → Option A systemd-managed server works, or --no-unit for direct run.

  • 2026-09-06 — Stabilization pass: 17-point code-level audit executed via Explorer(3) → Architect(decisions D-A..D-F) → Builder(ai/security/tooling/config/netprobe/f1-f3) → Tester(regression suite) → Reviewer(2 rounds). Security: Telegram sender-owner AND-gate + TELEGRAM_OWNER_ID registry/docs; Matrix MATRIX_ROOM_ID required; gpg --passphrase-fd 3 (no argv secret); /dev/tcp positional-arg form (checkport/smb-client/share-lib/NET_PROBE incl. escaping \$1/\$2); eval --no-command-execution now carried by both chat bridges (D-B), deny-by-default [y/N], tty-gated --trust; D-A soft-fail model ratified by Architect amendment (fail-closed either way; listeners are Restart=always so strict mode would crash-loop). AI: ALL ExecStart flags validated against installed llama.cpp (requested→error, default→omit+warn, CONFIG_REQUESTED_FLAGS), single-line ExecStart confirmed via systemd-analyze verify; hf single-file failure rc/exit-0 + meta-write bug fixed; LLAMACPP_HOST coherent; # POS_SUBCMDS + metadata gaps closed. Tooling: lint-conventions rewritten Bash-native (~24-30× faster, rules byte-identical, :num restored on 2 WARNs, planted-violation negative verified); pos system uninstall covers all 12 libs + scale-tail + flags dir + systemd USER units (|| true) + de-hardcoded plugin markers; safe anchored .bash_completion/.bashrc removals replace sed -i '/pos/d'. Config: canonical load_env_file in lib/config-ui.sh (CRLF strip, env-wins, XDG, LOADED_ENV_KEYS); 9 tools migrated; entertainment-lib collapsed to wrappers; docker-compose deliberately NOT migrated (source-semantics, documented). Tests: first committed regression suite — tests/run-tests.sh zero-dep runner + make test; 12 files / 179 checks / 0 skip / ~52s; hard skip contract (never lie); negative lint/gen-drift gates; systemd-analyze verify included. Verification: make gen idempotent, make check OK, make lint 0 FAIL, 0 WARN, make test green, bash -n clean, git diff --check clean; systemd-analyze verify PASS on generated unit; CLI smokes (pos --help, pos ai --help, pos ai hf --help, pos ai server --help, pos tree) OK.

  • 2026-09-06 — llamacpp optional-app installer + ai app category + pos ai server install-hint wiring: new apps/ai/llamacpp.sh (idempotent install_llamacpp()/uninstall_llamacpp() — GitHub release archive: scans /releases?per_page=10 for the first -bin-ubuntu-{x64,arm64}.tar.gz asset since releases/latest (v0.4.0 milestone) ships no binaries; installs to /usr/local/lib/llama.cpp-<tag> with --strip-components=1, symlinks every llama* binary into /usr/local/bin; uninstall removes the lib dir and only the symlinks whose target points into it); apps/install.sh CAT_NAMES += [ai]="AI / ML"; templates/app.sh categories comment += ai; DOC/APPS.md app-table row count updated (15→16 at the time; 18 after the 2026-09-06 stabilization pass) + categories line + llama.cpp catalog row; bin/pos-ai-server help "Requires:" + both err "llama-server not found…" lines (start/status) now name the app installer + invocation (bash apps/install.sh llamacpp, --apps, --full) and keep the GitHub URL (scrcpy phrasing pattern); DOC/POS.md ai row notes bash apps/install.sh llamacpp. Verified: live API probe confirmed real asset naming — llama-<tag>-bin-ubuntu-x64.tar.gz/-arm64.tar.gz on nightly bNNNNN releases, top-level dir present, .tar.gz not .zip (so tar replaces the brief's unzip step — no unnecessary apt install); bash -n touched scripts; bash apps/install.sh --uninstall llamacpp resolves app + idempotent uninstall rc 0 (no network); bash apps/ai/llamacpp.sh uninstall rc 0; make gen idempotent; make check green; make lint 0 FAIL / 0 WARN.

  • 2026-09-06 — Review-driven hardening of the AI tools (cycle over commits 387f23f/0856b25 + the llamacpp wiring): adversarial review of pos-ai-hf/pos-ai-server found 2 BLOCKING + 5 REQUIRED; Builder fixed F1 (--include/--exclude now bash-case glob filtering — array-shape-safe, no jq regex interpolation, composes gguf→filename→include→exclude), F2 (ExecStart rebuilt as a single-line, correctly-quoted command — systemd_quote() for executable + model path, systemd-analyze verify rc=0, dry-run byte-identical), F3 (--branch/--revision treated as aliases, last-arg-wins, dead BRANCH variable removed), F4 (parallel download drains ALL jobs — per-pid wait + failure collection, honest Downloaded: X of Y files, N failed: … summary, exit rc=1, no .hf-meta marking a half-downloaded model complete, EXIT-trap temp cleanup), F5 (detect_llama_version guarded (missing binary → clean error, never crash), validate_requested_flags errors on explicitly-requested flags the installed llama.cpp doesn't expose, version-aware message), F6 (pos ai hf cache [status|clear] real implementation — dir/count/size + confirm-fail-closed clear (via /dev/tty, tty-not-stdin so no INTERACTIVE_CMDS change); dead helpers removed). Maintainer convention sweep synced llamacpp into bin/pos-ai usage() lines 42/59 + DOC/POS.md AI_PROVIDER row (gemini\|openrouter\|llamacpp) + DOC/howto/ai.md (adapter list, --provider backends, backward-compat shorthand sentence, "Available providers" table row). Final Reviewer acceptance: APPROVE_WITH_NOTES, 0 REQUIRED. Verified: bash -n all bin/pos*; make gen idempotent; make check green; make lint 0 FAIL / 0 WARN; probe matrix — spaced-model-path unit (systemd-analyze verify rc=0 + 16-token word-split), forced-failure parallel download (rc=1, named failed file, no meta), cache clear deny/accept, --slots rejected with version-aware error, status without llama-server clean error.

  • 2026-09-06 — Convention sweep — llamacpp doc/usage sync: bin/pos-ai usage() provider lists (lines 42/59) now include llamacpp; DOC/POS.md AI_PROVIDER config row (gemini\|openrouter\|llamacpp) (Builder's 3 hand-edits verified consistent end-to-end); DOC/howto/ai.md adapter list, --provider backend list, backward-compat shorthand sentence, and "Available providers" table row all include llamacpp (facts from lib/ai-providers/llamacpp.sh). Verified: bash -n all bin/pos*; make gen idempotent; make check green; make lint 0 FAIL / 0 WARN.

  • 2026-09-05pos ai hf parallel downloads + advanced features (commits 387f23f, 0856b25): up to 4 concurrent file downloads (PARALLEL_DOWNLOADS=4, env/config seam), new info/files subcommands, --include/--exclude glob filtering, --revision (commit/tag/branch), refactored hf_gguf_quant_gate(), better progress feedback + error messaging, cache-management framework stub. Full backward compatibility preserved. Verified: bash -n; make gen && make check green at commit; make lint re-verified 0 FAIL / 0 WARN during the 2026-09-06 restore (POS_EXAMPLES dedupe).

  • 2026-09-05pos ai server advanced options (commit 0856b25): GPU offload --gpu-layers/--gpu-threads/--tensor-split, processing --batch-size/--ubatch-size, sampling --temperature/--top-k/--top-p/--repetition-penalty, endpoints --metrics/--health/--slots, memory --mmap/--mlock, llama.cpp version awareness (detect_llama_version()) + server feature validation. Backward compatible; defaults unchanged. Verified: bash -n; make gen && make check green at commit; make lint re-verified 0 FAIL / 0 WARN during the 2026-09-06 restore.

  • 2026-09-06pos ai llamacpp provider forwarder + shorthand: new bin/pos-ai-llamacpp thin forwarder (byte mirror of the gemini forwarder, # POS_SUBCMDS: ask chat models sessions capture), llamacpp dispatch case in bin/pos-ai (pos ai llamacpp <subcmd> …pos ai --provider llamacpp <subcmd> …), ai-llamacpp added to bin/pos INTERACTIVE_CMDS (chat reads stdin → tee-pipe guard), POS.md hand-edits (--provider row + backward-compat sentence). Verified: bash -n; make gen idempotent; make check green; make lint 0 FAIL / 0 WARN; smoke — pos ai llamacpp --help/providers/ask all parse as provider llamacpp (no "Unknown ai subcommand"; curl connect error only when no local llama.cpp server, expected).

  • 2026-09-05pos ai hf recursive+filter+quant+list overhaul: hf_repo_files() now fetches …/tree/{branch}?recursive=true via the new hf_paginate() (walks Link: rel="next" pages, concatenates with jq -s 'add', hard cap HF_MAX_PAGES=20); hf_api() gains an optional header-dump arg + absolute-URL support (backward compatible). HF_GGUF_FILTER verbatim exclusion constant (.gguf suffix, case-insensitive, mmproj|imatrix|clip|vision|projector|mtp excluded) fixes --gguf selecting only mmproj files on quant-directory repos; new hf_quant_candidates()/hf_gguf_quant_gate() with --quant <dir> (multi-dir repos error listing candidates until --quant, single-dir auto-selects, flat repos reject it, requires --gguf); new hf_list_files() + --list remote-file mode (sorted human-size rows, prints exactly what download would fetch incl. the same quant gate — parity). Explicit filename matching: full path → exact, bare name → basename with ambiguity error; explicit filename wins over --gguf/--quant. Docs: POS.md ai row, usage() replacement, # POS_FLAGS + # POS_EXAMPLES (generic org/model-GGUF, no repo hardcoding), completions regenerated. Verified: stub harness /tmp/opencode/hf-test/run-tests.sh 25 cases / 97 assertions green (20 core + 5 optional); live smoke recursive tree shape OK; bash -n; make gen && make check green; make lint 0 FAIL / 0 WARN. Chain: Detective (root cause) → Architect (decisions) → Builder → Reviewer.

  • 2026-09-04 — Fix pos ai hf download --gguf crashing with jq: error: endswith() requires string inputs (user report). Root cause: hf_repo_files() primary path returned the RAW HF tree API response ({oid,path,size,type} — no rfilename field), so .rfilename was null for every entry; the --gguf filter endswith(.rfilename) crashed, and single-file/all-files/meta/summary modes were silently broken too (built URLs with literal "null"). Fix: normalize the tree response to [.[] | select(type == "object" and .type == "file") | {rfilename: .path, size: (.size // 0)}] (same {rfilename,size} shape the sibling fallback already emits — hardened against error-object bodies: {"error":…}[] rc 0, was rc 5); --gguf filter gains a type == "string" guard; empty results get mode-aware messages (" not found in ", "No .gguf files found in — try without --gguf", "No files to download"). Verified: fixture harness /tmp/opencode/hf-test2/run-tests.sh 12/12 green; live API: normalize → 13 records / 0 nulls, --gguf → exactly 10 .gguf (no README/LICENSE/.gitattributes); tiny real download (download Qwen/… LICENSE) OK; user confirmed the full --gguf command now downloads [1/10] …; bash -n; make gen && make check green; make lint 0 FAIL / 0 WARN. Chain: Detective (root cause + sweep) → Builder (3-hunk fix + hardening) → Reviewer APPROVE_WITH_NOTES.

  • 2026-09-04 — Fix pos media ytsync add <@handle> treating a channel's tabs as videos (live user report): bare channel URLs (@handle, /c/, /user/, /channel/ID, music.youtube.com/channel/ID) return the channel's tab structure (Videos/Live/Shorts — _type:"playlist", url:null, id==channel_id) in yt-dlp --flat-playlist mode, so ytsync tried to download the channel ID as a video and failed with "This video is unavailable". Fix: probe-time canonicalization — new canonical_channel_url() called at the top of run_probe() appends /videos to bare channel URLs (works for add AND sync of already-stored bare-handle registry entries, no migration; explicit tabs /videos|shorts|streams|live|playlists|featured|… untouched; ?v=/?list=/youtu.be untouched); collect_entries() filters to watchable entries (watch?v=|youtu.be/|/shorts/) with a _type=="video" fallback guard so empty channels degrade to graceful 0-new. Live: sync --dry-run now resolves 3Blue1Brown (channel · 151 videos) with real titles. Verified: stub harness /tmp/opencode/ytsync-test/run-tests.sh 32/32 green; bash -n; make gen && make check green; make lint 0 FAIL / 0 WARN. Chain: Detective (root cause + spec) → Builder → Reviewer APPROVE_WITH_NOTES.

  • 2026-09-04pos ai hf (bin/pos-ai-hf) — Hugging Face model downloader. Subcommands: download <repo-id> [filename] (single file, whole repo, --gguf filter, --branch <rev>, --output <dir>), search <query>, list, remove. Downloads to ~/.local/share/linux_post_install/ai/models/<namespace>-<model-name>/ (seam-guarded HF_DOWNLOAD_DIR), writes .hf-meta JSON per repo, prints structured summary (📥/📁). Config extends the existing ai scope via # POS_CONFIG: aiHF_TOKEN (secret) and HF_DOWNLOAD_DIR in ~/.config/linux_post_install/ai.env with env-var precedence. Auth on all requests; HTTP 429 rate-limit sleep + retry once; resume via curl -C -; progress bars to stderr. Deps: curl/jq guards before --help; no stdin → not in INTERACTIVE_CMDS. Verified: stub-PATH suite /tmp/opencode/hf-test/run-tests.sh 46/46 green (argument parsing, download single/multi/gguf/branch/output, search, list, remove, config/token, output format); bash -n; make gen && make check green; make lint 0 FAIL / 0 WARN. Docs: POS.md ai row + detail block.

  • 2026-09-04pos media grab (bin/pos-media-grab) — auto-download a URL as audio or video. Classifies by domain (YouTube Music/SoundCloud/Bandcamp → mp3; YouTube/Vimeo/Twitch → mp4) with --audio/--video overrides and GRAB_DEFAULT config (pos config grab, default video) for unknown domains; --best default for video (non-interactive, --worst override); all flags (--output, --no-playlist, --cookies, --dry-run) forwarded to mp3/mp4; prints a clean summary (🎵/🎬 title, duration, path, size). Telegram listener (bin/pos-communication-telegram-listener) gains url_detect + a URL routing step between the prefix map and AI bridge — bare http(s) URLs route to pos media grab --best (600s timeout). Verified: /tmp/opencode/media-grab-test/run-tests.sh 28 cases / 70 assertions green; bash -n on both files; make gen && make check green; make lint 0 FAIL / 0 WARN.

  • 2026-08-21 — Share suite interactive layer (lib/share-lib.sh + menu modes for all five pos share * tools): bare invocation now opens an EOF-safe looping menu instead of printing usage. New lib/share-lib.sh (436 lines) owns the shared primitives — share_menu_guard/share_menu_run/share_pick/share_ask_value (quit on EOF so non-tty callers can't hang), share_require_bin/share_port_probe/share_service_active/share_path_probe rc-only probes, share_usb_records (blank-line-record parser for usbsrv listings), share_smb_shares (smbclient -g Disk enumeration incl. guest→auth retry) + share_usb_devices/share_usb_clients, share_nfs_exports (showmount), share_folder_candidates (bounded-probe scan of mounted targets + conventional roots; container overlay/tmpfs/nsfs excluded via findmnt; clients build their own mountpoint pickers on top), and advisories share_ufw_blocks_ports+share_offer_fix. Tools keep every legacy flag/subcommand byte-compatible (verbatim command bodies, thin menu layer on top): nfs-server gains a client-spec presets picker + inactive-service/UFW offers, nfs-client gains idempotent unmount/unpersist (already-absent = report, rc0) + persist-verify-with-rollback + replace-confirm, smb-server gets UFW offer + menu tree, smb-client gets enumerate→pick→mount with account reuse (SMB_AUTH_USER contract), usb-server picker-first with raw-listing manual-entry fallback when the server listing is unreadable. New seams: EXPORTS_FILE (nfs-server), UNIT_DIR (nfs-client); bin/pos INTERACTIVE_CMDS += both stdin-reading share tools; install.sh lib list += share-lib.sh; preinstall.sh += smbclient (smb-client enumeration dep). Docs: POS.md share rows/detail, howto/share.md per-tool Interactive-menu notes, SCRIPTS.md phase table + new ## lib/share-lib.sh section, DEV.md lib row + env-seam registry, AGENT_Context hand-maintained spots (lib table row 436, Phase-2 prose). Verified: 80-case stub battery vs recaptured deterministic golden — only the 9 documented intentional deltas differ (additive help lines, seam-path strings in messages, missing-dep message delta, unmount-idle FLAGGED→rc0, unpersist round-trip now works, usbs-bare usage→menu guard); 12/12 PTY tests (menus open/quit non-tty, filter/zero-match/default/cancel picker semantics, full nfs-server share flow writes the export line, usb-server share via pickers + down-server fallback, smb-client guest-enumerate→manual-share flow); real /etc/exports + /etc/systemd/system md5-verified untouched; make gen && make check green, make lint 0 FAIL / 0 WARN.

  • 2026-08-21 — Refreshed AGENTS.md against the codebase: HOWTO category list corrected to match DOC/howto/* (ai/share/schedule, no bare "usb"); CI bullet now states only verifiable facts (lint.yml job gates, push-to-main/PR, ci-ok/<sha>/ci-fail/<sha> result tags) instead of the uncheckable act-runner naming; new Doc conflicts bullet encoding the MAINTENANCE.md → Phase 0 authority order and templates/*.sh as required starting points. Every other claim re-verified against scripts/{gen-docs,check-sync,lint-conventions}.sh, bin/pos (dispatch loop, INTERACTIVE_CMDS), .gitignore/.gitmodules, lib/config-ui.sh; gates green before and after.

  • 2026-08-15pos docker stack (bin/pos-docker-stack) — containers grouped by their Docker Compose project. Each stack is a section (project name, sorted) with lines container-name status ports; containers with no compose project land in a Standalone section at the end; ends with Stacks: N containers: N standalone: N. Running only by default, -a|--all includes stopped/exited (like docker ps -a). Status colored on a terminal (Up* green, Exited*/Dead*/Created* red, Paused*/Restarting* yellow); exit 0 also when no containers. Data via docker ps with --format '{{.Names}}{{"\u001f"}}{{.Label "com.docker.compose.project"}}{{"\u001f"}}{{.Status}}{{"\u001f"}}{{.Ports}}' (compose v2 sets the project label; {{"\u001f"}} escapes in the Go template), parsed with awk -F'\x1f' + IFS=$'\x1f' read everywhere — tab/pipe delimiters are IFS whitespace or inside values, so \x1f (DEV.md:213 gotcha); dash padding via sed not tr (tr corrupts multi-byte ). Deps guard (docker) before --help; no stdin → not in INTERACTIVE_CMDS; # POS_FLAGS: -a --all. Docs: POS.md docker row + detail, howto/docker.md table + section, bin/pos usage EXAMPLES, AGENT_Context §14 row. Verified: stub-PATH suite /tmp/opencode/docker-stack-test/run-tests.sh 23/23 (grouping, sorted stacks, -a shows exited, standalone, empty daemon rc=0, colored status, missing docker rc=1, --help after deps guard); live runs against the real daemon (affine/audiobookshelf/convertx/gitea stacks, affine_migration_job Exited (0) + lab1 Exited (137) under -a); dispatch via pos docker stack; make gen && make check, make lint 0 FAIL / 0 WARN.

  • 2026-08-15 — Fix pos media sync offering a Ventoy stick's EFI partition as the sync target: with the data partition unmounted, the 32 MB VTOYEFI ESP was the only mounted USB partition, usb_detect offered it with no context, and cp died mid-copy with No space left on device (live-box report). usb_detect now fetches FSTYPE/PARTTYPENAME and excludes EFI system partitions (Ventoy VTOYEFI, /boot/efi) from both the mounted list and the mount-offer list; USB_MOUNTED entries carry mp|label|size|model|fs and usb_pick_root shows that in the single-stick confirm and the multi-stick/partition picker (1) /media/Ventoy (1.1T, Ventoy, exfat)), while USB_ROOT stays a bare mountpoint (${root%|*}) so pos system backup (${root%/}/backups) is unaffected. pos-media-sync gained a pre-flight space check (measures exactly what needs_copy would copy vs df -Pk, err/warn before any copy) — no more mid-copy ENOSPC. Docs: howto/media.md target-picking note, SCRIPTS.md usb-lib paragraph, AGENT_Context hand-maintained lib row (194→205). Verified: new stub harness /tmp/opencode/vtoyefi-run.sh (ESP filtered from mounted + mount-offer, multi-pick shows only the data partition, space fit/too-small/dry-run-warn) green; /tmp/opencode/backup-test still green; live check printf 'n\ns\n' | bash bin/pos-media-sync --mp3 no longer offers VTOYEFI (offers unmounted sda1 Ventoy instead); make gen && make check, make lint 0 FAIL / 0 WARN.

  • 2026-08-15 — Fix pos media sync reporting success with 0 files when the source is a symlink: it enumerated with plain find "$SRC", and GNU find (default -P) does not descend a command-line symlink to a directory — ~/Music -> /mnt/hdd/…/music therefore yielded zero matches, the loop never ran, and the tool printed 0 added, 0 updated, 0 unchanged without creating the target dir (live-box report). Switched to find -H "$SRC" (follows only command-line symlinks; inner-symlink semantics unchanged). howto/media.md sync section notes symlinked sources are followed. Caught live, not by the 46-case stub suite (which used a real temp dir source — lesson: add a symlink-root fixture). Verified: printf 'y\n' | bash bin/pos-media-sync --mp3 --dry-run now lists all 31 mp3s as "would copy"; make gen && make check green.

  • 2026-08-14pos system backup — smart USB detection: lsblk TRAN (lsusb/by-id cross-check), mount offer for plugged-in-but-unmounted sticks, sha256-verified copy (stub-suite 54/54).

  • 2026-08-05pos communication telegram--parse-mode (plain/markdown/html).

  • 2026-08-05 — doc/code sync gate — make gen + make check + pre-commit hook.

  • 2026-08-05pos usb server — USB Redirector control tool (494eae2).

  • 2026-08-05pos <category> --help auto-discovery in the dispatcher.

  • 2026-08-05 — AGENTS.md with lazy-loaded DOC references.

  • 2026-08-06 — Fix entertainment timer 1h not firing — interval_to_oncalendar emitted invalid OnCalendar=*-*-* */N:00:00 (systemd rejects */N in the hour field); now *-*-* 00/N:00:00. Dropped the cron fallback entirely: scheduling is systemd user timers only (sync_cron/interval_to_cron/cron_block removed), status simplified, Nd intervals rejected with a clear error.

  • 2026-08-06 — Nested pos subcommands — # POS_SUBCMDS: header annotation (telegram, docker-compose, docker-vbox) + make gen emits a _pos_subcmds completion map; nested tools (telegram listener) auto-list under their parent instead of as a flat sibling (telegram-listener) in pos <category> and tab-completion; generic tool-level completion (subcommands + flags + --help).

  • 2026-08-06 — Telegram listenerpos communication telegram listener: interactive /command → bash map editor + owner-only polling daemon as a systemd user service (map in ~/.config/linux_post_install/telegram_commands.env, re-read per message; /help, unknown-command reply, 60s timeout, stdout reply).

  • 2026-08-06 — NFS in pos systempos system nfs-server (status/share/ unshare/list/reload/enable/disable, idempotent /etc/exports edits, generic default with Tailscale/WireGuard/LAN examples) + pos system nfs-client (mount/unmount/list + persistent mounts as systemd .mount units ordered after network-online.target, no fstab); nfs-kernel-server + nfs-common added to preinstall PACKAGES.

  • 2026-08-06pos HOW-TO guide set — DOC/HOWTO.md index + per-category DOC/howto/*.md (network, docker, media, system, ssh, usb, communication, entertainment) with flags, recipes, config, and troubleshooting; wired into DOC/README, root README, AGENTS.md.

  • 2026-08-06 — Multi-platform alerting — lib/notify.sh routes via NOTIFY_PLATFORM (notify.env, default telegram; sender contract for Matrix/Synapse later), system.env shared config for health/backup, dynamic effective values in --help, telegram --markdown alias.

  • 2026-08-06 — Tier 1 — pos system health (dashboard + --send), lib/notify.sh (wired into backup + firewall), daily digest timer via postinstall.

  • 2026-08-06 — Document Map index + Entertainment section in AGENT_Context (cf36780).

  • 2026-08-06 — Entertainment module — plugins (weather/joke/gold), pos entertainment config/enable/disable/send/status, auto-trigger + Telegram send.

  • 2026-08-07pos system health --send notification-only; listener @quiet prefix (run mapped command without replying, for commands that self-notify). /status=@quiet pos system health --send = exactly one digest.

  • 2026-08-09 — Matrix/Synapse communication tools — pos communication matrix sender + listener, completing the second notify platform lib/notify.sh was designed for (NOTIFY_PLATFORM=telegram,matrix fan-out; the sender implements the send <value> [--markdown] contract via notify_sender_name()'s default key→tool mapping, no lib changes). Sender (bin/pos-communication-matrix-sender): send <value> [--markdown] [--room <id|alias>] PUTs m.room.message (m.text) to the client-server API v3 — room ids/aliases URL-encoded (#pos:example.org%23pos%3A…), unique per-message txn id, --markdown sends org.matrix.custom.html via a best-effort markdown→HTML converter (bold/italic/code/fences/strike/links/headers/lists, escapes HTML, never fails the send); login --user <@id> (masked password prompt → m.login.password → saves access_token+user_id); test. Config scope matrix (~/.config/linux_post_install/matrix.env, MATRIX_HOMESERVER/MATRIX_ACCESS_TOKEN/MATRIX_USER_ID/MATRIX_ROOM_ID, secret masked) registered via # POS_CONFIG:pos config matrix + tab-completion scope. Listener (bin/pos-communication-matrix-listener): systemd user daemon (pos-matrix-listener.service) long-polling /sync (30s timeout, per-sync since token, compact filter dropping presence/account_data/device noise, m.room.message only); reacts to MATRIX_USER_ID's own messages (resolved via /account/whoami if unset), MATRIX_ROOM_ID restricts to one room; / and ! both resolve; replies threaded m.in_reply_to; @quiet no-reply marker; /cmd::desc=… map descriptions; ai … bridge (pos ai gemini ask, per-room session matrix-<room>, ai /reset clears, markdown stripped); interactive editor (--status/--enable/--disable/--run), 60s command timeout, exit-code prefix, ~3800-char truncation. communication-matrix-listener added to INTERACTIVE_CMDS (stdin editor + forever-loop daemon). Docs: POS.md rows + "in detail" sections + ai bridge note, howto/communication.md rewritten Matrix sections, HOWTO.md index + config table + platform note, bin/pos usage EXAMPLES; make gen && make check green. Verified against a mock homeserver: send plain/markdown/--room/test request shape (URL-encoding, Bearer auth, JSON body), login token save, listener owner-filter + /status reply + /help + @quiet silence + non-zero exit reply + interactive editor add. — state-based threshold rule monitors (eventer). Each line of ~/.config/linux_post_install/event.env is an independent rule: ["<msg>" if ] <check-command> <op> <threshold> (op > < >= <= == !=, unit suffix ok 60c/80%). The check command is run on every pass and its first numeric output compared float-safe; operator detected as the rightmost op threshold pair so checks containing their own >/< (awk, redirection) parse fine. Alerts once on false→true plus one recovery message on true→false (no repeats while a condition holds); per-rule state in ~/.local/share/linux_post_install/eventer/state/ keyed by rule-line hash (editing a rule resets its state). Subcommands: run (timer entrypoint), config (interactive add/remove/edit with validation by test-running the check), list (rules + live values), enable [interval] (systemd user timer pos-event-trigger.timer + oneshot service; 5m…weekly or OnCalendar=…; graceful warnings when no user systemd manager, loginctl enable-linger attempt), disable, status. --dry-run honors the DEV.md dry-run convention. Alerts via lib/notify.sh (Telegram default; other platforms via NOTIFY_PLATFORM). New: bin/pos-system-event-trigger, lib/eventer-lib.sh, config/event.env template (installed no-clobber by postinstall), lib/eventer-lib.sh installed by install.sh, system-event-trigger added to INTERACTIVE_CMDS, usage EXAMPLES row. Docs: POS.md system row, HOWTO.md index row, howto/event-trigger.md; make gen && make check green; functional tests covered trigger/recovery/no-repeat, float + unit parsing, editor add/remove/edit + validation + dry-run, timer enable/disable/status (graceful), dispatcher routing.

  • 2026-08-09pos media mp3/mp4 hardened + smart format selection. Both tools: yt-dlp calls go through spawn (honor $DRY_RUN; --dry-run prints the exact command and skips dep checks), -o/--output, --no-playlist, --cookies (file existence check), clean ffmpeg/yt-dlp guards, # POS_FLAGS: for completion, full metadata (--embed-metadata --embed-chapters --embed-thumbnail --no-overwrites, mp3 also --convert-thumbnails jpg + --parse-metadata "%(artist,uploader)s:%(artist)s" so the uploader fills the artist tag). mp3 gains --by-artist (~/Music/<artist>/<title>.mp3). mp4 gains -f <id> / --best / --worst (no prompt), conflict validation, and an interactive picker that shows a curated -F table ([audio]/[video]/[combo] grouping, raw clutter dropped) on stderr — stdout carries only the chosen id (ui_pick lesson) — with id validation against the real table and empty/best default. Docs: howto/media.md rewritten (flags, metadata, by-artist, troubleshooting); make gen && make check green.

  • 2026-08-09 — Telegram ai … now answers about a message you reply to: the listener extracts reply_to_message.text (falls back to caption) from each update and passes it to handle_message; the AI bridge prefixes the prompt with [Reply context — the message you are replying to]. So replying to a /status output and asking ai check this details gives the model the actual output. Applies only to the AI bridge (mapped /commands untouched); reply context rides in the user turn so the session records what was analyzed. Docs: howto/ai.md bridge section.

  • 2026-08-09pos ai gemini sessions + Telegram-friendly replies. --session <name> gives ask/chat persistent memory (~/.local/share/linux_post_install/ai/<name>.json, capped at 40 turns, pruning keeps the first user turn as scene); new sessions subcommand (list / reset <name>). Telegram listener now keeps one session per chat (telegram-<chat_id>) with ai /reset to clear. New --system "<text>" flag injects a Gemini systemInstruction (via jq merge) sent every turn but never stored in the session file; the listener passes a Telegram-voice prompt ("reply like a friendly Telegram chat, use emojis") and strips markdown (**, *, backticks, #, links, lists, blockquotes) from replies before sendMessage, since messages go out as plain text. Docs: howto/ai.md (flags, sessions, bridge memory/formatting), make gen && make check green.

  • 2026-08-09 — Fixed pos config secret-value corruption: cfg_read_secret's cursor-advance echo went to stdout and, since the function is called via $(...), a leading \n ended up inside every secret value → the env file got AI_GEMINI_API_KEY="\n<key>", unreadable by cfg_value/load_config (menu showed (not set), pos ai gemini demanded a key). The newline now goes to the terminal (echo >&2). Defense in depth: cfg_write/write_config_key strip CR and truncate multi-line pastes (warn), cfg_value and the ai/telegram load_configs strip CR on read. Verified on a real PTY (piped tests couldn't reproduce — non-TTY stdin skips the echo path).

  • 2026-08-09ai category — pos ai gemini (ask/chat/models) via Google Gemini REST API. ask prints only the answer (pipe/script/Telegram-friendly), chat is a multi-turn REPL (q/quit/Ctrl+C, /reset, empty input re-prompts), models lists generateContent-capable ids and flags the default; --model override; default gemini-2.5-flash. Config scope ai (AI_GEMINI_API_KEY secret + AI_GEMINI_MODEL) in ~/.config/linux_post_install/ai.env, edited via pos config ai; config/ai.env template installed no-clobber by postinstall; ai-gemini added to INTERACTIVE_CMDS. Telegram listener now answers non-command messages starting with ai via pos ai gemini ask (owner chat only; error replies carry the pos config ai hint) — future intents (reminders) slot in as more case arms in handle_message. Docs: POS.md ai section + listener bridge, howto/ai.md, HOWTO/README index rows, bin/pos usage example.

  • 2026-08-09 — Entertainment plugins gold + weather now emit emoji-visualized Telegram messages. Gold: headline is USD/gram (XAU/oz ÷ 31.1034768), ounce as reference, bid/ask, cleaned timestamp (+00:00/fractional seconds stripped). Weather: per-WMO-code emoji (☀️/🌙 day-night aware for clear sky), °C + feels-like, humidity, wind with unit spacing. Both verified live; emojis are safe in the default plain send mode.

  • 2026-08-09pos tree: prints the live pos command tree (categories → commands → subcommands) by deriving the hierarchy from bin/pos-* filenames + # POS: / # POS_SUBCMDS: headers, so it always matches what the dispatcher can run. Category-less like pos-config; --depth N limit; pos help tree works. Docs: POS.md tree section, bin/pos usage example, make gen regenerated the AGENT_Context tree/dispatch/filetable + _pos_flags[tree].

  • 2026-08-09 — Telegram sender config / config set removed — redundant with pos config telegram (same # POS_CONFIG: registry, masked token display + input, chat-id validation, chmod 600); sender/listener error hints now point there. Deep-review bugfixes in the same commit: mapped /command values containing | are no longer truncated (load_map switched from a | to a \x1f delimiter — previously /up=echo hi | head silently ran echo hi ); pos entertainment send <plugin> [args…] actually forwards the extra args (every arg was shifted in the flag loop, so $@ was empty) and passes -- before the message so leading-- plugin output isn't parsed as an option; write_config_key (entertainment-lib) and cfg_write (config-ui) replaced unescaped sed -i "s|^K=.*|K=\"$v\"|" with grep-v+append so values with &/|/\ no longer mangle (also the path all telegram config now flows through); sync_systemd daemon-reloads after removing timer units; digits config validation accepts negative group/supergroup chat ids (-100…).

  • 2026-08-09 — Fixed telegram listener editor crash on remove/edit/test: ui_pick printed its menu listing to stdout, so idx="$(ui_pick)" captured the menu and the number, and MAP_CMDS[$idx] (arithmetic array subscript) blew up with "syntax error in expression". Menu decoration now goes to stderr; only the picked index is emitted on stdout. Pre-existing bug (before the ::desc work), exposed by the description column.

  • 2026-08-09 — Telegram listener pushes its mapped /commands to the bot's / menu via setMyCommands (auto after every map edit, on --enable, and at daemon start; manual --sync-commands flag). Map lines may carry a menu description: /cmd::short description=bash command (falls back to the bash command, ~40 chars). Names are validated against Telegram's lowercase [a-z0-9_] rule — invalid ones are skipped from the menu with a warning but still resolve when typed; empty map clears the menu. Fixed latent bugs found by the sync work: map_has (awk END{exit 1} overrode the match), and warn() went to stdout so it leaked into the generated JSON (now stderr).

  • 2026-08-09pos config <TAB> scope completion is now cached at gen time (_pos_config_scopes array emitted by make gen from the # POS_CONFIG: registry) instead of scanning ~40 tools per TAB — a per-keypress subshell storm that wedged interactive shells for minutes on the loaded homelab box. Two stuck -bash sessions (69%/38% CPU) killed. plugin_marker/plugin_keys hardened with || true so config_keys no longer aborts mid-scan under set -euo pipefail on mixed lib/plugin dirs (installed layout) — fixes missing plugin keys in pos config entertainment.

  • 2026-08-09pos config <scope> interactive config editor: reads the # POS_CONFIG: registry across tools into a single runtime config (~/.config/linux_post_install/*.env, one file per scope, chmod 600); secret masking with show/hide toggle, digits:/num:/url: validation, - to clear, blank keeps; *plugins marker expands plugin vars (entertainment) from entertainment-lib.sh; desc::example value-format hints shown in the editor; gen-docs now handles category-less tools (pos-config), fixed a set -e+pipefail bug that truncated the header registry.

  • 2026-08-09pos-communication-telegrampos-communication-telegram-sender: one canonical send (dropped the legacy --send flag, which duplicated the send subcommand in completion). pos communication telegram <TAB> now completes to just sender listener. lib/notify.sh maps platform telegramtelegram-sender via notify_sender_name(); entertainment-send + health --send check updated. Removed phantom subcommands from howto/communication.md (webhook/logs/broadcast/file never existed).

  • 2026-08-09 — Structure/convention audit fix: --dry-run now truly dry (spawn() honors DRY_RUN, install.sh exports it to child phases, postinstall mutations run-wrapped); gen-docs.sh no longer chmods regenerated files to 0600; make check now syntax-checks apps/entertainment/features/templates; .gitignore protects config/authorized_keys + config/rclone.conf; honest --send confirmation; docs refreshed (notify.sh in lib lists, pos-health systemd units, tsui, scripts/, INTERACTIVE_CMDS).

  • 2026-08-11 — Docs: DEV.md / AGENTS.md / AGENT_Context improved from the SMB session's lessons. DEV.md: new "Testing tools that need root / systemd / missing deps" (env-override test seams — FLAGS_DIR/SMB_CONF/SMB_CREDS_DIR/UNIT_DIR precedents — + stub-PATH fakes + PTY prompt driving via script); new Best Practice "Managed Config Blocks" (start/end marker idiom incl. the inblock == 1 awk guard, validate-then-apply, hot reload); deps-guards-run-before---help made explicit (previously only inferable by reading the NFS tools); "Update the docs" checklist completed (howto index/section, Common Tasks row, AGENTS.md Quick facts, AGENT_TODO Done move). AGENTS.md: clarified which filetable line-count rows are hand-maintained (non-pos-* files above the marker) + when to bump them; deps-guard clause added to Quick facts. AGENT_Context "Adding a New Tool" steps 67 mirror the above. make gen && make check green.

  • 2026-08-11share category grows SMB: pos share smb server (bin/pos-share-smb-server) + pos share smb client (bin/pos-share-smb-client), completing the share trio (usb/nfs/smb). Server: status/share/unshare/list/adduser/deluser/reload/enable/disable; idempotent marker blocks in /etc/samba/smb.conf (# >>> pos-managed share: <name># <<< end pos-managed share — hand edits outside markers survive; inblock==1-guarded awk so removing one block never eats another's end marker), testparm validation before apply + smbcontrol smbd reload-config hot reload; --read-only/--guest/--users u1,u2 flags with unrestricted-share warnings; smbpasswd user management (prompts, requires system user first). Client: mount/unmount/list/persist/unpersist; password prompt via /dev/tty, throwaway chmod-600 credentials for one-shot mounts, persistent creds at /etc/samba/credentials/<name> (chmod 600); persist writes a systemd .mount unit (systemd-escape) with x-systemd.automount + _netdev — mounts on first access, never blocks boot. Both source lib/notify.sh for mutations; added to INTERACTIVE_CMDS (prompting subcommands). Deps: samba + cifs-utils added to preinstall PACKAGES. SMB_CONF/SMB_CREDS_DIR/UNIT_DIR env-overridable for tests (FLAGS_DIR precedent). Docs: POS.md share rows, howto/share.md SMB sections, HOWTO index row, AGENT_Context Common Tasks, AGENTS.md categories. make gen && make check green; logic tested via stubbed PATH + temp config (marker idempotency, guest + user persist flows).

  • 2026-08-11pos network checkport nmap overhaul: two-pass engine — pass 1 = fast -Pn -T4 --max-retries 1 scan of only the asked ports (was: all 65535) with per-port state + nmap service names; pass 2 (--versions, opt-in) = -sV --version-light on open ports only (generous host-timeout — version probing a silent service otherwise made nmap skip the host entirely), fallback fast banner probe for open TCP with no version info; TCP fast path ~2s for 3 ports. Unprivileged UDP now falls back to the nc engine (Debian nmap -sU requires root and quit outright); IPv6 hosts get -6; no output/filtered states set rc=1; --timeout scales nmap host-timeouts. New --versions flag in # POS_FLAGS: (completions regenerated) + usage text; port-metadata fallback retained. make gen && make check green.

  • 2026-08-11pos communication matrix sender login error reporting: captures HTTP status + Matrix errcode/error from the JSON body (temp file, not stdout) instead of a generic "wrong credentials?" message — distinguishes unreachable homeserver from rejected credentials; auto-prepends @ when --user is bare (e.g. --user alice:example.org@alice:example.org).

  • 2026-08-11 — New share category — usb and nfs moved out of pos usb / pos system into pos share: pos share usb server (was pos-usb-server), pos share nfs server + pos share nfs client (were pos-system-nfs-*). Renamed the three tools (bin/pos-share-*), updated # POS: headers/usage strings, INTERACTIVE_CMDS (usb-servershare-usb-server), bin/pos usage() EXAMPLES, and the notify-scope comment in pos-system-backup. Docs: new DOC/howto/share.md (USB + NFS consolidated; howto/usb.md deleted, NFS sections stripped from howto/system.md), POS.md ### share section (replaces ### usb, nfs rows moved out of ### system), HOWTO/README indices, AGENT_Context hand-written spots, root README, DEV.md INTERACTIVE_CMDS example, AGENTS.md categories. Category is the home for future smb. make gen && make check green; /usr/local/bin refreshed.

  • 2026-08-12pos network download (bin/pos-network-download) — aria2 JSON-RPC daemon + queue control. Daemon: persistent aria2c as a systemd user service (pos-aria2.service, ${XDG_CONFIG_HOME:-$HOME/.config}/systemd/user, enable --now + linger warning on headless boxes), --rpc-listen-port=6800, generated RPC_SECRET in ~/.config/linux_post_install/download.env (chmod 600, env override), unit flags --continue=true --max-connection-per-server=16 --split=16 --seed-time=0 --dir=$HOME/Downloads. Commands (18): start/stop/status (+ bare overview = status+list), add <url…> [--dir --out --split --tmux], torrent <file|magnet…> [--dir --seed --tmux] (base64 addTorrent), metalink <file|url> [--tmux], list (active/waiting/stopped table), info/files/peers <gid>, pause|resume|remove [gid|all] (--forceforce*), purge, move <gid> <pos>, limit [gid] <speed> (--upload, 0=unlimited, 2M/512K), set <k=v…> [--gid], watch [gid] (2s live repoll; exits when that gid completes). --tmux opens a detached dl-<name> session running watch <gid> (name from --out/URL basename, sanitized, 40-char truncate, -2 on collision; closes itself on completion). Deps: aria2c/jq/curl guards before --help; aria2 added to preinstall PACKAGES. No stdin → not in INTERACTIVE_CMDS. JSON built with jq -nc --arg (never string interpolation — fixes JSON-quote bugs); # POS_SUBCMDS: (18) + # POS_FLAGS: → completions. Test seams RPC_PORT/RPC_SECRET/DOWNLOAD_DIR/USER_SYSTEMD_DIR/ACTIVE_MARKER; 76-case stub-PATH behavior suite green (unit content, secret 600, add→gid, tables, queue ops, error paths). Docs: POS.md network row+detail, howto/network.md section, HOWTO index, AGENT_Context Common Tasks row. make gen && make check green.

  • 2026-08-12pos network download grows outage resilience: restart <gid> (re-queue from history — torrents via rebuilt magnet urn:btih: + &tr= trackers, HTTP via original URIs with dir/out preserved, --continue=true resumes partials; options --dir/--seed/--split/--tmux), retry <gid|all> (smart retry — waits out internet outages via NET_PROBE seam, re-queues, retry_verify polls the new gid; aria2 error 3 = real problem → diagnosed + marked permanent in ~/.config/linux_post_install/download.retry as url:<uri>/bt:<infohash>, retry all skips them, manual restart overrides; --once/--quiet timer mode; --interval/--max-wait), and the retry healer systemd user pair (pos-aria2-retry.service oneshot retry all --once --quiet + pos-aria2-retry.timer 2min, Persistent) that arms on download start (add/torrent/metalink/restart) and disables itself when nothing is left; watch <gid> now auto-restarts its download after an outage. Fixes from stub-suite review: ensure_healer was missing from the three submit paths; RESTART_NAME was lost across do_restart's process-substitution subshell (now a download_name() helper); restart exited 1 because the [ tmux -eq 1 ] && tmux_watch test was the function's last statement. Verification: stub-based test harness (/tmp/opencode/dl-test — curl/systemctl stubs with tellStatus fixtures, NET_PROBE file-flip, unit enable/disable logging) 119/119 green, incl. new restart/retry/healer/watch-heal cases. Docs: POS.md download rows + outage-resilience paragraph, howto/network.md outage recipe, SYSTEMD.md per-user units section, AGENT_Context + completions regenerated. make gen && make check green.

  • 2026-08-12pos network download replace <gid> <url> + fresh-link status advisory. status now flags stopped errored downloads whose source is marked permanently failing in download.retry (needs fresh link: <name> (<gid>) — pos network download replace … <new-url>; one tellStopped RPC, id-match in jq). replace <gid> <url> re-queues a dead single-file HTTP/FTP download with a new URL keeping the same dir + file name (partial resumes via --continue=true), unmarks the old source (retry_unmark, literal grep -vxF — URL-safe), and reuses retry_verify so a dead replacement link is diagnosed + marked permanent; torrents/active/multi-file are rejected with hints; --dir/--split/--tmux supported. retry_verify hardened to ${quiet:-0} so it works outside cmd_retry. Stub suite grew a replace section (advisory match, success + unmark + advisory-clear, dead new link marked, torrent/active/arg errors, prefix gid) — tellStopped fixtures gained uris (real aria2 includes them). 141/141 green; docs: POS.md row + outage paragraph, howto/network.md dead-link recipe. make gen && make check green.

  • 2026-08-12pos system event-trigger (eventer) generalized into pos system schedule — the scheduler replaces the single-timer threshold monitor with per-job systemd user timers (pos-schedule-<name>.{timer,service}, Persistent, ExecStart run <name>, reconciled on enable/disable — orphan units + the legacy pos-event-trigger timer auto-removed). Each job is a chmod-600 file ~/.config/linux_post_install/schedule.d/<name>.env: INTERVAL (5m..59m/1h..23h/hourly/daily/weekly/OnCalendar=…), NOTIFY policy, optional MSG, RULE (threshold only), and COMMAND = literal remainder of the line (pipes/quotes/sudo need no escaping). Policies: always (full output every run), onchange (diff vs last run, first run always sends), onerror (non-zero exit or empty output), threshold (old event-trigger behavior: first numeric vs RULE, alert on false→true + recovery, per-job firing state), never (silent side-effect jobs — no notify; run log + last-run record still kept). Per-run logs/state in ~/.local/share/linux_post_install/schedule/{logs,state}/. Subcommands: run [name|all], list, config (interactive add/edit/remove/enable/disable with validation), enable [name|all], disable [name|all], status, migrate (converts legacy event.env rules → schedule.d/rule-N.env threshold jobs, adopts the legacy timer's OnCalendar or 5m, removes the old timer). Files: bin/pos-system-event-triggerbin/pos-system-schedule, lib/eventer-lib.shlib/scheduler-lib.sh (git mv; installed by install.sh), config/event.env + config/event-rules.templateconfig/schedule.d/ starter jobs (nvme-health via sudo -n smartctl with the user's exact grep — sudoers NOPASSWD documented; cpu-temp + disk-root thresholds; silent log-cleanup), postinstall installs them no-clobber into an empty schedule.d/ (legacy event.env users get a migrate hint instead). bin/pos EXAMPLES + INTERACTIVE_CMDS (system-schedule config) updated. Supersedes the "Tier 2: watch plugins" backlog idea. Docs: POS.md system row rewritten, howto/event-trigger.md → howto/schedule.md (job syntax, policies, NVMe recipe, migration), HOWTO.md index row + config table + scheduling bullet, AGENT_Context lib row + Common Tasks row. make gen && make check green; stub-harness suite (fake systemctl/sudo/smartctl/sensors/df + fake telegram sender logging, env seams SCHEDULE_DIR/SCHEDULE_STATE_DIR/SCHEDULE_LOG_DIR/USER_SYSTEMD_DIR/SCHED_LEGACY_ENV) covers all 5 policies (threshold cross/recover/no-repeat, onchange first/diff/same, onerror, always, never-silent), COMMAND literal-pipe parsing, enable/disable/status + orphan/legacy cleanup, migrate (incl. skip-existing + dry-run), and dispatch.

  • 2026-08-13pos communication scrcpy audio control: scrcpy already forwards device audio to the desktop by default (answer: yes, default is sound-to-desktop). Added SCRCPY_AUDIO config key (pos config scrcpy, default true): false/no/0--no-audio, true/yes/1 → nothing (default), anything else → error. Docs: POS_CONFIG header, POS.md config table, howto/communication.md Mirror section, HOWTO.md env row. Verified: harness +7 tests (47/47 green — false/true/yes/0/invalid/combined-order), bash -n, make gen && make check green.

  • 2026-08-13pos communication scrcpy --new-display support: new SCRCPY_NEW_DISPLAY config key (pos config scrcpy) — true/yes → bare --new-display (default size/dpi), 1920x1080, 1920x1080/420 or /240--new-display=<value>; inline validation in _mirror (err runs in the main shell, not a process-substitution subshell) rejects anything else with the accepted forms. Docs: POS_CONFIG header, POS.md command+config tables, howto/communication.md Mirror section, HOWTO.md env row, usage() example. CLI pass-through pos communication scrcpy --new-display=1920x1080 also works verbatim. Verified: harness +8 tests (40/40 green — WxH, true, WxH/DPI, /DPI, invalid-rejected, env>config, combined order), bash -n, make gen && make check green.

  • 2026-08-13 — Fix pos communication scrcpy mirror failure on the real box (ERROR: Unexpected additional argument: on every mirror, bare or with flags): _extra_flags() ran printf '%s\n' with an empty array expansion, which prints one blank line; _mirror()'s while read turned that into an empty-string arg passed to scrcpy. Fix: _extra_flags now returns early when SCRCPY_EXTRA_FLAGS is empty (and printf '%s\n' "${extra[@]}" when set), and _mirror defensively skips blank entries ([ -n "$f" ] && cmd+=("$f")). Rebuilt the stub-PATH suite (/tmp/scrcpy-run-test.sh, outside the wiped $TEST_DIR) — 32/32 green incl. the regression (bare mirror → zero args to scrcpy) and EXTRA_FLAGS + passthrough mixed. bash -n, make gen && make check green.

  • 2026-08-13 — Fix scrcpy apt install on the live box: preinstall apt install failed with Unable to locate package scrcpy (Debian/Ubuntu need contrib/universe for scrcpy, and the apt build is older anyway). Removed scrcpy from preinstall.sh PACKAGES (kept adb); scrcpy now installs via the existing optional app apps/media/scrcpy.sh (GitHub latest, bundles adb) — docs (POS.md, howto/communication.md) and the tool's deps-guard error reworded to lead with that path. Re-verified: bash -n, stub suite 21/21, make gen && make check green.

  • 2026-08-13pos communication scrcpy (bin/pos-communication-scrcpy): wrapper over scrcpy+adb for Android mirroring/control. Subcommands: bare scrcpy (mirror — config defaults + verbatim pass-through of any scrcpy flag; no device → friendly error + hints), devices (adb devices -l), record [file] [--headless] (default $SCRCPY_RECORD_DIR/<device>_<date>.mp4, --headless = --no-playback for headless servers), tcpip [port] (USB→wireless switch + prints connect with the auto-detected device IP), connect <ip[:port]> (adb connect + mirror -s), push (default /sdcard/Download = scrcpy's own default), pull, screenshot (adb exec-out screencap -p → PNG in RECORD_DIR), info (model/android/sdk/serial via getprop). Config scope scrcpy (~/.config/linux_post_install/scrcpy.env, pos config scrcpy): SCRCPY_SERIAL/MAX_SIZE/MAX_FPS/BIT_RATE/FULLSCREEN/RECORD_DIR/PUSH_TARGET/EXTRA_FLAGS, env-var precedence. Deps scrcpy + adb added to preinstall PACKAGES; docs note the apt build is older and point to the existing apps/media/scrcpy.sh app installer (GitHub latest, bundles adb) — researched 2026 releases (current v4.1). Conventions: # POS:/# POS_SUBCMDS:/# POS_CONFIG: headers, deps guards before -h|--help, no stdin → no INTERACTIVE_CMDS. Verified: bash -n, stub-PATH suite /tmp/opencode/scrcpy-run-test.sh 21/21 green (fake adb/scrcpy echo-args, HOME isolation, env/file precedence, rc paths, screenshot bytes), make gen && make check green, dispatch via pos communication scrcpy --help. Docs: POS.md communication table + detail block, howto/communication.md section, HOWTO.md index + env row, AGENT_Context Common Tasks row + gen'd tree/dispatch/filetable.

  • 2026-08-13 — Entertainment-module hardening (approved Tier 1 + Tier 2): delivery moved to notify_send (platform follows NOTIFY_PLATFORM, default Telegram) via lib/notify.sh sourced by bin/pos-entertainment-send; a last-run state is recorded per plugin (~/.local/share/linux_post_install/entertainment/last/<plugin> — rc + timestamp) on every non---print run and shown by pos entertainment status, which also lists installed-but-not-enabled plugins; a send that fails while fired by a timer (gated on $INVOCATION_ID) additionally notifies the configured platforms. New message-safe plugin lib lib/entertainment-plugin-lib.sh (defines only plugin_*, never writes stdout — the stdout contract stays "message only"): plugin_load_config (entertainment.env + env precedence), plugin_have, plugin_require, plugin_err, plugin_http_json <url> [--key <jq>] [-H <header>] (curl --max-time 20 --retry 2); weather/joke/gold refactored onto it. pos entertainment config gains get|unset|ls|edit (edit via the shared pos config UI — added to INTERACTIVE_CMDS). Tier 2: new shared lib lib/user-timers-lib.sh (only ut_*: ut_interval_to_oncalendar, ut_interval_label, ut_unit_name, ut_write_unit_pair incl. TimeoutStopSec=5s + Persistent + network-online deps, ut_ensure_linger, USER_SYSTEMD_DIR) dedupes the systemd user-timer machinery between lib/entertainment-lib.sh and lib/scheduler-lib.sh (the latter's sched_* duplicates deleted; both source it; collides-with-nothing). install.sh Phase 2 lib list += the two new libs; SCRIPTS.md/DEV.md/POS.md/howto/entertainment.md/AGENT_Context updated (hand-maintained lib rows: entertainment-lib 354→311, scheduler-lib 830→760, +112 user-timers-lib, +67 plugin lib). Verified: bash -n everywhere; smoke-tested in an isolated HOME=/tmp/enttest (status, config get/set/unset/ls, send path rc=0, failing plugin records rc=1, error-case message hygiene); make gen && make check green.

  • 2026-08-13 — Fast pos-unit shutdown: every systemd unit a pos tool writes (or systemd/ ships) now sets TimeoutStopSec=5s (+ KillMode=control-group on the daemons) so a stuck process can't stall a reboot for the 90s systemd default. Applied at all 7 template sites: pos-communication-telegram-listener, pos-communication-matrix-listener (also gained a trap 'kill $(jobs -p) 2>/dev/null; exit 0' TERM INT in run_daemon so stop returns sub-second), pos-network-download (aria2 + retry-healer units), lib/scheduler-lib.sh sched_write_units, lib/entertainment-lib.sh write_units, and systemd/{ssh-agent,autostart,usb-automount}.service. Legacy-unit cleanup: the repo no longer ships pos-health.{service,timer} / pos-entertainment.service (they were documented but postinstall never created them — found stale only on the live box, FAILED); removed their stale references from SYSTEMD.md (deleted the pos-health.service section + gating special-case, added a new Stop behavior section), POS.md, HOWTO.md, howto/system.md (now documents the pos system schedule job replacement + removal commands), AGENT_Context (tree, phase description, selfcontained table). DEV.md Best Practices gains a Systemd units convention (TimeoutStopSec=5s + TERM trap + regeneration caveat). Verified: bash -n on all edited scripts; make gen && make check green (filetable rows for the two listeners + network-download auto-regenerated, hand-maintained lib rows bumped 350→354 / 822→830). Live-box application is manual (this session was a Google Cloud Shell, not the real machine): regenerate units via pos network download start, pos communication telegram listener --enable, pos system schedule enable <job>, pos entertainment enable <plugin>, then sudo systemctl disable --now pos-health.timer pos-health.service 2>/dev/null; sudo rm -f /etc/systemd/system/pos-health.{service,timer} && sudo systemctl daemon-reload.

  • 2026-08-13 — Bootstrap output transparency (install.sh / preinstall.sh / postinstall.sh): removed the redundant apt update (preinstall.sh owns it — install.sh previously ran it twice, showing two identical OK apt update lines); Phase 2 now names what it installs — libs line (libs -> /usr/local/bin (644): common.sh flags.sh …), plugin names in the count line, x64_bin names, per-feature feature installed/overwritten + feature flag set logs with a N features installed: … summary — and the misleading "47 scripts + libs" label is fixed to 47 scripts + 6 libs (the 6 libs were outside the counter); preinstall prints Installing N packages (apt install -y): with the 40-name list wrapped at 80 cols; postinstall now logs silent skips — config/authorized_keys is empty — nothing to add (empty file previously looped zero times with no message), schedule.d already exists, keeping it (restructured the condition so the message is accurate when the dest exists vs config/schedule.d absent), and a per-service service enabled: <name> line. No output-layer changes (no --verbose, no log file — decided scope). Verified: bash -n + --dry-run smokes of phases 1/2/3 showing every new line (learned: install.sh:19 hardcodes export DRY_RUN=0, so an env DRY_RUN=1 is ignored — the flag --dry-run is required), hand-maintained filetable count rows bumped (install.sh 206→223, preinstall.sh 73→75, postinstall.sh 163→168), make gen && make check green. usb-automount left live (user choice).

  • 2026-08-13usb-automount feature, integrated exactly like autostart: features/usb-automount.sh (root-guard re-exec via sudo; first-root-run self-install of udev rule /etc/udev/rules.d/99-usb-automount.rulesACTION=="add", KERNEL=="sd[a-z]*", SUBSYSTEM=="block", ENV{ID_BUS}=="usb", TAG+="systemd", SYSTEMD_WANTS="usb-automount.service" — + udevadm control --reload + trigger --subsystem-match=block; an existing/edited rule is never overwritten; scans lsblk -J for unmounted removable partitions/raw whole-disk filesystems, mounts each at /media/<label> — vfat/exfat/ntfs world-writable via -o umask=000, fallback plain mount, label-collision bump -2/-3, no label → usb-<name>, logs ${HOME:-/root}/.usb-automount.log) + systemd/usb-automount.service (Type=oneshot, WantedBy=multi-user.target — boot + hotplug + manual systemctl start usb-automount), gated in postinstall.sh's systemd loop exactly like autostart (flag_is_set usb-automount → skip with hint). Purpose: a plugged-in stick is auto-mounted world-writable, ready for pos system backup's post-verify USB copy. Docs: SYSTEMD.md (service section + gating code block), SCRIPTS.md (feature section + systemd bullet + TOC), AGENT_Context tree + filetable rows (postinstall.sh count corrected 152→163 — it was already 6 lines stale), README index rows. Verified with a stub suite (/tmp/opencode/usb-automount-test — lsblk JSON fixtures, mount/mountpoint/udevadm/sudo stubs, MOUNT_BASE/UDEV_RULES_DIR seams, HOME isolation): 47/47 green. make gen && make check green. Gotcha learned: ${VAR:-{...}} with a { inside the parameter-expansion default mis-parses in bash (emits a stray } — printf of a multi-line value showed }}); avoid braces in :- defaults.

  • 2026-08-13pos system backup copies the finished backup to a USB stick. Detection runs after the archive verifies (so a stick plugged in while the backup ran is found; if none is mounted, one re-scan prompt before giving up — s skips, EOF from cron skips silently, rc stays 0). Single stick → y/N confirm; several → numbered pick (0 = skip). Copy lands in <usb>/backups/ (mkdir -p; chmod 600 best-effort — vfat chmod failures warn, never fail), and the transfer is proven 100% by sha256 source-vs-copy before any success is announced: mismatch → warn with both hashes + notify_send "USB copy FAILED…" + rc=1 (the ERR trap is re-armed mid-script so a USB-phase failure no longer notifies "Backup FAILED"). Detection: lsblk -J → recursive jq filter (rm==true && mounted && type part|disk, space-safe via JSON) or pinned BACKUP_USB_ROOT seam (= <root>/backups/, skips detection — also the test seam). Docs: usage() Environment, POS.md backup row, howto/system.md (USB section + env table + mismatch troubleshooting), DEV.md system.env list. Verified with a stub suite (/tmp/opencode/backup-test — sudo/gpg/lsblk/sender stubs, HOME isolation, per-test lsblk JSON fixtures, corrupting-cp + vfat-chmod override stubs): 40/40 green (skip s/EOF, seam y/n, detect single, multi pick 2/0, re-scan after replug, corrupt copy rc=1 + honest notify, vfat tolerance). make gen && make check green.

  • 2026-08-13pos share smb-server share now guards the two common NT_STATUS_ACCESS_DENIED causes at share time (warnings only): --users entries missing from the Samba passdb (pdbedit -L, cut to user column, grep -qxF per user — pointer to pos share smb-server adduser <user>), and ancestors of the share path lacking other:+x traversal (sticky dirs like /tmp count as traversable via the t slot; fix hint chmod o+x <dir>). Both wired into the share case after require_root_dir; howto/share.md SMB section + troubleshooting updated. Rooted in reports/bug-report-smb-server-access-denied.md (committed as the spec). Verified with a stub-PATH suite (/tmp/opencode/smb-test — pdbedit/systemctl/smbcontrol/testparm/smbpasswd stubs, SMB_CONF seam): 16/16 green.

  • 2026-08-13 — Docs hardening from the schedule-session review (sole-developer call: terse, session-learned). DEV.md §7 env-seam registry now lists USER_SYSTEMD_DIR (bin/pos-network-download, bin/pos-communication-{telegram,matrix}-listener, lib/scheduler-lib.sh) + the scheduler's SCHEDULE_* seams, and documents the missing-:--guard gotcha (a VAR="${XDG…:-…}" without leading VAR:- overrides the seam — stub runs then silently write to the real $HOME; fix: USER_SYSTEMD_DIR="${USER_SYSTEMD_DIR:-…}"). New-tool test checklist gains an env-seam review step (grep for unguarded config writes + prove with VAR=/tmp/x). §7 notes stub harnesses are throwaway by design — build in /tmp/opencode/<tool>-test/, leave there, keep only the pattern. howto/schedule.md documents that migrate copies the rule LHS verbatim as COMMAND (old tool never had disk root/loadavg shorthands — rewrite those jobs with real commands). make check green.

  • 2026-08-14 — Gitea Actions gate is now live and green end-to-end: act_runner (v0.6.1, labels ubuntu-latest) registered on 100.100.1.2 (~/srv/gitea/runner/, standalone compose next to the ScaleTail gitea; CONFIG_FILE=/config.yaml env required or run.sh never reads the config; --add-host gitea.skink-platy.ts.net:100.111.241.54 so the job container reaches gitea). First real runs caught a deterministic gen-drift: plain sort in scripts/gen-docs.sh is locale-dependent (category-less tool keys start with |, which collates after letters under the CI container's locale → pos-config/pos-tree reordered), so the git diff --exit-code step failed. Fixed with export LC_ALL=C in gen-docs.sh (byte-order sort) + regenerated DOC/AGENT_Context_Project.md (config/tree now sort after the letter categories); make check OK, make lint 0 FAIL / 0 WARN. Live CI verdicts: the run for e0b5b11 (workflow commit) and the empty trigger 98a767c both FAILED on the drift; the run for 9d058b7 (the fix) SUCCEEDED (🏁 Job succeeded).

  • 2026-08-14 — Gitea Actions gate added: .gitea/workflows/lint.yml runs make gen + git diff --exit-code (gen-drift) + make check + make lint on every push/PR. Verified locally the exact four steps pass (gen idempotent, check OK, lint 0 FAIL / 0 WARN). "no CI" lines updated in AGENTS.md (Quick facts → CI bullet, notes a registered act_runner is required) and DEV.md (stub harnesses note: CI runs static gates only, not behaviour suites). Gitea 1.26.4 confirmed reachable; runner registration completed the same day (see the entry above).

  • 2026-08-14 — Convention-drift maintenance fix session (completed the audit backlog MAINTENANCE.md, M-001..M-023, all VERIFIED; gate scripts/lint-conventions.sh + make lint now 0 FAIL / 0 WARN; make gen && make check green). P0 bugs: M-002/003/004 added docker-compose docker-vbox network-hotspot to INTERACTIVE_CMDS (stdin/log-pipe prompt swallow); M-005 install.sh --steps now expands documented N-M ranges via normalize_steps_spec() (dry-run verified); M-006 feature-vs-docs decision: --send/--markdown not restored (health is a console-only reporter by design since fe7708f; scheduler NOTIFY=always covers delivery) — 5 docs corrected instead; M-007 lib/notify.sh:57 fallback routed to stderr (stdout-leak on standalone source). P1: M-008..M-014 deps guards moved before -h|--help in docker-health/docker-ps (converted to command -v X || err), network-scan, share-usb-server, media-mp3/mp4 (guards before help with a --dry-run pre-scan preserving the documented no-deps preview); system-health documented as the sanctioned graceful-degradation no-guard pattern in DEV.md — lint refined accordingly (first_guard_line only matches real guards; first_line skips comments; precision fixes, not weakenings); M-015 system-firewall gained usage()+-h|--help (root-gated first; verified via sudo); M-016 ffmpeg added to preinstall PACKAGES. P2: M-017/M-018 autostart + usb-automount gained the feature-template preamble (flags.sh load, usage); M-019 chmod +x apps/media/scrcpy.sh; M-020 SCALE_DIR/CONFIG_ENV :- seams in pos-docker-compose (verified via overrides; follow-on fix: DIM color var missing from common.sh crashed pos docker compose config — added it); M-021 CONFIG_DIR centralized as the canonical XDG-aware seam in common.sh, per-file duplicates dropped (standalone-sourced notify.sh/config-ui.sh/matrix+telegram tools keep an identical guarded copy — "no shared lib? inline fallbacks"); M-022 plugin_* prefix collision resolved by renaming the internal registry helpers to ent_plugin_* (the documented plugin-authoring API plugin_have/plugin_require/plugin_load_config/plugin_http_json kept for user plugins); M-023 six tools (pos-config, pos-tree, pos-entertainment-{config,enable,disable,status}) now filename-referenced in DOC/POS.md. Hand-maintained AGENT_Context line-count rows bumped (install.sh 223→248, preinstall 75→76, common.sh 144→151, notify.sh 76→87 stale-corrected, autostart 14→50, usb-automount 134→138); make lint target wired in the Makefile. MAINTENANCE.md kept as the working record (uncommitted by design).

  • 2026-08-14pos system backup optional encryption (--no-encrypt flag + BACKUP_ENCRYPT=0 env, flag-or-env — user chose "Flag + env only"): plain path keeps a verified .tar.gz with no password prompt (headless/cron safe); encrypt path unchanged (prompt → gpg AES-256 → decrypt-verify; the gpg dep-guard moved into the encrypt branch so plain backups no longer require gnupg). Arg parsing rewritten as a loop over "$@" so pos system backup <folder> --no-encrypt works with the flag after the folder; usage() documents all three forms + the plain artifact name; # POS_FLAGS: --service --no-encrypt; config/system.env template gains #BACKUP_ENCRYPT=0; POS.md row + howto/system.md section updated. Verified: stub suite +2 cases (T18 flag / T19 env: plain .tar.gz artifact, gpg never called via $GPG_CALLED, USB copy + sha256 of the plain archive, notify wording) — 65/65 green; bash -n, make gen && make check, make lint 0 FAIL / 0 WARN.

  • 2026-08-14 — CI green-check via plain git (no SSH to the runner, no API tokens — user chose "CI tags + git ls-remote" + "scripts/ci-status.sh helper"): .gitea/workflows/lint.yml scoped to on: push: branches: [main] (tag pushes no longer re-trigger it) and the gate step now reports its own outcome as a lightweight tag — ci-ok/$GITHUB_SHA on success / ci-fail/$GITHUB_SHA on failure, pushed over HTTP with the jobs automatic GITEA_TOKEN to http://oauth2:${GITEA_TOKEN}@gitea.skink-platy.ts.net:3000/admin/Linux_post_install.git (runner container already host-maps that hostname to 100.111.241.54); steps.gates.conclusion decides ok/fail, if: always() (guarded to push events) covers failed gate runs, and an existing-tag guard makes re-runs idempotent. New executable scripts/ci-status.sh [--wait] [<sha>] reads the tags via git ls-remote (origin, CI_STATUS_REMOTE override): GREEN (0) / RED (1) / PENDING (2); --wait polls every 10s up to 10 min. DEV.md §CI gains a "Checking green without SSH" bullet. Verified: bash -n, yaml-parse OK, make gen && make check, make lint 0 FAIL / 0 WARN; first live-tag verification pending the push (fallback if Gitea clamps token-push: PAT as workflow secret).

  • 2026-08-14pos media sync (bin/pos-media-sync) — incremental Music → USB sync, plus the shared USB layer it builds on. New lib lib/usb-lib.sh (194 lines, installed by install.sh): usb_detect (lsblk JSON, TRAN + lsusb/by-id cross-check → USB_MOUNTED/USB_UNMOUNTED), usb_related_present, usb_mount_offer (/media/<label> mount-offer, usb-automount scheme), usb_pick_root <prefix> <subfolder> <giveup-msg> (detect → mount-offer → single/multi picker → USB_ROOT); seams USB_MOUNT_BASE/USB_BYID with BACKUP_MOUNT_BASE/BACKUP_USB_BYID aliases so existing system.env lines keep working; TRAN-fallback warning deduped to once per scan. pos-system-backup refactored onto it (216 lines, was 364) — re-ran the backup stub suite: 65/65 green. Sync tool: add/update only, never deletes (user choice); --mp3/--mp4 filter (neither = both), --source <dir> (default MEDIA_SYNC_SOURCE/$HOME/Music), --dry-run preview with counts; copies missing/changed (size/mtime) files into <usb>/Music/ (MEDIA_SYNC_DEST) preserving the tree via cp --preserve=timestamps; result notified via lib/notify.sh; media-sync added to INTERACTIVE_CMDS; deps guards (lsblk/jq) before -h|--help. Docs: POS.md media row, howto/media.md section, SCRIPTS.md lib section + Phase-2 lib list, system.env seams, DEV.md env-seam registry, AGENT_Context Common Tasks + hand-maintained lib row (+usb-lib 194) + gen'd tree/dispatch/filetable/flags. Verified: new stub suite /tmp/opencode/msync-run.sh 46/46 green (fresh/no-op/update/filter/dry-run/multi-stick/mount-offer/no-USB skip/never-delete/--source/TRAN-fallback/notify) — caught and fixed an inverted needs_copy return; make gen && make check, make lint 0 FAIL / 0 WARN; dispatch via pos media sync --help + pos media listing.

  • 2026-08-22pos media ytsync (bin/pos-media-ytsync) — incremental YouTube channel/playlist sync into ~/Videos, implemented per the Architect decisions D1D9 + Designer UX contract (reportAgents/2026-08-22-*.md). Subcommands add [url] / sync [name] / list / remove <name> + --dry-run; bare invocation = interactive menu (/dev/tty reads, EOF-safe, NOT in INTERACTIVE_CMDS so dispatcher tee logging is kept; empty state goes straight to the URL prompt). One yt-dlp call per new video (bestvideo*+bestaudio/best → MP4, metadata/chapters/thumbnail, --no-overwrites, --windows-filenames --trim-filenames 120, retries 3), per-video [n/N] title heartbeat lines, LF-only logs (spinner TTY-gated); probe = yt-dlp --flat-playlist -J parsed with jq, new-list diffed against the per-source --download-archive BEFORE downloads (exact counts, exact dry-run plans, zero speculative downloads). State machine-owned outside ~/Videos: $YTSYNC_STATE_DIR/{registry(\x1f-delimited slug⇥type⇥url⇥subdir⇥playlist_title⇥added_ts), archive/<slug>.txt, history.log}, atomic temp+mv writes; remove keeps files AND archive (re-add resumes incrementally); ?v=+&list= URLs download the single video only. Exit codes: 0 incl. no-op/cancel/non-tty-guard; 1 reserved for missing deps, invalid explicit URL, unknown/ambiguous name, wholesale source failure. Notify digest only when new>0 or failed>0 (+ ERR-trap alarm around download passes) via opt-in lib/notify.sh. Config scope ytsync: YTSYNC_VIDEOS_DIR / YTSYNC_EXTRA_ARGS (pos config ytsync); automation documented as a pos system schedule job (COMMAND=pos media ytsync sync, NOTIFY=never). Deps guards before -h|--help with yt-dlp+jq active under --dry-run (the preview IS the probe; ffmpeg skipped there). Docs: tools-docs/ytsync.md (new dir), POS.md media row + notes, HOWTO.md row, howto/media.md section + troubleshooting, AGENT_Context Common-Tasks row, bin/pos EXAMPLES line. Verified: make gen && make check && make lint 0 FAIL / 0 WARN; PATH-stub yt-dlp suite (add happy path, incremental 0-new idempotency, playlist NNN numbering, dry-run zero writes, non-tty guard rc0, remove-keeps-archive) with real $HOME byte-untouched via seams.

  • 2026-08-23 — ytsync post-review fixes (from reportAgents/2026-08-23-reviewer-ytsync.md, ACCEPT_WITH_NITS): classify_url now treats youtu.be/<id> short links (with or without &list=, incl. scheme-less + ?si= forms) as single videos — same path as ?v= — so they get type video + --no-playlist instead of being misfiled as playlists; usage() watch-link note reworded; tools-docs classification table gains the short-link row and the invocation block gains the previously undocumented --convert-thumbnails jpg; POS.md/howto media wording extended. Verified: classify_url matrix (6 URL shapes) + stub-PATH end-to-end add (registry type=video, download call carries --no-playlist + canonical watch URL); make gen && make check && make lint 0 FAIL / 0 WARN.

  • 2026-08-23 — ytsync menu render bugfix (bin/pos-media-ytsync, live-box report): cut -d'·' at :292/:301 used U+00B7 = 2 bytes UTF-8 (GNU cut is byte-oriented → "delimiter must be a single character", masked by || true so the · last run … suffix and LAST SYNC column never rendered); replaced with grep/tail capture + ${last%% ·*} parameter expansion (semantics identical incl. empty-string=no-last-run); :1053 printf '----…\n' format starting with - parsed as invalid option → printf '%s\n' '----…'. Chain: Detective root cause (reportAgents/2026-08-23-detective-ytsync-menu-errors.md) → Builder 3-site fix (-builder-ytsync-menu-fix.md, pty probe: suffix + separator render, zero stderr noise) → Reviewer ACCEPT-WITH-NITS (-reviewer-ytsync-menu-fix.md delivered inline). Gates re-run by Orchestrator post-review: make gen idempotent, check OK, lint 0 FAIL / 0 WARN.

  • 2026-08-23 — Menu Phase 1 (user-ratified decision "b"): category-neutral menu library extracted from share-suite Pattern B + four P1 tool menus. New lib/menu-lib.sh (169 ln): menu_guard/menu_run/menu_pick/menu_ask_value (stderr render, /dev/tty reads, EOF fail-closed rc=1, index/value→stdout); lib/share-lib.sh (436→318) keeps its public names as pure delegating shims so all five pos share * tools stay untouched; install.sh Phase-2 explicit lib list += menu-lib.sh. Opt-in no-args+tty front doors (or menu verb, # POS_SUBCMDS: registered, completions regen'd) on pos media sync (164→216: Sync-now/Preview/mp3/mp4/source-folder items), pos system backup (216→292: typed/service-root/plain variants, every backup behind folder-naming y/N), pos docker compose (366→487: ls/up/down/restart/logs/update/config items, down/restart/update confirm-gated naming the stack), pos system schedule (81→151: list/status/run-now(confirm)/enable/disable/editor — timer-invoked run <name> verb dispatch byte-identical to HEAD). INTERACTIVE_CMDS unchanged; all CLI verbs byte-compatible. Docs: POS.md ×4 rows, DEV.md lib row, SCRIPTS.md sections, AGENT_Context rows + GEN. Chain: Explorer survey (37 tools, reportAgents/2026-08-23-explorer-pos-menu-survey.md) → Designer classification (-designer-pos-menu-suitability.md: 14 MENU-FIT / 7 CONDITIONAL / 16 NO-FIT) → Builder T1/T2/T3 (-builder-t1-menu-lib-extraction.md, -t2-p1-menus-media-backup.md, -t3-p1-menus-compose-schedule.md; T3 discloses a mid-verify symlink clobber restored+re-verified) → Reviewer ACCEPT_WITH_NOTES over the consolidated diff (-reviewer-phase1-menu.md, T3 integrity clean). Verified: bash -n ×7, pty probes (render/quit/EOF/non-tty fail-closed/destructive prompt-abort), gates green after each pass and re-run by Orchestrator post-review (make gen idempotent · make check OK · make lint 0 FAIL / 0 WARN). Open for later phases: P2 (docker-vbox, network-download), firewall style-migration decision, usb-server menu in POS_FLAGS nit (owning track).

  • 2026-08-23 — Menu Phase 2 + firewall style-migration (decision "a" activated: P1 landed, lib/menu-lib.sh exists). pos docker vbox (157→261): 6-item menu hub over the inline case verbs via a quoted self-invocation menu_self (verbs never re-enter the menu → no recursion); enter hands over the terminal and returns to the loop; rm/create behind VM-naming y/N. pos network download (950→1104): 13-item top-verb map onto existing cmd_* fns — add URL (menu_ask_value, optional --tmux), gid-pick → info/pause/resume/remove/restart (remove names name+gid before delete), typed-confirm purge, watch handover, daemon start/stop (stop confirmed); non-fatal RPC liveness gate (-m 3) keeps queue views alive on a dead daemon; deliberately NOT added to INTERACTIVE_CMDS — menu-lib's tty-guarded reads make membership unnecessary and keep tee-logging for all scripted verbs (survey E-002; Reviewer traced the lint pass as honest through uses_stdin). pos system firewall (308→325) migrated to repo-standard mechanics ONLY: menu heredoc render → stderr { … } >&2 (body byte-preserved), all 38 interactive reads → /dev/tty via tool-local tty_read() (EOF/no-tty → pointer + rc1, never hangs), prompt_ipver de-command-substituted so EOF exits gracefully; root gate / per-cmd confirm / typed RESET / pager / notify / every ufw invocation untouched. Both new tools register # POS_SUBCMDS: += menu; POS.md rows updated; GEN regen'd. Chain: Builder T4 (reportAgents/2026-08-23-builder-t4-p2-menus-vbox-download.md; correctly caught an Orchestrator brief error claiming download was in INTERACTIVE_CMDS) + T5 (-t5-firewall-menu-migration.md; pty parity captures vs pre-edit baseline) → Reviewer ACCEPT-WITH-NITS over both (-reviewer-phase2-menu.md, transcribed by Orchestrator; recursion/injection analysis, 13/13 mapping proof, four T5 intents verified hunk-by-hunk). Verified: bash -n ×3 + gates green after each pass; final trio re-run by Orchestrator post-T5 — make check OK · make lint 0 FAIL / 0 WARN (76s under box load ~7; the earlier apparent lint hang was shared-box CPU contention, no code issue). Remaining notes for later sessions: errexit kills whole menu when a backing verb hard-fails (repo-wide pattern, all six menus); confirm() EOF hits set-u unbound yn (pre-existing common.sh); vbox create EOF at dir prompt degrades to default while name/image prompts abort (cosmetic).

  • 2026-08-26pos ai alias activation rework (Option B) + pos config listing readability, per the 2026-08-26 Architect/Designer specs (AgentsReport/{architect,designer}/2026-08-26-*.md). Alias activation: the stale sourced-snapshot mechanism is gone — every pos ai alias invocation runs _alias_sync() (two-way reconciliation: render-diff-install of one executable wrapper per ENV record at ~/.local/bin/<name> chmod 755 via mktemp+mv with a bash -n pre-commit guard; marker-guarded deletion of owned wrappers missing from ENV; legacy ai-aliases.sh generation stopped and generator-marker-guarded auto-removal with an unalias <names> remediation hint; loud PATH guidance when ~/.local/bin is off PATH). Edits are live on next invocation with no shell reload (kills the reported stale-gemini-alias bug class); create refuses foreign-file and PATH-binary collisions; show gains the wrapper path; pos-system-uninstall sweeps the wrappers by their line-2 marker in discovery+removal. Dup-table menu bug fixed with a single _alias_table renderer (menu option 4 returns to the loop whose pre-render already shows fresh state). Config readability (lib/config-ui.sh, fully generic): new optional # POS_CONFIG: field types — @Caption / @[KEY=v1|v2] Caption group captions (condition evaluated per render via cfg_value; inactive groups dimmed with a textual reason, never hidden → numbering stable; empty-alt segment = unset-as-default) and *providers=<tag> adapter filtering (zero match warns once + suppresses its caption); uniform typography tier for ALL scopes (bold title/keys, CYAN rule, dim numbers/placeholders/examples/captions, hanging-indent wrap clamped 60120 cols, whole render block → stderr per menu-lib house pattern, honest prompt Number to edit [r=refresh, q=quit]:); masking/edit flow byte-compatible, no per-scope branches. bin/pos-ai line-6 header adopted to the caption/tag syntax (single-line change). Verified: stub-PATH harness (HOME=/tmp/…, CONFIG_DIR seam, argv-capturing pos shim) covering %q quoting round-trips (quotes/backticks/$()/%/unicode), staleness kill-test, orphan retraction, collision-refusal matrix, legacy migration (marker + foreign), PATH-absent warning, non-tty guard, idempotent double-sync; rendered-output diffs vs Designer mockups for ai AND old-format system; gates make gen && make check && make lint 0 FAIL / 0 WARN.

  • 2026-08-27 — Critical fix: paste injection + multiline paste in pos ai alias's Insert Prompt (root cause: menu_ask_value → plain line-oriented read -rp; a multi-line Ctrl+V paste floods the tty queue, read consumes only line one and the rest execute as commands later or get eaten by the next prompt — user-verified $(whoami)/; ls/sudo apt update behavior). New menu_read_value() in lib/menu-lib.sh (169→362): raw-mode (stty -icanon -echo -isig min 1 time 0) bracketed-paste-aware value reader — \e[?2004h/l markers, text inside [200~…[201~ inserted LITERALLY (embedded newlines/CR are data), Enter submits only outside a paste, Backspace/DEL/Left/Right/Home/End/Delete/Ctrl-U edit, Ctrl-D-on-empty + Ctrl-C/Z/\ cancel (terminal restored first); bytes read chunk-wise via dd bs=4096|od -tx1|tr — NOT bash's read builtin, which self-interrupts on an ETX byte from a tty even with ISIG disabled (SIGINTs the whole script on Ctrl-C); confirmed read -erp (readline) atomically consumes a paste but returns only its first line, so a custom reader was required. bin/pos-ai-alias (712→760): _alias_prompt_encode/_decode (backslash→\\, newline→\n; literal [ = ] comparisons — bash case patterns don't match a single backslash), _alias_prompt_truncate newline-safe + max-length arg; load/save encode/decode the prompt field; edit wizard shows a truncated display default but Enter restores the FULL original prompt (fixes pre-existing silent truncation of >80-char prompts), empty-original Enter continues. Verified: pty harnesses (/tmp/pty_{menulib,cancel,e2e_alias}.py, /tmp/roundtrip_test.sh) — bracketed multiline paste captured verbatim incl. C:\temp\note/$(whoami)/; ls/echo test/sudo apt update, nothing executed, clean exit; single-line paste; Ctrl-D and Ctrl-C both cancel cleanly (CANCELLED→DONE, terminal restored); full create→list→show→edit E2E with decode round-trip and Enter-keeps-full; bash -n ×2, make gen && make check, make lint 0 FAIL / 0 WARN.

  • 2026-08-27 — Configurable AI-bridge trigger word for the Telegram listener: the hard-coded ai prefix in pos-communication-telegram-listener became TELEGRAM_AI_PREFIX (default ai) — messages starting with <prefix> (case-insensitive, literal match) are forwarded to Gemini. New prefix verb: pos communication telegram listener prefix shows the current word, prefix <word> sets it (validated [A-Za-z0-9][A-Za-z0-9_-]*, writes TELEGRAM_AI_PREFIX to telegram.env chmod 600); also editable via pos config telegram (field added to the sender's # POS_CONFIG: telegram scope — registry-driven, no code in config-ui). Matching is per-message hot-reloaded (like the command map — no daemon restart), via scoped shopt -s nocasematch + quoted-literal =~ prefix (bash case patterns can't do literal-then-whitespace + case-insensitivity in one test); ai_bridge_prefix() precedence: telegram.env > env from load_config > default ai. --status shows the current prefix; usage + # POS_SUBCMDS: prefix added (completions regenerate). Preserved edge: bare ai (no trailing space) never matched the old regex, so it still falls through to "Unknown command". Docs: POS.md listener rows/paragraph, howto/ai.md Telegram section + troubleshooting (also corrected a stale claim that AI errors reply with a pos config ai hint — code replies AI error: … only). Verified: function-level routing harness (/tmp/ai_prefix_routing_test.sh — extraction of the real listener functions + PATH stub pos): default ai/AI routes, bare-prefix and unknown-command fallthrough, ai /reset and custom-bot /reset reset the session, custom bot/BOT routes and old ai no longer routes, per-message hot-reload after removing the var; CLI verb tests (show/set/invalid rc 1/leading-digit/--status); dispatch smoke pos communication telegram listener prefix + flat form; pos config telegram render shows the field; bash -n ×2, make gen && make check, make lint 0 FAIL / 0 WARN.

  • 2026-08-29 — Generic text-prefix map for the Telegram listener (user's clarification superseding the scalar TELEGRAM_AI_PREFIX setter): bin/pos-communication-telegram-listener (623→782) now routes any non-command message <word> <text> to a mapped command with <text> appended as ONE quoted argument — opencode=opencode turns "opencode check cpu" into opencode "check cpu". New map file telegram_prefixes.env (chmod 600, re-read per message, @quiet values, 120s cap, empty→OK, exit <rc> reply, syntax-checked on save, first-file-match wins, case-insensitive, word must be space-delimited so bare <word> still falls through). Routing order: text-prefix map → built-in Gemini ai bridge → /command map → Unknown (a mapped ai shadows the bridge). prefix verb reworked: bare = list map + bridge word; prefix <word> <command...> = map (validated [A-Za-z0-9][A-Za-z0-9_-]*, bash -n via check_syntax); prefix <word> = show one; prefix -r <word> = remove; the AI-bridge word itself is now set ONLY via pos config telegram (TELEGRAM_AI_PREFIX, default ai--status + bare prefix still display it). run_and_reply() extracted to share /command-map (60s) and prefix (120s) execution semantics; dispatch passes "${@:2}". Docs: POS.md listener rows/paragraph, howto/communication.md bullet, howto/ai.md (prefix-map + shadowing), usage(), # POS: header, AGENT_Context regen. Verified: routing harness /tmp/prefix_map_routing_test.sh 27/27 (ai-bridge regression incl. /reset, opencode remainder=ONE arg, case-insensitivity, bare/trailing-space fallthrough, shadowing, no partial-prefix false match, exit/OK/@quiet/env-expansion, /command-map regression via run_and_reply); CLI verb suite (set/show/remove/missing rc 1/invalid word rc 1/invalid cmd rc 1 — fixed latent set -e cmdsubst abort on syntax errors); dispatch smoke nested + flat + --status; pos config telegram render; bash -n, make gen && make check, make lint 0 FAIL / 0 WARN.