Architect-approved (C): install.sh now skips+aborts (exit 0) when the installed version equals the current git-derived version, with a --force flag to re-install. Version scheme 0.0cN (N = git commit count) bumps by construction on every commit. - install_version(): derives 0.0c$count via git rev-list; empty when .git absent (gate skipped); INSTALL_VERSION_OVERRIDE presence-check seam for tests (empty override simulates no-git deterministically) - Gate after arg parse, before phases; numeric comparison (strip 0.0c, -eq); messages: 'Already installed (X). Use --force to re-install.' and '(dry-run) Would skip install: already at version X', both exit 0 - flag_set installed_version after 'Bootstrap complete' banner (not in dry-run, even under --force, never on phase failure) - Docs: SCRIPTS.md flag table + gate description, AGENT_Context line count 248->301 + flags + flow diagram, README --force row, tests/README row, AGENT_TODO Done entry Verified: new tests/t-install-version.sh 21 checks (9 contracted cases, real install.sh + hermetic env seams); suite 20 files / 461 checks / 0 fail / 0 skip; make gen byte-idempotent; make check OK; make lint 0 FAIL, 0 WARN; bash -n clean; git diff --check clean; Reviewer APPROVE_WITH_NOTES with 3 mutation disproofs (8/21, 9/21, 4/21 fail)
99 KiB
AGENT_TODO — Worklist & Idea Backlog
Living list of what we are doing, what is next, and what we might do later.
Deep history lives in git: git log --follow AGENT_TODO.md, git blame, and
the individual feature commits — the Done section below is just a readable
summary (newest last).
Conventions
- Now — items actively being worked on this session (only a few).
- Next — queued, well-scoped items.
- Later — idea backlog. Ideas marked NOT NOW were evaluated and rejected for the stated reason; revisit only if circumstances change.
- When a task is completed: move it from Now/Next into Done (dated one-line) in the same commit that finishes the work.
Now
Next
-
- Wire alerting into more tools as they are added (default: source
lib/notify.sh, callnotify_sendon success/failure).
- Wire alerting into more tools as they are added (default: source
Later
- Tier 2:
pos healthextras — temperature/fan/load average thresholds,ss -tlnport checks for known services, SMART status for disks. - Tier 3: backup rotation + remote target — keep-N rotations, upload to
rclone remote after verify,
--remoteflag, digest reports rotation age. - Tier 3:
pos secretvault — gpg/age-encrypted key-value store; backend for future tools that need stored tokens. - Tier 3:
pos inventory— machine manifest (OS, packages, services, mounted disks, USB devices) exportable as markdown/JSON. - Tier 4:
pos self update— pull repo,make gen && make check, re-run install.sh to refresh/usr/local/bin. - Tier 4:
pos new— scaffold a new tool fromtemplates/pos-tool.sh(category, name, POS header, exec bit, doc stubs). - NOT NOW: per-category
bin/subdirectories — flatbin/+ filename dispatch scales fine; revisit only ifbin/passes ~40 files. - NOT NOW: split
lib/entertainment-lib.sh— fine under 600 lines; revisit if it grows.
Done
-
2026-09-08 —
install.shversion gate (Architect→Builder): skip+abort when installed version == current version,--forceto bypass, version scheme0.0c<git commit count>(auto-bumps per commit).install_version()derives0.0c$(git rev-list --count HEAD); empty when.gitabsent → gate skipped (silently);INSTALL_VERSION_OVERRIDEenv var (presence-check) = test seam. Gate after arg-parse, before phases, numeric comparison (strip0.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=0init,--forceparse + usage.flag_set installed_version "$CURRENT_VERSION"after "Bootstrap complete" banner (only when DRY_RUN≠1 and version non-empty; even under --force). Newtests/t-install-version.sh(21 checks / 9 cases). Docs: README/SCRIPTS/AGENT_Context (flags, flow, line count 248→301, tests/README row). Verified:bash -nclean;make genidempotent;make checkOK;make lint0 FAIL / 0 WARN;make testsuite green. -
2026-09-07 —
pos aiAPI-key contract mismatch fixed (Architect→Builder→Reviewer; docs/history evidence): docs claimedAI_API_KEYwas the required primary key, butresolve_key()read only provider-specific keys (7ae2e77had removed shared-key priority to fix cross-provider leakage; docs never updated). Decision C: provider key stays primary (leakage guard intact), legacyAI_API_KEYhonored as backward-compat fallback when the provider's own key is empty;cmd_providers()"configured" mirrors it;require_key()messages byte-stable;AI_API_KEYNOT 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 regressiontests/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 genbyte-idempotent;make checkOK;make lint0 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,postool 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 check→make lint0/0 →make test18 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-06 —
pos aiOpenRouter 402 + unbounded session — Architect→Builder→Reviewer. User hit OpenRouter 402 on theassistalias: "You requested up to 131072 tokens, but can only afford 4511" — no provider sentmax_tokens, so OpenRouter pre-bills the routed model's full worst-case output (131072 onopenrouter/auto); user also asked to bound session history to last-5 requests. Architect decisions:AI_MAX_TOKENS(num, default 2048, real cost cap) sent asmax_tokenson OpenRouter andgenerationConfig.maxOutputTokenson Gemini (llamacpp skipped — local/free);AI_SESSION_TURNS(messages, 2 per exchange; default 40 kept back-compat; 10 = last 5 conversations) resolved lazily insession_pushbecause config loads after the hardcoded line-25 default; both registered in thebin/pos-ai# POS_CONFIG:@Generalsection → visible inpos config ai. Reviewer hardening (CHANGES_REQUIRED → fixed): unguarded env input could reach jqtonumber(0/-5/010/abc) — both providers +session_pushnow 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 genidempotent,make checkOK,make lint0/0,make test17 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-06 —
pos ai aliascreate-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:410passed""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 featuref61766b0/9f289ba3/300b742a), NOT a 2026-09-06 regression; 11 othermenu_ask_valuecallers correct (6 external rely on empty=cancel, 4 pass defaults) → no global semantic change allowed. Architect: opt-in--allow-emptyflag onmenu_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 --versionprints to STDERR —detect_llama_version's2>/dev/nullswallowed it → "installed llama.cpp unknown"; (2)printf|grep -qunderpipefail→ SIGPIPE rc=141 race randomly rejecting valid flags from the 59 KB--help; (3)resolve_modelexpected flat files but the HF downloader creates<models>/<repo>/file.ggufdirs →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_businlib/common.shpre-flights all three tools;--no-unitdirect-run escape hatch for SSH/headless; candidates narrowed tollama-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.3→1misparse). 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 genidempotent,make checkOK,make lint0 FAIL / 0 WARN,make test269/269 (~49 s),bash -nclean,git diff --checkclean. Post-fix: user's machine needed onlyexport XDG_RUNTIME_DIR=/run/user/1000(linger already on) → Option A systemd-managed server works, or--no-unitfor 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_IDregistry/docs; MatrixMATRIX_ROOM_IDrequired; gpg--passphrase-fd 3(no argv secret);/dev/tcppositional-arg form (checkport/smb-client/share-lib/NET_PROBE incl. escaping\$1/\$2); eval--no-command-executionnow 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 areRestart=alwaysso 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 viasystemd-analyze verify; hf single-file failure rc/exit-0 + meta-write bug fixed;LLAMACPP_HOSTcoherent;# POS_SUBCMDS+ metadata gaps closed. Tooling: lint-conventions rewritten Bash-native (~24-30× faster, rules byte-identical,:numrestored on 2 WARNs, planted-violation negative verified);pos system uninstallcovers all 12 libs + scale-tail + flags dir + systemd USER units (|| true) + de-hardcoded plugin markers; safe anchored.bash_completion/.bashrcremovals replacesed -i '/pos/d'. Config: canonicalload_env_fileinlib/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.shzero-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 genidempotent,make checkOK,make lint0 FAIL, 0 WARN,make testgreen,bash -nclean,git diff --checkclean;systemd-analyze verifyPASS 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 +
aiapp category +pos ai serverinstall-hint wiring: newapps/ai/llamacpp.sh(idempotentinstall_llamacpp()/uninstall_llamacpp()— GitHub release archive: scans/releases?per_page=10for the first-bin-ubuntu-{x64,arm64}.tar.gzasset sincereleases/latest(v0.4.0 milestone) ships no binaries; installs to/usr/local/lib/llama.cpp-<tag>with--strip-components=1, symlinks everyllama*binary into/usr/local/bin; uninstall removes the lib dir and only the symlinks whose target points into it);apps/install.shCAT_NAMES +=[ai]="AI / ML";templates/app.shcategories 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-serverhelp "Requires:" + botherr "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 notesbash apps/install.sh llamacpp. Verified: live API probe confirmed real asset naming —llama-<tag>-bin-ubuntu-x64.tar.gz/-arm64.tar.gzon nightly bNNNNN releases, top-level dir present,.tar.gznot.zip(sotarreplaces the brief'sunzipstep — no unnecessary apt install);bash -ntouched scripts;bash apps/install.sh --uninstall llamacppresolves app + idempotent uninstall rc 0 (no network);bash apps/ai/llamacpp.sh uninstallrc 0;make genidempotent;make checkgreen;make lint0 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-serverfound 2 BLOCKING + 5 REQUIRED; Builder fixed F1 (--include/--excludenow bash-caseglob filtering — array-shape-safe, no jq regex interpolation, composes gguf→filename→include→exclude), F2 (ExecStartrebuilt as a single-line, correctly-quoted command —systemd_quote()for executable + model path,systemd-analyze verifyrc=0, dry-run byte-identical), F3 (--branch/--revisiontreated as aliases, last-arg-wins, deadBRANCHvariable removed), F4 (parallel download drains ALL jobs — per-pidwait+ failure collection, honestDownloaded: X of Y files, N failed: …summary, exit rc=1, no.hf-metamarking a half-downloaded model complete, EXIT-trap temp cleanup), F5 (detect_llama_versionguarded (missing binary → clean error, never crash),validate_requested_flagserrors 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 syncedllamacppintobin/pos-aiusage() lines 42/59 +DOC/POS.mdAI_PROVIDER row(gemini\|openrouter\|llamacpp)+DOC/howto/ai.md(adapter list,--providerbackends, backward-compat shorthand sentence, "Available providers" table row). Final Reviewer acceptance: APPROVE_WITH_NOTES, 0 REQUIRED. Verified:bash -nallbin/pos*;make genidempotent;make checkgreen;make lint0 FAIL / 0 WARN; probe matrix — spaced-model-path unit (systemd-analyze verifyrc=0 + 16-token word-split), forced-failure parallel download (rc=1, named failed file, no meta), cache clear deny/accept,--slotsrejected with version-aware error,statuswithout llama-server clean error. -
2026-09-06 — Convention sweep — llamacpp doc/usage sync:
bin/pos-aiusage() provider lists (lines 42/59) now include llamacpp;DOC/POS.mdAI_PROVIDER config row(gemini\|openrouter\|llamacpp)(Builder's 3 hand-edits verified consistent end-to-end);DOC/howto/ai.mdadapter list,--providerbackend list, backward-compat shorthand sentence, and "Available providers" table row all include llamacpp (facts fromlib/ai-providers/llamacpp.sh). Verified:bash -nallbin/pos*;make genidempotent;make checkgreen;make lint0 FAIL / 0 WARN. -
2026-09-05 —
pos ai hfparallel downloads + advanced features (commits387f23f,0856b25): up to 4 concurrent file downloads (PARALLEL_DOWNLOADS=4, env/config seam), newinfo/filessubcommands,--include/--excludeglob filtering,--revision(commit/tag/branch), refactoredhf_gguf_quant_gate(), better progress feedback + error messaging, cache-management framework stub. Full backward compatibility preserved. Verified:bash -n;make gen && make checkgreen at commit;make lintre-verified 0 FAIL / 0 WARN during the 2026-09-06 restore (POS_EXAMPLES dedupe). -
2026-09-05 —
pos ai serveradvanced options (commit0856b25): 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 checkgreen at commit;make lintre-verified 0 FAIL / 0 WARN during the 2026-09-06 restore. -
2026-09-06 —
pos ai llamacppprovider forwarder + shorthand: newbin/pos-ai-llamacppthin forwarder (byte mirror of the gemini forwarder,# POS_SUBCMDS: ask chat models sessions capture),llamacppdispatch case inbin/pos-ai(pos ai llamacpp <subcmd> …≡pos ai --provider llamacpp <subcmd> …),ai-llamacppadded tobin/posINTERACTIVE_CMDS (chat reads stdin → tee-pipe guard), POS.md hand-edits (--providerrow + backward-compat sentence). Verified:bash -n;make genidempotent;make checkgreen;make lint0 FAIL / 0 WARN; smoke —pos ai llamacpp --help/providers/askall parse as provider llamacpp (no "Unknown ai subcommand"; curl connect error only when no local llama.cpp server, expected). -
2026-09-05 —
pos ai hfrecursive+filter+quant+list overhaul:hf_repo_files()now fetches…/tree/{branch}?recursive=truevia the newhf_paginate()(walksLink: rel="next"pages, concatenates withjq -s 'add', hard capHF_MAX_PAGES=20);hf_api()gains an optional header-dump arg + absolute-URL support (backward compatible).HF_GGUF_FILTERverbatim exclusion constant (.ggufsuffix, case-insensitive,mmproj|imatrix|clip|vision|projector|mtpexcluded) fixes--ggufselecting only mmproj files on quant-directory repos; newhf_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); newhf_list_files()+--listremote-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(genericorg/model-GGUF, no repo hardcoding), completions regenerated. Verified: stub harness/tmp/opencode/hf-test/run-tests.sh25 cases / 97 assertions green (20 core + 5 optional); live smoke recursive tree shape OK;bash -n;make gen && make checkgreen;make lint0 FAIL / 0 WARN. Chain: Detective (root cause) → Architect (decisions) → Builder → Reviewer. -
2026-09-04 — Fix
pos ai hf download --ggufcrashing withjq: 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}— norfilenamefield), so.rfilenamewas null for every entry; the--gguffilterendswith(.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);--gguffilter gains atype == "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.sh12/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--ggufcommand now downloads[1/10] …;bash -n;make gen && make checkgreen;make lint0 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) inyt-dlp --flat-playlistmode, so ytsync tried to download the channel ID as a video and failed with "This video is unavailable". Fix: probe-time canonicalization — newcanonical_channel_url()called at the top ofrun_probe()appends/videosto bare channel URLs (works foraddANDsyncof already-stored bare-handle registry entries, no migration; explicit tabs/videos|shorts|streams|live|playlists|featured|…untouched;?v=/?list=/youtu.beuntouched);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-runnow resolves3Blue1Brown (channel · 151 videos)with real titles. Verified: stub harness/tmp/opencode/ytsync-test/run-tests.sh32/32 green;bash -n;make gen && make checkgreen;make lint0 FAIL / 0 WARN. Chain: Detective (root cause + spec) → Builder → Reviewer APPROVE_WITH_NOTES. -
2026-09-04 —
pos ai hf(bin/pos-ai-hf) — Hugging Face model downloader. Subcommands:download <repo-id> [filename](single file, whole repo,--gguffilter,--branch <rev>,--output <dir>),search <query>,list,remove. Downloads to~/.local/share/linux_post_install/ai/models/<namespace>-<model-name>/(seam-guardedHF_DOWNLOAD_DIR), writes.hf-metaJSON per repo, prints structured summary (📥/📁). Config extends the existingaiscope via# POS_CONFIG: ai—HF_TOKEN(secret) andHF_DOWNLOAD_DIRin~/.config/linux_post_install/ai.envwith env-var precedence. Auth on all requests; HTTP 429 rate-limit sleep + retry once; resume viacurl -C -; progress bars to stderr. Deps:curl/jqguards before--help; no stdin → not in INTERACTIVE_CMDS. Verified: stub-PATH suite/tmp/opencode/hf-test/run-tests.sh46/46 green (argument parsing, download single/multi/gguf/branch/output, search, list, remove, config/token, output format);bash -n;make gen && make checkgreen;make lint0 FAIL / 0 WARN. Docs: POS.md ai row + detail block. -
2026-09-04 —
pos 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/--videooverrides andGRAB_DEFAULTconfig (pos config grab, defaultvideo) for unknown domains;--bestdefault for video (non-interactive,--worstoverride); 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) gainsurl_detect+ a URL routing step between the prefix map and AI bridge — bare http(s) URLs route topos media grab --best(600s timeout). Verified:/tmp/opencode/media-grab-test/run-tests.sh28 cases / 70 assertions green;bash -non both files;make gen && make checkgreen;make lint0 FAIL / 0 WARN. -
2026-08-21 — Share suite interactive layer (
lib/share-lib.sh+ menu modes for all fivepos share *tools): bare invocation now opens an EOF-safe looping menu instead of printing usage. Newlib/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_proberc-only probes,share_usb_records(blank-line-record parser for usbsrv listings),share_smb_shares(smbclient-gDisk 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 advisoriesshare_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_USERcontract), 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/posINTERACTIVE_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.shsection, 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/systemmd5-verified untouched;make gen && make checkgreen,make lint0 FAIL / 0 WARN. -
2026-08-21 — Refreshed
AGENTS.mdagainst the codebase: HOWTO category list corrected to matchDOC/howto/*(ai/share/schedule, no bare "usb"); CI bullet now states only verifiable facts (lint.ymljobgates, push-to-main/PR,ci-ok/<sha>/ci-fail/<sha>result tags) instead of the uncheckable act-runner naming; new Doc conflicts bullet encoding theMAINTENANCE.md → Phase 0authority order andtemplates/*.shas required starting points. Every other claim re-verified againstscripts/{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-15 —
pos docker stack(bin/pos-docker-stack) — containers grouped by their Docker Compose project. Each stack is a section (project name, sorted) with linescontainer-name status ports; containers with no compose project land in aStandalonesection at the end; ends withStacks: N containers: N standalone: N. Running only by default,-a|--allincludes stopped/exited (likedocker ps -a). Status colored on a terminal (Up*green,Exited*/Dead*/Created*red,Paused*/Restarting*yellow); exit 0 also when no containers. Data viadocker pswith--format '{{.Names}}{{"\u001f"}}{{.Label "com.docker.compose.project"}}{{"\u001f"}}{{.Status}}{{"\u001f"}}{{.Ports}}'(compose v2 sets the project label;{{"\u001f"}}escapes in the Go template), parsed withawk -F'\x1f'+IFS=$'\x1f' readeverywhere — tab/pipe delimiters are IFS whitespace or inside values, so\x1f(DEV.md:213 gotcha); dash padding viasednottr(tr corrupts multi-byte─). Deps guard (docker) before--help; no stdin → not inINTERACTIVE_CMDS;# POS_FLAGS: -a --all. Docs: POS.md docker row + detail, howto/docker.md table + section,bin/posusage EXAMPLES, AGENT_Context §14 row. Verified: stub-PATH suite/tmp/opencode/docker-stack-test/run-tests.sh23/23 (grouping, sorted stacks,-ashows exited, standalone, empty daemon rc=0, colored status, missing docker rc=1,--helpafter deps guard); live runs against the real daemon (affine/audiobookshelf/convertx/gitea stacks,affine_migration_job Exited (0)+lab1 Exited (137)under-a); dispatch viapos docker stack;make gen && make check,make lint0 FAIL / 0 WARN. -
2026-08-15 — Fix
pos media syncoffering a Ventoy stick's EFI partition as the sync target: with the data partition unmounted, the 32 MBVTOYEFIESP was the only mounted USB partition,usb_detectoffered it with no context, andcpdied mid-copy withNo space left on device(live-box report).usb_detectnow fetchesFSTYPE/PARTTYPENAMEand excludes EFI system partitions (VentoyVTOYEFI,/boot/efi) from both the mounted list and the mount-offer list;USB_MOUNTEDentries carrymp|label|size|model|fsandusb_pick_rootshows that in the single-stick confirm and the multi-stick/partition picker (1) /media/Ventoy (1.1T, Ventoy, exfat)), whileUSB_ROOTstays a bare mountpoint (${root%|*}) sopos system backup(${root%/}/backups) is unaffected.pos-media-syncgained a pre-flight space check (measures exactly whatneeds_copywould copy vsdf -Pk,err/warnbefore any copy) — no more mid-copy ENOSPC. Docs: howto/media.md target-picking note, SCRIPTS.md usb-lib paragraph, AGENT_Context hand-maintained lib row (194→205). Verified: new stub harness/tmp/opencode/vtoyefi-run.sh(ESP filtered from mounted + mount-offer, multi-pick shows only the data partition, space fit/too-small/dry-run-warn) green;/tmp/opencode/backup-teststill green; live checkprintf 'n\ns\n' | bash bin/pos-media-sync --mp3no longer offers VTOYEFI (offers unmountedsda1Ventoy instead);make gen && make check,make lint0 FAIL / 0 WARN. -
2026-08-15 — Fix
pos media syncreporting success with 0 files when the source is a symlink: it enumerated with plainfind "$SRC", and GNU find (default-P) does not descend a command-line symlink to a directory —~/Music -> /mnt/hdd/…/musictherefore yielded zero matches, the loop never ran, and the tool printed0 added, 0 updated, 0 unchangedwithout creating the target dir (live-box report). Switched tofind -H "$SRC"(follows only command-line symlinks; inner-symlink semantics unchanged). howto/media.md sync section notes symlinked sources are followed. Caught live, not by the 46-case stub suite (which used a real temp dir source — lesson: add a symlink-root fixture). Verified:printf 'y\n' | bash bin/pos-media-sync --mp3 --dry-runnow lists all 31 mp3s as "would copy";make gen && make checkgreen. -
2026-08-14 —
pos system backup— smart USB detection: lsblk TRAN (lsusb/by-id cross-check), mount offer for plugged-in-but-unmounted sticks, sha256-verified copy (stub-suite 54/54). -
2026-08-05 —
pos communication telegram—--parse-mode(plain/markdown/html). -
2026-08-05 — doc/code sync gate —
make gen+make check+ pre-commit hook. -
2026-08-05 —
pos usb server— USB Redirector control tool (494eae2). -
2026-08-05 —
pos <category> --helpauto-discovery in the dispatcher. -
2026-08-05 — AGENTS.md with lazy-loaded DOC references.
-
2026-08-06 — Fix entertainment timer
1hnot firing —interval_to_oncalendaremitted invalidOnCalendar=*-*-* */N:00:00(systemd rejects*/Nin the hour field); now*-*-* 00/N:00:00. Dropped the cron fallback entirely: scheduling is systemd user timers only (sync_cron/interval_to_cron/cron_blockremoved),statussimplified,Ndintervals rejected with a clear error. -
2026-08-06 — Nested
possubcommands —# POS_SUBCMDS:header annotation (telegram, docker-compose, docker-vbox) +make genemits a_pos_subcmdscompletion map; nested tools (telegram listener) auto-list under their parent instead of as a flat sibling (telegram-listener) inpos <category>and tab-completion; generic tool-level completion (subcommands + flags +--help). -
2026-08-06 — Telegram listener —
pos communication telegram listener: interactive/command→ bash map editor + owner-only polling daemon as a systemd user service (map in~/.config/linux_post_install/telegram_commands.env, re-read per message;/help, unknown-command reply, 60s timeout, stdout reply). -
2026-08-06 — NFS in
pos system—pos system nfs-server(status/share/ unshare/list/reload/enable/disable, idempotent /etc/exports edits, generic default with Tailscale/WireGuard/LAN examples) +pos system nfs-client(mount/unmount/list + persistent mounts as systemd.mountunits ordered after network-online.target, no fstab);nfs-kernel-server+nfs-commonadded to preinstall PACKAGES. -
2026-08-06 —
posHOW-TO guide set —DOC/HOWTO.mdindex + per-categoryDOC/howto/*.md(network, docker, media, system, ssh, usb, communication, entertainment) with flags, recipes, config, and troubleshooting; wired into DOC/README, root README, AGENTS.md. -
2026-08-06 — Multi-platform alerting —
lib/notify.shroutes viaNOTIFY_PLATFORM(notify.env, default telegram; sender contract for Matrix/Synapse later),system.envshared config for health/backup, dynamic effective values in--help, telegram--markdownalias. -
2026-08-06 — Tier 1 —
pos system health(dashboard +--send),lib/notify.sh(wired into backup + firewall), daily digest timer via postinstall. -
2026-08-06 — Document Map index + Entertainment section in AGENT_Context (
cf36780). -
2026-08-06 — Entertainment module — plugins (weather/joke/gold),
pos entertainment config/enable/disable/send/status, auto-trigger + Telegram send. -
2026-08-07 —
pos system health --sendnotification-only; listener@quietprefix (run mapped command without replying, for commands that self-notify)./status=@quiet pos system health --send= exactly one digest. -
2026-08-09 — Matrix/Synapse
communicationtools —pos communication matrix sender+listener, completing the second notify platformlib/notify.shwas designed for (NOTIFY_PLATFORM=telegram,matrixfan-out; the sender implements thesend <value> [--markdown]contract vianotify_sender_name()'s default key→tool mapping, no lib changes). Sender (bin/pos-communication-matrix-sender):send <value> [--markdown] [--room <id|alias>]PUTsm.room.message(m.text) to the client-server API v3 — room ids/aliases URL-encoded (#pos:example.org→%23pos%3A…), unique per-message txn id,--markdownsendsorg.matrix.custom.htmlvia a best-effort markdown→HTML converter (bold/italic/code/fences/strike/links/headers/lists, escapes HTML, never fails the send);login --user <@id>(masked password prompt →m.login.password→ savesaccess_token+user_id);test. Config scopematrix(~/.config/linux_post_install/matrix.env,MATRIX_HOMESERVER/MATRIX_ACCESS_TOKEN/MATRIX_USER_ID/MATRIX_ROOM_ID, secret masked) registered via# POS_CONFIG:→pos config matrix+ tab-completion scope. Listener (bin/pos-communication-matrix-listener): systemd user daemon (pos-matrix-listener.service) long-polling/sync(30s timeout, per-syncsincetoken, compact filter dropping presence/account_data/device noise,m.room.messageonly); reacts toMATRIX_USER_ID's own messages (resolved via/account/whoamiif unset),MATRIX_ROOM_IDrestricts to one room;/and!both resolve; replies threadedm.in_reply_to;@quietno-reply marker;/cmd::desc=…map descriptions;ai …bridge (pos ai gemini ask, per-room sessionmatrix-<room>,ai /resetclears, markdown stripped); interactive editor (--status/--enable/--disable/--run), 60s command timeout, exit-code prefix, ~3800-char truncation.communication-matrix-listeneradded toINTERACTIVE_CMDS(stdin editor + forever-loop daemon). Docs: POS.md rows + "in detail" sections + ai bridge note, howto/communication.md rewritten Matrix sections, HOWTO.md index + config table + platform note,bin/posusage EXAMPLES;make gen && make checkgreen. Verified against a mock homeserver: send plain/markdown/--room/test request shape (URL-encoding, Bearer auth, JSON body), login token save, listener owner-filter +/statusreply +/help+@quietsilence + non-zero exit reply + interactive editor add. — state-based threshold rule monitors (eventer). Each line of~/.config/linux_post_install/event.envis an independent rule:["<msg>" if ] <check-command> <op> <threshold>(op> < >= <= == !=, unit suffix ok60c/80%). The check command is run on every pass and its first numeric output compared float-safe; operator detected as the rightmostop thresholdpair so checks containing their own>/<(awk, redirection) parse fine. Alerts once on false→true plus one recovery message on true→false (no repeats while a condition holds); per-rule state in~/.local/share/linux_post_install/eventer/state/keyed by rule-line hash (editing a rule resets its state). Subcommands:run(timer entrypoint),config(interactive add/remove/edit with validation by test-running the check),list(rules + live values),enable [interval](systemd user timerpos-event-trigger.timer+ oneshot service;5m…weeklyorOnCalendar=…; graceful warnings when no user systemd manager,loginctl enable-lingerattempt),disable,status.--dry-runhonors the DEV.md dry-run convention. Alerts vialib/notify.sh(Telegram default; other platforms viaNOTIFY_PLATFORM). New:bin/pos-system-event-trigger,lib/eventer-lib.sh,config/event.envtemplate (installed no-clobber by postinstall),lib/eventer-lib.shinstalled by install.sh,system-event-triggeradded toINTERACTIVE_CMDS, usage EXAMPLES row. Docs: POS.md system row, HOWTO.md index row, howto/event-trigger.md;make gen && make checkgreen; functional tests covered trigger/recovery/no-repeat, float + unit parsing, editor add/remove/edit + validation + dry-run, timer enable/disable/status (graceful), dispatcher routing. -
2026-08-09 —
pos media mp3/mp4hardened + smart format selection. Both tools: yt-dlp calls go throughspawn(honor$DRY_RUN;--dry-runprints the exact command and skips dep checks),-o/--output,--no-playlist,--cookies(file existence check), clean ffmpeg/yt-dlp guards,# POS_FLAGS:for completion, full metadata (--embed-metadata --embed-chapters --embed-thumbnail --no-overwrites, mp3 also--convert-thumbnails jpg+--parse-metadata "%(artist,uploader)s:%(artist)s"so the uploader fills the artist tag). mp3 gains--by-artist(~/Music/<artist>/<title>.mp3). mp4 gains-f <id>/--best/--worst(no prompt), conflict validation, and an interactive picker that shows a curated-Ftable ([audio]/[video]/[combo]grouping, raw clutter dropped) on stderr — stdout carries only the chosen id (ui_pick lesson) — with id validation against the real table and empty/best default. Docs: howto/media.md rewritten (flags, metadata, by-artist, troubleshooting);make gen && make checkgreen. -
2026-08-09 — Telegram
ai …now answers about a message you reply to: the listener extractsreply_to_message.text(falls back tocaption) from each update and passes it tohandle_message; the AI bridge prefixes the prompt with[Reply context — the message you are replying to]. So replying to a/statusoutput and askingai check this detailsgives the model the actual output. Applies only to the AI bridge (mapped/commandsuntouched); reply context rides in the user turn so the session records what was analyzed. Docs: howto/ai.md bridge section. -
2026-08-09 —
pos ai geminisessions + Telegram-friendly replies.--session <name>givesask/chatpersistent memory (~/.local/share/linux_post_install/ai/<name>.json, capped at 40 turns, pruning keeps the first user turn as scene); newsessionssubcommand (list /reset <name>). Telegram listener now keeps one session per chat (telegram-<chat_id>) withai /resetto clear. New--system "<text>"flag injects a GeminisystemInstruction(viajqmerge) sent every turn but never stored in the session file; the listener passes a Telegram-voice prompt ("reply like a friendly Telegram chat, use emojis") and strips markdown (**,*, backticks,#, links, lists, blockquotes) from replies beforesendMessage, since messages go out as plain text. Docs: howto/ai.md (flags, sessions, bridge memory/formatting),make gen && make checkgreen. -
2026-08-09 — Fixed
pos configsecret-value corruption:cfg_read_secret's cursor-advanceechowent to stdout and, since the function is called via$(...), a leading\nended up inside every secret value → the env file gotAI_GEMINI_API_KEY="\n<key>", unreadable bycfg_value/load_config(menu showed(not set),pos ai geminidemanded a key). The newline now goes to the terminal (echo >&2). Defense in depth:cfg_write/write_config_keystrip CR and truncate multi-line pastes (warn),cfg_valueand the ai/telegramload_configs strip CR on read. Verified on a real PTY (piped tests couldn't reproduce — non-TTY stdin skips the echo path). -
2026-08-09 —
aicategory —pos ai gemini(ask/chat/models) via Google Gemini REST API.askprints only the answer (pipe/script/Telegram-friendly),chatis a multi-turn REPL (q/quit/Ctrl+C,/reset, empty input re-prompts),modelslists generateContent-capable ids and flags the default;--modeloverride; defaultgemini-2.5-flash. Config scopeai(AI_GEMINI_API_KEYsecret +AI_GEMINI_MODEL) in~/.config/linux_post_install/ai.env, edited viapos config ai;config/ai.envtemplate installed no-clobber by postinstall;ai-geminiadded toINTERACTIVE_CMDS. Telegram listener now answers non-command messages starting withaiviapos ai gemini ask(owner chat only; error replies carry thepos config aihint) — future intents (reminders) slot in as more case arms inhandle_message. Docs: POS.mdaisection + listener bridge, howto/ai.md, HOWTO/README index rows,bin/posusage example. -
2026-08-09 — Entertainment plugins
gold+weathernow emit emoji-visualized Telegram messages. Gold: headline is USD/gram (XAU/oz ÷ 31.1034768), ounce as reference, bid/ask, cleaned timestamp (+00:00/fractional seconds stripped). Weather: per-WMO-code emoji (☀️/🌙 day-night aware for clear sky), °C + feels-like, humidity, wind with unit spacing. Both verified live; emojis are safe in the default plain send mode. -
2026-08-09 —
pos tree: prints the liveposcommand tree (categories → commands → subcommands) by deriving the hierarchy frombin/pos-*filenames +# POS:/# POS_SUBCMDS:headers, so it always matches what the dispatcher can run. Category-less likepos-config;--depth Nlimit;pos help treeworks. Docs: POS.mdtreesection,bin/posusage example,make genregenerated the AGENT_Context tree/dispatch/filetable +_pos_flags[tree]. -
2026-08-09 — Telegram sender
config/config setremoved — redundant withpos config telegram(same# POS_CONFIG:registry, masked token display + input, chat-id validation, chmod 600); sender/listener error hints now point there. Deep-review bugfixes in the same commit: mapped/commandvalues containing|are no longer truncated (load_mapswitched from a|to a\x1fdelimiter — previously/up=echo hi | headsilently ranecho hi);pos entertainment send <plugin> [args…]actually forwards the extra args (every arg wasshifted in the flag loop, so$@was empty) and passes--before the message so leading--plugin output isn't parsed as an option;write_config_key(entertainment-lib) andcfg_write(config-ui) replaced unescapedsed -i "s|^K=.*|K=\"$v\"|"with grep-v+append so values with&/|/\no longer mangle (also the path all telegram config now flows through);sync_systemddaemon-reloads after removing timer units;digitsconfig validation accepts negative group/supergroup chat ids (-100…). -
2026-08-09 — Fixed telegram listener editor crash on remove/edit/test:
ui_pickprinted its menu listing to stdout, soidx="$(ui_pick)"captured the menu and the number, andMAP_CMDS[$idx](arithmetic array subscript) blew up with "syntax error in expression". Menu decoration now goes to stderr; only the picked index is emitted on stdout. Pre-existing bug (before the::descwork), exposed by the description column. -
2026-08-09 — Telegram listener pushes its mapped
/commandsto the bot's/menu viasetMyCommands(auto after every map edit, on--enable, and at daemon start; manual--sync-commandsflag). Map lines may carry a menu description:/cmd::short description=bash command(falls back to the bash command, ~40 chars). Names are validated against Telegram's lowercase[a-z0-9_]rule — invalid ones are skipped from the menu with a warning but still resolve when typed; empty map clears the menu. Fixed latent bugs found by the sync work:map_has(awkEND{exit 1}overrode the match), andwarn()went to stdout so it leaked into the generated JSON (now stderr). -
2026-08-09 —
pos config <TAB>scope completion is now cached at gen time (_pos_config_scopesarray emitted bymake genfrom the# POS_CONFIG:registry) instead of scanning ~40 tools per TAB — a per-keypress subshell storm that wedged interactive shells for minutes on the loaded homelab box. Two stuck-bashsessions (69%/38% CPU) killed.plugin_marker/plugin_keyshardened with|| truesoconfig_keysno longer aborts mid-scan underset -euo pipefailon mixed lib/plugin dirs (installed layout) — fixes missing plugin keys inpos config entertainment. -
2026-08-09 —
pos config <scope>interactive config editor: reads the# POS_CONFIG:registry across tools into a single runtime config (~/.config/linux_post_install/*.env, one file per scope, chmod 600); secret masking with show/hide toggle,digits:/num:/url:validation,-to clear, blank keeps;*pluginsmarker expands plugin vars (entertainment) fromentertainment-lib.sh;desc::examplevalue-format hints shown in the editor; gen-docs now handles category-less tools (pos-config), fixed aset -e+pipefailbug that truncated the header registry. -
2026-08-09 —
pos-communication-telegram→pos-communication-telegram-sender: one canonicalsend(dropped the legacy--sendflag, which duplicated thesendsubcommand in completion).pos communication telegram <TAB>now completes to justsender listener.lib/notify.shmaps platformtelegram→telegram-sendervianotify_sender_name(); entertainment-send + health--sendcheck updated. Removed phantom subcommands from howto/communication.md (webhook/logs/broadcast/file never existed). -
2026-08-09 — Structure/convention audit fix:
--dry-runnow truly dry (spawn()honorsDRY_RUN, install.sh exports it to child phases, postinstall mutations run-wrapped);gen-docs.shno longer chmods regenerated files to 0600;make checknow syntax-checks apps/entertainment/features/templates;.gitignoreprotectsconfig/authorized_keys+config/rclone.conf; honest--sendconfirmation; docs refreshed (notify.sh in lib lists, pos-health systemd units, tsui, scripts/, INTERACTIVE_CMDS). -
2026-08-11 — Docs: DEV.md / AGENTS.md / AGENT_Context improved from the SMB session's lessons. DEV.md: new "Testing tools that need root / systemd / missing deps" (env-override test seams —
FLAGS_DIR/SMB_CONF/SMB_CREDS_DIR/UNIT_DIRprecedents — + stub-PATH fakes + PTY prompt driving viascript); new Best Practice "Managed Config Blocks" (start/end marker idiom incl. theinblock == 1awk guard, validate-then-apply, hot reload); deps-guards-run-before---helpmade explicit (previously only inferable by reading the NFS tools); "Update the docs" checklist completed (howto index/section, Common Tasks row, AGENTS.md Quick facts, AGENT_TODO Done move). AGENTS.md: clarified which filetable line-count rows are hand-maintained (non-pos-*files above the marker) + when to bump them; deps-guard clause added to Quick facts. AGENT_Context "Adding a New Tool" steps 6–7 mirror the above.make gen && make checkgreen. -
2026-08-11 —
sharecategory grows SMB:pos share smb server(bin/pos-share-smb-server) +pos share smb client(bin/pos-share-smb-client), completing the share trio (usb/nfs/smb). Server:status/share/unshare/list/adduser/deluser/reload/enable/disable; idempotent marker blocks in/etc/samba/smb.conf(# >>> pos-managed share: <name>…# <<< end pos-managed share— hand edits outside markers survive;inblock==1-guarded awk so removing one block never eats another's end marker),testparmvalidation before apply +smbcontrol smbd reload-confighot reload;--read-only/--guest/--users u1,u2flags with unrestricted-share warnings;smbpasswduser management (prompts, requires system user first). Client:mount/unmount/list/persist/unpersist; password prompt via/dev/tty, throwaway chmod-600 credentials for one-shot mounts, persistent creds at/etc/samba/credentials/<name>(chmod 600);persistwrites a systemd.mountunit (systemd-escape) withx-systemd.automount+_netdev— mounts on first access, never blocks boot. Both sourcelib/notify.shfor mutations; added toINTERACTIVE_CMDS(prompting subcommands). Deps:samba+cifs-utilsadded to preinstall PACKAGES.SMB_CONF/SMB_CREDS_DIR/UNIT_DIRenv-overridable for tests (FLAGS_DIR precedent). Docs: POS.md share rows, howto/share.md SMB sections, HOWTO index row, AGENT_Context Common Tasks, AGENTS.md categories.make gen && make checkgreen; logic tested via stubbed PATH + temp config (marker idempotency, guest + user persist flows). -
2026-08-11 —
pos network checkportnmap overhaul: two-pass engine — pass 1 = fast-Pn -T4 --max-retries 1scan of only the asked ports (was: all 65535) with per-port state + nmap service names; pass 2 (--versions, opt-in) =-sV --version-lighton open ports only (generous host-timeout — version probing a silent service otherwise made nmap skip the host entirely), fallback fast banner probe for open TCP with no version info; TCP fast path ~2s for 3 ports. Unprivileged UDP now falls back to the nc engine (Debian nmap-sUrequires root and quit outright); IPv6 hosts get-6;no output/filtered states set rc=1;--timeoutscales nmap host-timeouts. New--versionsflag in# POS_FLAGS:(completions regenerated) + usage text; port-metadata fallback retained.make gen && make checkgreen. -
2026-08-11 —
pos communication matrix sender loginerror reporting: captures HTTP status + Matrixerrcode/errorfrom the JSON body (temp file, not stdout) instead of a generic "wrong credentials?" message — distinguishes unreachable homeserver from rejected credentials; auto-prepends@when--useris bare (e.g.--user alice:example.org→@alice:example.org). -
2026-08-11 — New
sharecategory —usbandnfsmoved out ofpos usb/pos systemintopos share:pos share usb server(waspos-usb-server),pos share nfs server+pos share nfs client(werepos-system-nfs-*). Renamed the three tools (bin/pos-share-*), updated# POS:headers/usage strings,INTERACTIVE_CMDS(usb-server→share-usb-server),bin/posusage() EXAMPLES, and the notify-scope comment inpos-system-backup. Docs: newDOC/howto/share.md(USB + NFS consolidated;howto/usb.mddeleted, NFS sections stripped fromhowto/system.md), POS.md### sharesection (replaces### usb, nfs rows moved out of### system), HOWTO/README indices, AGENT_Context hand-written spots, root README, DEV.mdINTERACTIVE_CMDSexample, AGENTS.md categories. Category is the home for futuresmb.make gen && make checkgreen;/usr/local/binrefreshed. -
2026-08-12 —
pos network download(bin/pos-network-download) — aria2 JSON-RPC daemon + queue control. Daemon: persistentaria2cas a systemd user service (pos-aria2.service,${XDG_CONFIG_HOME:-$HOME/.config}/systemd/user,enable --now+ linger warning on headless boxes),--rpc-listen-port=6800, generatedRPC_SECRETin~/.config/linux_post_install/download.env(chmod 600, env override), unit flags--continue=true --max-connection-per-server=16 --split=16 --seed-time=0 --dir=$HOME/Downloads. Commands (18):start/stop/status(+ bare overview = status+list),add <url…> [--dir --out --split --tmux],torrent <file|magnet…> [--dir --seed --tmux](base64addTorrent),metalink <file|url> [--tmux],list(active/waiting/stopped table),info/files/peers <gid>,pause|resume|remove [gid|all](--force→force*),purge,move <gid> <pos>,limit [gid] <speed>(--upload, 0=unlimited,2M/512K),set <k=v…> [--gid],watch [gid](2s live repoll; exits when that gid completes).--tmuxopens a detacheddl-<name>session runningwatch <gid>(name from--out/URL basename, sanitized, 40-char truncate,-2on collision; closes itself on completion). Deps:aria2c/jq/curlguards before--help;aria2added to preinstall PACKAGES. No stdin → not inINTERACTIVE_CMDS. JSON built withjq -nc --arg(never string interpolation — fixes JSON-quote bugs);# POS_SUBCMDS:(18) +# POS_FLAGS:→ completions. Test seamsRPC_PORT/RPC_SECRET/DOWNLOAD_DIR/USER_SYSTEMD_DIR/ACTIVE_MARKER; 76-case stub-PATH behavior suite green (unit content, secret 600, add→gid, tables, queue ops, error paths). Docs: POS.md network row+detail, howto/network.md section, HOWTO index, AGENT_Context Common Tasks row.make gen && make checkgreen. -
2026-08-12 —
pos network downloadgrows outage resilience:restart <gid>(re-queue from history — torrents via rebuilt magneturn:btih:+&tr=trackers, HTTP via original URIs withdir/outpreserved,--continue=trueresumes partials; options--dir/--seed/--split/--tmux),retry <gid|all>(smart retry — waits out internet outages viaNET_PROBEseam, re-queues,retry_verifypolls the new gid; aria2 error 3 = real problem → diagnosed + marked permanent in~/.config/linux_post_install/download.retryasurl:<uri>/bt:<infohash>,retry allskips them, manual restart overrides;--once/--quiettimer mode;--interval/--max-wait), and the retry healer systemd user pair (pos-aria2-retry.serviceoneshotretry all --once --quiet+pos-aria2-retry.timer2min,Persistent) that arms on download start (add/torrent/metalink/restart) and disables itself when nothing is left;watch <gid>now auto-restarts its download after an outage. Fixes from stub-suite review:ensure_healerwas missing from the three submit paths;RESTART_NAMEwas lost acrossdo_restart's process-substitution subshell (now adownload_name()helper);restartexited 1 because the[ tmux -eq 1 ] && tmux_watchtest was the function's last statement. Verification: stub-based test harness (/tmp/opencode/dl-test— curl/systemctl stubs with tellStatus fixtures,NET_PROBEfile-flip, unit enable/disable logging) 119/119 green, incl. new restart/retry/healer/watch-heal cases. Docs: POS.md download rows + outage-resilience paragraph, howto/network.md outage recipe, SYSTEMD.md per-user units section, AGENT_Context + completions regenerated.make gen && make checkgreen. -
2026-08-12 —
pos network download replace <gid> <url>+ fresh-link status advisory.statusnow flags stopped errored downloads whose source is marked permanently failing indownload.retry(needs fresh link: <name> (<gid>) — pos network download replace … <new-url>; onetellStoppedRPC, id-match in jq).replace <gid> <url>re-queues a dead single-file HTTP/FTP download with a new URL keeping the samedir+ file name (partial resumes via--continue=true), unmarks the old source (retry_unmark, literalgrep -vxF— URL-safe), and reusesretry_verifyso a dead replacement link is diagnosed + marked permanent; torrents/active/multi-file are rejected with hints;--dir/--split/--tmuxsupported.retry_verifyhardened to${quiet:-0}so it works outsidecmd_retry. Stub suite grew areplacesection (advisory match, success + unmark + advisory-clear, dead new link marked, torrent/active/arg errors, prefix gid) — tellStopped fixtures gaineduris(real aria2 includes them). 141/141 green; docs: POS.md row + outage paragraph, howto/network.md dead-link recipe.make gen && make checkgreen. -
2026-08-12 —
pos system event-trigger(eventer) generalized intopos system schedule— the scheduler replaces the single-timer threshold monitor with per-job systemd user timers (pos-schedule-<name>.{timer,service},Persistent, ExecStartrun <name>, reconciled onenable/disable— orphan units + the legacypos-event-triggertimer auto-removed). Each job is a chmod-600 file~/.config/linux_post_install/schedule.d/<name>.env:INTERVAL(5m..59m/1h..23h/hourly/daily/weekly/OnCalendar=…),NOTIFYpolicy, optionalMSG,RULE(threshold only), andCOMMAND= literal remainder of the line (pipes/quotes/sudoneed no escaping). Policies:always(full output every run),onchange(diff vs last run, first run always sends),onerror(non-zero exit or empty output),threshold(old event-trigger behavior: first numeric vsRULE, alert on false→true + recovery, per-job firing state),never(silent side-effect jobs — no notify; run log + last-run record still kept). Per-run logs/state in~/.local/share/linux_post_install/schedule/{logs,state}/. Subcommands:run [name|all],list,config(interactive add/edit/remove/enable/disable with validation),enable [name|all],disable [name|all],status,migrate(converts legacyevent.envrules →schedule.d/rule-N.envthreshold jobs, adopts the legacy timer's OnCalendar or 5m, removes the old timer). Files:bin/pos-system-event-trigger→bin/pos-system-schedule,lib/eventer-lib.sh→lib/scheduler-lib.sh(git mv; installed by install.sh),config/event.env+config/event-rules.template→config/schedule.d/starter jobs (nvme-health viasudo -n smartctlwith the user's exact grep — sudoers NOPASSWD documented; cpu-temp + disk-root thresholds; silent log-cleanup), postinstall installs them no-clobber into an emptyschedule.d/(legacyevent.envusers get a migrate hint instead).bin/posEXAMPLES + INTERACTIVE_CMDS (system-schedule config) updated. Supersedes the "Tier 2: watch plugins" backlog idea. Docs: POS.md system row rewritten, howto/event-trigger.md → howto/schedule.md (job syntax, policies, NVMe recipe, migration), HOWTO.md index row + config table + scheduling bullet, AGENT_Context lib row + Common Tasks row.make gen && make checkgreen; stub-harness suite (fakesystemctl/sudo/smartctl/sensors/df+ fake telegram sender logging, env seamsSCHEDULE_DIR/SCHEDULE_STATE_DIR/SCHEDULE_LOG_DIR/USER_SYSTEMD_DIR/SCHED_LEGACY_ENV) covers all 5 policies (threshold cross/recover/no-repeat, onchange first/diff/same, onerror, always, never-silent), COMMAND literal-pipe parsing, enable/disable/status + orphan/legacy cleanup, migrate (incl. skip-existing + dry-run), and dispatch. -
2026-08-13 —
pos communication scrcpyaudio control: scrcpy already forwards device audio to the desktop by default (answer: yes, default is sound-to-desktop). AddedSCRCPY_AUDIOconfig key (pos config scrcpy, defaulttrue):false/no/0→--no-audio,true/yes/1→ nothing (default), anything else → error. Docs: POS_CONFIG header, POS.md config table, howto/communication.md Mirror section, HOWTO.md env row. Verified: harness +7 tests (47/47 green — false/true/yes/0/invalid/combined-order),bash -n,make gen && make checkgreen. -
2026-08-13 —
pos communication scrcpy --new-displaysupport: newSCRCPY_NEW_DISPLAYconfig key (pos config scrcpy) —true/yes→ bare--new-display(default size/dpi),1920x1080,1920x1080/420or/240→--new-display=<value>; inline validation in_mirror(err runs in the main shell, not a process-substitution subshell) rejects anything else with the accepted forms. Docs: POS_CONFIG header, POS.md command+config tables, howto/communication.md Mirror section, HOWTO.md env row, usage() example. CLI pass-throughpos communication scrcpy --new-display=1920x1080also works verbatim. Verified: harness +8 tests (40/40 green — WxH, true, WxH/DPI, /DPI, invalid-rejected, env>config, combined order),bash -n,make gen && make checkgreen. -
2026-08-13 — Fix
pos communication scrcpymirror failure on the real box (ERROR: Unexpected additional argument:on every mirror, bare or with flags):_extra_flags()ranprintf '%s\n'with an empty array expansion, which prints one blank line;_mirror()'swhile readturned that into an empty-string arg passed to scrcpy. Fix:_extra_flagsnow returns early whenSCRCPY_EXTRA_FLAGSis empty (andprintf '%s\n' "${extra[@]}"when set), and_mirrordefensively skips blank entries ([ -n "$f" ] && cmd+=("$f")). Rebuilt the stub-PATH suite (/tmp/scrcpy-run-test.sh, outside the wiped$TEST_DIR) — 32/32 green incl. the regression (bare mirror → zero args to scrcpy) and EXTRA_FLAGS + passthrough mixed.bash -n,make gen && make checkgreen. -
2026-08-13 — Fix scrcpy apt install on the live box: preinstall
apt installfailed withUnable to locate package scrcpy(Debian/Ubuntu need contrib/universe forscrcpy, and the apt build is older anyway). Removedscrcpyfrompreinstall.shPACKAGES (keptadb);scrcpynow installs via the existing optional appapps/media/scrcpy.sh(GitHub latest, bundles adb) — docs (POS.md, howto/communication.md) and the tool's deps-guard error reworded to lead with that path. Re-verified:bash -n, stub suite 21/21,make gen && make checkgreen. -
2026-08-13 —
pos communication scrcpy(bin/pos-communication-scrcpy): wrapper over scrcpy+adb for Android mirroring/control. Subcommands: barescrcpy(mirror — config defaults + verbatim pass-through of any scrcpy flag; no device → friendly error + hints),devices(adb devices -l),record [file] [--headless](default$SCRCPY_RECORD_DIR/<device>_<date>.mp4,--headless=--no-playbackfor headless servers),tcpip [port](USB→wireless switch + printsconnectwith the auto-detected device IP),connect <ip[:port]>(adb connect + mirror-s),push(default/sdcard/Download= scrcpy's own default),pull,screenshot(adb exec-out screencap -p→ PNG in RECORD_DIR),info(model/android/sdk/serial via getprop). Config scopescrcpy(~/.config/linux_post_install/scrcpy.env,pos config scrcpy):SCRCPY_SERIAL/MAX_SIZE/MAX_FPS/BIT_RATE/FULLSCREEN/RECORD_DIR/PUSH_TARGET/EXTRA_FLAGS, env-var precedence. Depsscrcpy+adbadded to preinstall PACKAGES; docs note the apt build is older and point to the existingapps/media/scrcpy.shapp installer (GitHub latest, bundles adb) — researched 2026 releases (current v4.1). Conventions:# POS:/# POS_SUBCMDS:/# POS_CONFIG:headers, deps guards before-h|--help, no stdin → no INTERACTIVE_CMDS. Verified:bash -n, stub-PATH suite/tmp/opencode/scrcpy-run-test.sh21/21 green (fake adb/scrcpy echo-args, HOME isolation, env/file precedence, rc paths, screenshot bytes),make gen && make checkgreen, dispatch viapos communication scrcpy --help. Docs: POS.md communication table + detail block, howto/communication.md section, HOWTO.md index + env row, AGENT_Context Common Tasks row + gen'd tree/dispatch/filetable. -
2026-08-13 — Entertainment-module hardening (approved Tier 1 + Tier 2): delivery moved to
notify_send(platform followsNOTIFY_PLATFORM, default Telegram) vialib/notify.shsourced bybin/pos-entertainment-send; a last-run state is recorded per plugin (~/.local/share/linux_post_install/entertainment/last/<plugin>— rc + timestamp) on every non---printrun and shown bypos entertainment status, which also lists installed-but-not-enabled plugins; a send that fails while fired by a timer (gated on$INVOCATION_ID) additionally notifies the configured platforms. New message-safe plugin liblib/entertainment-plugin-lib.sh(defines onlyplugin_*, never writes stdout — the stdout contract stays "message only"):plugin_load_config(entertainment.env + env precedence),plugin_have,plugin_require,plugin_err,plugin_http_json <url> [--key <jq>] [-H <header>](curl--max-time 20 --retry 2);weather/joke/goldrefactored onto it.pos entertainment configgainsget|unset|ls|edit(edit via the sharedpos configUI — added toINTERACTIVE_CMDS). Tier 2: new shared liblib/user-timers-lib.sh(onlyut_*:ut_interval_to_oncalendar,ut_interval_label,ut_unit_name,ut_write_unit_pairincl.TimeoutStopSec=5s+Persistent+ network-online deps,ut_ensure_linger,USER_SYSTEMD_DIR) dedupes the systemd user-timer machinery betweenlib/entertainment-lib.shandlib/scheduler-lib.sh(the latter'ssched_*duplicates deleted; both source it; collides-with-nothing).install.shPhase 2 lib list += the two new libs; SCRIPTS.md/DEV.md/POS.md/howto/entertainment.md/AGENT_Context updated (hand-maintained lib rows: entertainment-lib 354→311, scheduler-lib 830→760, +112 user-timers-lib, +67 plugin lib). Verified:bash -neverywhere; smoke-tested in an isolatedHOME=/tmp/enttest(status, config get/set/unset/ls, send path rc=0, failing plugin records rc=1, error-case message hygiene);make gen && make checkgreen. -
2026-08-13 — Fast pos-unit shutdown: every systemd unit a pos tool writes (or
systemd/ships) now setsTimeoutStopSec=5s(+KillMode=control-groupon the daemons) so a stuck process can't stall a reboot for the 90s systemd default. Applied at all 7 template sites:pos-communication-telegram-listener,pos-communication-matrix-listener(also gained atrap 'kill $(jobs -p) 2>/dev/null; exit 0' TERM INTinrun_daemonso stop returns sub-second),pos-network-download(aria2 + retry-healer units),lib/scheduler-lib.shsched_write_units,lib/entertainment-lib.shwrite_units, andsystemd/{ssh-agent,autostart,usb-automount}.service. Legacy-unit cleanup: the repo no longer shipspos-health.{service,timer}/pos-entertainment.service(they were documented but postinstall never created them — found stale only on the live box, FAILED); removed their stale references from SYSTEMD.md (deleted thepos-health.servicesection + gating special-case, added a new Stop behavior section), POS.md, HOWTO.md, howto/system.md (now documents thepos system schedulejob replacement + removal commands), AGENT_Context (tree, phase description, selfcontained table). DEV.md Best Practices gains a Systemd units convention (TimeoutStopSec=5s + TERM trap + regeneration caveat). Verified:bash -non all edited scripts;make gen && make checkgreen (filetable rows for the two listeners + network-download auto-regenerated, hand-maintained lib rows bumped 350→354 / 822→830). Live-box application is manual (this session was a Google Cloud Shell, not the real machine): regenerate units viapos network download start,pos communication telegram listener --enable,pos system schedule enable <job>,pos entertainment enable <plugin>, thensudo systemctl disable --now pos-health.timer pos-health.service 2>/dev/null; sudo rm -f /etc/systemd/system/pos-health.{service,timer} && sudo systemctl daemon-reload. -
2026-08-13 — Bootstrap output transparency (
install.sh/preinstall.sh/postinstall.sh): removed the redundantapt update(preinstall.sh owns it — install.sh previously ran it twice, showing two identicalOK apt updatelines); Phase 2 now names what it installs — libs line (libs -> /usr/local/bin (644): common.sh flags.sh …), plugin names in the count line, x64_bin names, per-featurefeature installed/overwritten+feature flag setlogs with aN features installed: …summary — and the misleading"47 scripts + libs"label is fixed to47 scripts + 6 libs(the 6 libs were outside the counter); preinstall printsInstalling N packages (apt install -y):with the 40-name list wrapped at 80 cols; postinstall now logs silent skips —config/authorized_keys is empty — nothing to add(empty file previously looped zero times with no message),schedule.d already exists, keeping it(restructured the condition so the message is accurate when the dest exists vs config/schedule.d absent), and a per-serviceservice enabled: <name>line. No output-layer changes (no--verbose, no log file — decided scope). Verified:bash -n+--dry-runsmokes of phases 1/2/3 showing every new line (learned:install.sh:19hardcodesexport DRY_RUN=0, so an envDRY_RUN=1is ignored — the flag--dry-runis required), hand-maintained filetable count rows bumped (install.sh 206→223, preinstall.sh 73→75, postinstall.sh 163→168),make gen && make checkgreen. usb-automount left live (user choice). -
2026-08-13 —
usb-automountfeature, integrated exactly likeautostart:features/usb-automount.sh(root-guard re-exec via sudo; first-root-run self-install of udev rule/etc/udev/rules.d/99-usb-automount.rules—ACTION=="add", KERNEL=="sd[a-z]*", SUBSYSTEM=="block", ENV{ID_BUS}=="usb", TAG+="systemd", SYSTEMD_WANTS="usb-automount.service"— +udevadm control --reload+trigger --subsystem-match=block; an existing/edited rule is never overwritten; scanslsblk -Jfor unmounted removable partitions/raw whole-disk filesystems, mounts each at/media/<label>— vfat/exfat/ntfs world-writable via-o umask=000, fallback plain mount, label-collision bump-2/-3, no label →usb-<name>, logs${HOME:-/root}/.usb-automount.log) +systemd/usb-automount.service(Type=oneshot,WantedBy=multi-user.target— boot + hotplug + manualsystemctl start usb-automount), gated in postinstall.sh's systemd loop exactly like autostart (flag_is_set usb-automount→ skip with hint). Purpose: a plugged-in stick is auto-mounted world-writable, ready forpos system backup's post-verify USB copy. Docs: SYSTEMD.md (service section + gating code block), SCRIPTS.md (feature section + systemd bullet + TOC), AGENT_Context tree + filetable rows (postinstall.sh count corrected 152→163 — it was already 6 lines stale), README index rows. Verified with a stub suite (/tmp/opencode/usb-automount-test— lsblk JSON fixtures, mount/mountpoint/udevadm/sudo stubs,MOUNT_BASE/UDEV_RULES_DIRseams, HOME isolation): 47/47 green.make gen && make checkgreen. Gotcha learned:${VAR:-{...}}with a{inside the parameter-expansion default mis-parses in bash (emits a stray}— printf of a multi-line value showed}}); avoid braces in:-defaults. -
2026-08-13 —
pos system backupcopies the finished backup to a USB stick. Detection runs after the archive verifies (so a stick plugged in while the backup ran is found; if none is mounted, one re-scan prompt before giving up —sskips, EOF from cron skips silently, rc stays 0). Single stick → y/N confirm; several → numbered pick (0 = skip). Copy lands in<usb>/backups/(mkdir -p;chmod 600best-effort — vfat chmod failures warn, never fail), and the transfer is proven 100% by sha256 source-vs-copy before any success is announced: mismatch → warn with both hashes +notify_send "USB copy FAILED…"+ rc=1 (the ERR trap is re-armed mid-script so a USB-phase failure no longer notifies "Backup FAILED"). Detection:lsblk -J→ recursive jq filter (rm==true && mounted && type part|disk, space-safe via JSON) or pinnedBACKUP_USB_ROOTseam (=<root>/backups/, skips detection — also the test seam). Docs: usage() Environment, POS.md backup row, howto/system.md (USB section + env table + mismatch troubleshooting), DEV.md system.env list. Verified with a stub suite (/tmp/opencode/backup-test— sudo/gpg/lsblk/sender stubs, HOME isolation, per-test lsblk JSON fixtures, corrupting-cp + vfat-chmod override stubs): 40/40 green (skip s/EOF, seam y/n, detect single, multi pick 2/0, re-scan after replug, corrupt copy rc=1 + honest notify, vfat tolerance).make gen && make checkgreen. -
2026-08-13 —
pos share smb-server sharenow guards the two commonNT_STATUS_ACCESS_DENIEDcauses at share time (warnings only):--usersentries missing from the Samba passdb (pdbedit -L, cut to user column,grep -qxFper user — pointer topos share smb-server adduser <user>), and ancestors of the share path lackingother:+xtraversal (sticky dirs like/tmpcount as traversable via thetslot; fix hintchmod o+x <dir>). Both wired into thesharecase afterrequire_root_dir; howto/share.md SMB section + troubleshooting updated. Rooted inreports/bug-report-smb-server-access-denied.md(committed as the spec). Verified with a stub-PATH suite (/tmp/opencode/smb-test— pdbedit/systemctl/smbcontrol/testparm/smbpasswd stubs,SMB_CONFseam): 16/16 green. -
2026-08-13 — Docs hardening from the schedule-session review (sole-developer call: terse, session-learned). DEV.md §7 env-seam registry now lists
USER_SYSTEMD_DIR(bin/pos-network-download,bin/pos-communication-{telegram,matrix}-listener,lib/scheduler-lib.sh) + the scheduler'sSCHEDULE_*seams, and documents the missing-:--guard gotcha (aVAR="${XDG…:-…}"without leadingVAR:-overrides the seam — stub runs then silently write to the real$HOME; fix:USER_SYSTEMD_DIR="${USER_SYSTEMD_DIR:-…}"). New-tool test checklist gains an env-seam review step (grep for unguarded config writes + prove withVAR=/tmp/x). §7 notes stub harnesses are throwaway by design — build in/tmp/opencode/<tool>-test/, leave there, keep only the pattern. howto/schedule.md documents thatmigratecopies the rule LHS verbatim asCOMMAND(old tool never haddisk root/loadavgshorthands — rewrite those jobs with real commands).make checkgreen. -
2026-08-14 — Gitea Actions gate is now live and green end-to-end: act_runner (v0.6.1, labels
ubuntu-latest) registered on100.100.1.2(~/srv/gitea/runner/, standalone compose next to the ScaleTail gitea;CONFIG_FILE=/config.yamlenv required orrun.shnever reads the config;--add-host gitea.skink-platy.ts.net:100.111.241.54so the job container reaches gitea). First real runs caught a deterministic gen-drift: plainsortinscripts/gen-docs.shis locale-dependent (category-less tool keys start with|, which collates after letters under the CI container's locale →pos-config/pos-treereordered), so thegit diff --exit-codestep failed. Fixed withexport LC_ALL=Cin gen-docs.sh (byte-order sort) + regeneratedDOC/AGENT_Context_Project.md(config/tree now sort after the letter categories);make checkOK,make lint0 FAIL / 0 WARN. Live CI verdicts: the run fore0b5b11(workflow commit) and the empty trigger98a767cboth FAILED on the drift; the run for9d058b7(the fix) SUCCEEDED (🏁 Job succeeded). -
2026-08-14 — Gitea Actions gate added:
.gitea/workflows/lint.ymlrunsmake gen+git diff --exit-code(gen-drift) +make check+make linton every push/PR. Verified locally the exact four steps pass (gen idempotent, check OK, lint 0 FAIL / 0 WARN). "no CI" lines updated in AGENTS.md (Quick facts → CI bullet, notes a registered act_runner is required) and DEV.md (stub harnesses note: CI runs static gates only, not behaviour suites). Gitea 1.26.4 confirmed reachable; runner registration completed the same day (see the entry above). -
2026-08-14 — Convention-drift maintenance fix session (completed the audit backlog
MAINTENANCE.md, M-001..M-023, all VERIFIED; gatescripts/lint-conventions.sh+make lintnow 0 FAIL / 0 WARN;make gen && make checkgreen). P0 bugs: M-002/003/004 addeddocker-compose docker-vbox network-hotspottoINTERACTIVE_CMDS(stdin/log-pipe prompt swallow); M-005install.sh --stepsnow expands documentedN-Mranges vianormalize_steps_spec()(dry-run verified); M-006 feature-vs-docs decision:--send/--markdownnot restored (health is a console-only reporter by design since fe7708f; schedulerNOTIFY=alwayscovers delivery) — 5 docs corrected instead; M-007lib/notify.sh:57fallback routed to stderr (stdout-leak on standalone source). P1: M-008..M-014 deps guards moved before-h|--helpin docker-health/docker-ps (converted tocommand -v X || err), network-scan, share-usb-server, media-mp3/mp4 (guards before help with a--dry-runpre-scan preserving the documented no-deps preview); system-health documented as the sanctioned graceful-degradation no-guard pattern in DEV.md — lint refined accordingly (first_guard_lineonly matches real guards;first_lineskips comments; precision fixes, not weakenings); M-015 system-firewall gainedusage()+-h|--help(root-gated first; verified via sudo); M-016ffmpegadded to preinstall PACKAGES. P2: M-017/M-018 autostart + usb-automount gained the feature-template preamble (flags.sh load, usage); M-019chmod +x apps/media/scrcpy.sh; M-020SCALE_DIR/CONFIG_ENV:-seams in pos-docker-compose (verified via overrides; follow-on fix:DIMcolor var missing from common.sh crashedpos docker compose config— added it); M-021CONFIG_DIRcentralized as the canonical XDG-aware seam in common.sh, per-file duplicates dropped (standalone-sourced notify.sh/config-ui.sh/matrix+telegram tools keep an identical guarded copy — "no shared lib? inline fallbacks"); M-022plugin_*prefix collision resolved by renaming the internal registry helpers toent_plugin_*(the documented plugin-authoring APIplugin_have/plugin_require/plugin_load_config/plugin_http_jsonkept for user plugins); M-023 six tools (pos-config, pos-tree, pos-entertainment-{config,enable,disable,status}) now filename-referenced in DOC/POS.md. Hand-maintained AGENT_Context line-count rows bumped (install.sh 223→248, preinstall 75→76, common.sh 144→151, notify.sh 76→87 stale-corrected, autostart 14→50, usb-automount 134→138);make linttarget wired in the Makefile.MAINTENANCE.mdkept as the working record (uncommitted by design). -
2026-08-14 —
pos system backupoptional encryption (--no-encryptflag +BACKUP_ENCRYPT=0env, flag-or-env — user chose "Flag + env only"): plain path keeps a verified.tar.gzwith no password prompt (headless/cron safe); encrypt path unchanged (prompt → gpg AES-256 → decrypt-verify; the gpg dep-guard moved into the encrypt branch so plain backups no longer requiregnupg). Arg parsing rewritten as a loop over"$@"sopos system backup <folder> --no-encryptworks with the flag after the folder; usage() documents all three forms + the plain artifact name;# POS_FLAGS: --service --no-encrypt;config/system.envtemplate gains#BACKUP_ENCRYPT=0; POS.md row + howto/system.md section updated. Verified: stub suite +2 cases (T18 flag / T19 env: plain .tar.gz artifact, gpg never called via$GPG_CALLED, USB copy + sha256 of the plain archive, notify wording) — 65/65 green;bash -n,make gen && make check,make lint0 FAIL / 0 WARN. -
2026-08-14 — CI green-check via plain git (no SSH to the runner, no API tokens — user chose "CI tags + git ls-remote" + "scripts/ci-status.sh helper"):
.gitea/workflows/lint.ymlscoped toon: push: branches: [main](tag pushes no longer re-trigger it) and the gate step now reports its own outcome as a lightweight tag —ci-ok/$GITHUB_SHAon success /ci-fail/$GITHUB_SHAon failure, pushed over HTTP with the jobs automaticGITEA_TOKENtohttp://oauth2:${GITEA_TOKEN}@gitea.skink-platy.ts.net:3000/admin/Linux_post_install.git(runner container already host-maps that hostname to 100.111.241.54);steps.gates.conclusiondecides ok/fail,if: always()(guarded topushevents) covers failed gate runs, and an existing-tag guard makes re-runs idempotent. New executablescripts/ci-status.sh [--wait] [<sha>]reads the tags viagit ls-remote(origin,CI_STATUS_REMOTEoverride): GREEN (0) / RED (1) / PENDING (2);--waitpolls every 10s up to 10 min. DEV.md §CI gains a "Checking green without SSH" bullet. Verified:bash -n, yaml-parse OK,make gen && make check,make lint0 FAIL / 0 WARN; first live-tag verification pending the push (fallback if Gitea clamps token-push: PAT as workflow secret). -
2026-08-14 —
pos media sync(bin/pos-media-sync) — incremental Music → USB sync, plus the shared USB layer it builds on. New liblib/usb-lib.sh(194 lines, installed by install.sh):usb_detect(lsblk JSON, TRAN + lsusb/by-id cross-check →USB_MOUNTED/USB_UNMOUNTED),usb_related_present,usb_mount_offer(/media/<label>mount-offer,usb-automountscheme),usb_pick_root <prefix> <subfolder> <giveup-msg>(detect → mount-offer → single/multi picker →USB_ROOT); seamsUSB_MOUNT_BASE/USB_BYIDwithBACKUP_MOUNT_BASE/BACKUP_USB_BYIDaliases so existingsystem.envlines keep working; TRAN-fallback warning deduped to once per scan.pos-system-backuprefactored onto it (216 lines, was 364) — re-ran the backup stub suite: 65/65 green. Sync tool: add/update only, never deletes (user choice);--mp3/--mp4filter (neither = both),--source <dir>(defaultMEDIA_SYNC_SOURCE/$HOME/Music),--dry-runpreview with counts; copies missing/changed (size/mtime) files into<usb>/Music/(MEDIA_SYNC_DEST) preserving the tree viacp --preserve=timestamps; result notified vialib/notify.sh;media-syncadded toINTERACTIVE_CMDS; deps guards (lsblk/jq) before-h|--help. Docs: POS.md media row, howto/media.md section, SCRIPTS.md lib section + Phase-2 lib list, system.env seams, DEV.md env-seam registry, AGENT_Context Common Tasks + hand-maintained lib row (+usb-lib 194) + gen'd tree/dispatch/filetable/flags. Verified: new stub suite/tmp/opencode/msync-run.sh46/46 green (fresh/no-op/update/filter/dry-run/multi-stick/mount-offer/no-USB skip/never-delete/--source/TRAN-fallback/notify) — caught and fixed an invertedneeds_copyreturn;make gen && make check,make lint0 FAIL / 0 WARN; dispatch viapos media sync --help+pos medialisting. -
2026-08-22 —
pos media ytsync(bin/pos-media-ytsync) — incremental YouTube channel/playlist sync into~/Videos, implemented per the Architect decisions D1–D9 + Designer UX contract (reportAgents/2026-08-22-*.md). Subcommandsadd [url] / sync [name] / list / remove <name>+--dry-run; bare invocation = interactive menu (/dev/ttyreads, 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] titleheartbeat lines, LF-only logs (spinner TTY-gated); probe =yt-dlp --flat-playlist -Jparsed with jq, new-list diffed against the per-source--download-archiveBEFORE 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;removekeeps 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-inlib/notify.sh. Config scopeytsync:YTSYNC_VIDEOS_DIR/YTSYNC_EXTRA_ARGS(pos config ytsync); automation documented as apos system schedulejob (COMMAND=pos media ytsync sync,NOTIFY=never). Deps guards before-h|--helpwith 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/posEXAMPLES line. Verified:make gen && make check && make lint0 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_urlnow treatsyoutu.be/<id>short links (with or without&list=, incl. scheme-less +?si=forms) as single videos — same path as?v=— so they get typevideo+--no-playlistinstead 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 lint0 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|| trueso 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); :1053printf '----…\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.mddelivered inline). Gates re-run by Orchestrator post-review:make genidempotent, 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 fivepos share *tools stay untouched; install.sh Phase-2 explicit lib list += menu-lib.sh. Opt-in no-args+tty front doors (ormenuverb,# POS_SUBCMDS:registered, completions regen'd) onpos 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-invokedrun <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 genidempotent ·make checkOK ·make lint0 FAIL / 0 WARN). Open for later phases: P2 (docker-vbox, network-download), firewall style-migration decision, usb-servermenuin POS_FLAGS nit (owning track). -
2026-08-23 — Menu Phase 2 + firewall style-migration (decision "a" activated: P1 landed,
lib/menu-lib.shexists).pos docker vbox(157→261): 6-item menu hub over the inline case verbs via a quoted self-invocationmenu_self(verbs never re-enter the menu → no recursion);enterhands 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 throughuses_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/ttyvia tool-localtty_read()(EOF/no-tty → pointer + rc1, never hangs),prompt_ipverde-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 checkOK ·make lint0 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 unboundyn(pre-existing common.sh); vbox create EOF at dir prompt degrades to default while name/image prompts abort (cosmetic). -
2026-08-26 —
pos ai aliasactivation rework (Option B) +pos configlisting 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 — everypos ai aliasinvocation 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 abash -npre-commit guard; marker-guarded deletion of owned wrappers missing from ENV; legacyai-aliases.shgeneration stopped and generator-marker-guarded auto-removal with anunalias <names>remediation hint; loud PATH guidance when~/.local/binis 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;showgains the wrapper path;pos-system-uninstallsweeps the wrappers by their line-2 marker in discovery+removal. Dup-table menu bug fixed with a single_alias_tablerenderer (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] Captiongroup captions (condition evaluated per render viacfg_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 60–120 cols, whole render block → stderr per menu-lib house pattern, honest promptNumber to edit [r=refresh, q=quit]:); masking/edit flow byte-compatible, no per-scope branches.bin/pos-ailine-6 header adopted to the caption/tag syntax (single-line change). Verified: stub-PATH harness (HOME=/tmp/…,CONFIG_DIRseam, argv-capturingposshim) 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 foraiAND old-formatsystem; gatesmake gen && make check && make lint0 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-orientedread -rp; a multi-line Ctrl+V paste floods the tty queue,readconsumes only line one and the rest execute as commands later or get eaten by the next prompt — user-verified$(whoami)/; ls/sudo apt updatebehavior). Newmenu_read_value()inlib/menu-lib.sh(169→362): raw-mode (stty -icanon -echo -isig min 1 time 0) bracketed-paste-aware value reader —\e[?2004h/lmarkers, 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 viadd bs=4096|od -tx1|tr— NOT bash'sreadbuiltin, which self-interrupts on an ETX byte from a tty even with ISIG disabled (SIGINTs the whole script on Ctrl-C); confirmedread -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 — bashcasepatterns don't match a single backslash),_alias_prompt_truncatenewline-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 lint0 FAIL / 0 WARN. -
2026-08-27 — Configurable AI-bridge trigger word for the Telegram listener: the hard-coded
aiprefix inpos-communication-telegram-listenerbecameTELEGRAM_AI_PREFIX(defaultai) — messages starting with<prefix>(case-insensitive, literal match) are forwarded to Gemini. Newprefixverb:pos communication telegram listener prefixshows the current word,prefix <word>sets it (validated[A-Za-z0-9][A-Za-z0-9_-]*, writesTELEGRAM_AI_PREFIXtotelegram.envchmod 600); also editable viapos 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 scopedshopt -s nocasematch+ quoted-literal=~prefix (bashcasepatterns can't do literal-then-whitespace + case-insensitivity in one test);ai_bridge_prefix()precedence: telegram.env > env from load_config > defaultai.--statusshows the current prefix; usage +# POS_SUBCMDS: prefixadded (completions regenerate). Preserved edge: bareai(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 apos config aihint — code repliesAI error: …only). Verified: function-level routing harness (/tmp/ai_prefix_routing_test.sh— extraction of the real listener functions + PATH stubpos): defaultai/AIroutes, bare-prefix and unknown-command fallthrough,ai /resetand custom-bot/resetreset the session, custombot/BOTroutes and oldaino longer routes, per-message hot-reload after removing the var; CLI verb tests (show/set/invalid rc 1/leading-digit/--status); dispatch smokepos communication telegram listener prefix+ flat form;pos config telegramrender shows the field;bash -n×2,make gen && make check,make lint0 FAIL / 0 WARN. -
2026-08-29 — Generic text-prefix map for the Telegram listener (user's clarification superseding the scalar
TELEGRAM_AI_PREFIXsetter):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=opencodeturns "opencode check cpu" intoopencode "check cpu". New map filetelegram_prefixes.env(chmod 600, re-read per message,@quietvalues, 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 Geminiaibridge →/commandmap → Unknown (a mappedaishadows the bridge).prefixverb reworked: bare = list map + bridge word;prefix <word> <command...>= map (validated[A-Za-z0-9][A-Za-z0-9_-]*,bash -nvia check_syntax);prefix <word>= show one;prefix -r <word>= remove; the AI-bridge word itself is now set ONLY viapos config telegram(TELEGRAM_AI_PREFIX, defaultai—--status+ bareprefixstill 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.sh27/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 latentset -ecmdsubst abort on syntax errors); dispatch smoke nested + flat +--status;pos config telegramrender;bash -n,make gen && make check,make lint0 FAIL / 0 WARN.