_pos_complete_categories offered only hyphenated categories plus a
hardcoded 'config', so 'pos <TAB>' never showed bank/tree. Category-less
tools are now discovered from the executable scan, and the words==3
dispatch routes no-hyphen tools to _pos_complete_tool so 'pos bank <TAB>'
offers list/add/show/run/edit/remove.
User report after the llamacpp app install: 'installed llama.cpp unknown',
valid flags rejected (randomly per run), 'Model not found' for the HF
downloader's own layout, and a systemd user-bus failure over SSH. Detective
(real b10822 binary, FACT) found four independent causes:
- version: llama-server --version prints to STDERR; detect_llama_version's
2>/dev/null swallowed it -> always 'unknown'. Now captures 2>&1 + accepts
semver/build tokens (incl. build 1.2.3 edge)
- validation: printf|grep -q under pipefail -> SIGPIPE rc=141 race randomly
rejected flags present in the 59 KB --help. Now pipe-less grep (no race);
20x determinism regression test
- model resolution: resolve_model accepted files only, but the HF downloader
creates <models>/<repo>/file.gguf dirs. Now expands a dir with exactly one
*.gguf (never silently picks; multi-gguf lists + errs)
- port: llama.cpp default 8080 vs tool/adapter 8088; validation reliability
means --port is now always pinned in the unit
- user bus: headless/SSH sessions lack XDG_RUNTIME_DIR -> ensure_user_bus in
lib/common.sh pre-flights all three systemctl --user tools with remediation
text; pos ai server --no-unit direct-run escape hatch (pidfile) for boxes
with no bus
- find_llamacpp narrowed to llama-server/llama-server-cuda (bare 'server'
fallback hazard); installer post-install sanity (version+help execute,
symlink targets resolve)
Architect decisions DQ1-DQ6 recorded. Tester: 4 new regression files
(version-from-stderr, 25x flag-validation determinism, model dir expansion,
bus pre-flight + E2E) + 3 fixture updates; suite 16 files / 269 checks.
Verified: make gen idempotent; make check OK; make lint 0 FAIL, 0 WARN;
make test 269/269 (~49s); bash -n clean; git diff --check clean.
Service manager (start/stop/status/models/logs) with systemd user
service generation, GPU auto-detection, model selection from pos ai hf
downloads. Provider adapter integrates with pos ai ask as --provider
llamacpp. Config extends existing ai scope with LLAMACPP_* keys.
87 test cases / 0 failed. make gen/check/lint 0 FAIL / 0 WARN.
Bash-native tool using curl/jq to download AI models from HF Hub.
Subcommands: download (single file/repo/gguf filter), search, list, remove.
Auth via HF_TOKEN in ai.env, resume support, disk space pre-flight,
rate limit handling, .hf-meta metadata tracking.
46 test cases / 0 failed. make gen/check/lint 0 FAIL / 0 WARN.
New tool that classifies URLs by domain and delegates to pos media mp3
(audio) or pos media mp4 (video). Listener gains URL detection step
between prefix map and AI bridge — bare URLs auto-download.
Domain rules: music.youtube.com/soundcloud/bandcamp → mp3,
youtube/vimeo/twitch → mp4 --best, unknown → configurable default.
28 test cases / 70 assertions / 0 failed.
make gen && make check green, make lint 0 FAIL / 0 WARN.
Add an optional5th 'trusted' field to aliases
(name|provider|session|prompt|trusted). Trusted aliases pass --trust to
pos ai, which makes _prompt_run_command auto-execute the agent's detected
commands without the Y/n confirmation (command still printed for audit).
- bin/pos-ai: new --trust global flag; _prompt_run_command takes trusted
arg and skips the prompt when set; POS_FLAGS + usage updated
- bin/pos-ai-alias: _ALIAS_TRUSTED array, 5-field env format (backward
compat: missing field defaults to untrusted), Trust column in table,
trust row in show, trust step (5/5) in create wizard with security
warning, trust toggle (4/4) with diff tag in edit wizard, wrapper
scripts get --trust when alias is trusted
- completions/pos.bash + gen docs updated
Gates: make gen && make check && make lint = 0 FAIL, 0 WARN
New bin/pos-docker-stack: docker ps output grouped by Docker Compose
project (stack). Each stack is a sorted section (name, status, ports);
containers without a compose project land in a Standalone group at the
end. Running only by default, -a|--all includes stopped/exited. Status
colored on a terminal; summary line 'Stacks: N containers: N
standalone: N'; exit 0 when empty.
Data via docker ps --format with \x1f delimiters (project label
com.docker.compose.project from compose v2); parsed with awk -F'\x1f'
+ IFS=$'\x1f' read — tab/pipe delimiters are IFS whitespace or appear
in values (DEV.md:213). Dash padding via sed, not tr (multi-byte).
Deps guard (docker) before --help; no stdin.
Docs: POS.md docker row + detail, howto/docker.md table + section,
bin/pos usage EXAMPLES, AGENT_Context Common Tasks row. Verified:
stub suite 23/23, live daemon runs, dispatch, make gen && make check,
make lint 0 FAIL / 0 WARN.
The single-timer threshold monitor generalizes into a scheduler: each job is a
chmod-600 file in schedule.d/<name>.env (INTERVAL 5m..59m/1h..23h/hourly/daily/
weekly/OnCalendar=..., NOTIFY policy, optional MSG, RULE for threshold,
COMMAND = literal rest of line) with its own systemd user timer pair
(pos-schedule-<name>.timer + oneshot .service, Persistent, reconciled on
enable/disable — orphan units + the legacy pos-event-trigger timer
auto-removed). Policies: always (full output every run), onchange (diff vs
last run, first run sends), onerror (non-zero exit or empty output),
threshold (old event-trigger behavior: first numeric vs RULE, alert on
false→true + recovery, per-job firing state), never (silent side-effect jobs).
run [name|all], list, config (interactive add/edit/remove/enable/disable with
validation), enable/disable [name|all], status, migrate (converts legacy
event.env rules → rule-N.env threshold jobs, verbatim LHS as COMMAND, adopts
the legacy timer's OnCalendar or 5m, removes the old timer). Per-run logs +
state in ~/.local/share/linux_post_install/schedule/{logs,state}/.
config/event.env + event-rules.template → config/schedule.d/ starter jobs
(nvme-health, cpu-temp, disk-root, silent log-cleanup); postinstall installs
them no-clobber into an empty schedule.d/ (legacy event.env users get a
migrate hint instead). bin/pos EXAMPLES + INTERACTIVE_CMDS
(system-schedule config) updated; install.sh ships the renamed tool+lib.
restart <gid>: re-queue from history — torrents via rebuilt magnet
(urn:btih: + &tr= trackers), HTTP via original URIs keeping dir/out;
--continue=true resumes partials, complete files verify instantly.
retry <gid|all>: waits out internet outages (NET_PROBE seam,
--interval/--max-wait), re-queues and re-verifies; aria2 error 3 = real
problem → diagnosed + marked permanent (url:/bt: ids in download.retry,
skipped by retry all, manual restart overrides); --once/--quiet for the
healer timer.
Healer: pos-aria2-retry.{service,timer} user units — arms on download
start (add/torrent/metalink/restart), disables when nothing left.
watch <gid> auto-restarts after an outage.
Fixes from stub-suite review: ensure_healer missing from submit paths;
RESTART_NAME lost across do_restart subshell (download_name helper);
restart exited 1 (tmux test as last statement).
Stub harness (/tmp/opencode/dl-test) 119/119 green; make gen && make check green.
Docs: POS.md rows, howto/network.md outage recipe, SYSTEMD.md user units.
Persistent aria2c as a systemd user service (pos-aria2.service, enable --now,
linger warning) on localhost:6800 with a generated RPC secret in
~/.config/linux_post_install/download.env (chmod 600, env override). 18
commands: start/stop/status, add/torrent/metalink (auto-start, --tmux live
view), list/info/files/peers, pause/resume/remove/purge/move, limit/set,
watch (2s live repoll). JSON built via jq -nc --arg, never string
interpolation. Deps: aria2 in preinstall PACKAGES, aria2c/jq/curl guards
before --help. Docs: POS.md + howto/network.md + indices + Common Tasks row.
pos communication matrix sender: send (plain/--markdown/--room), login
(password->access token via m.login.password), test. Implements the
lib/notify.sh sender contract, so NOTIFY_PLATFORM=telegram,matrix now
fans out for real; matrix.env config scope registered for pos config.
pos communication matrix listener: systemd user daemon long-polling
/sync (since token, compact m.room.message filter); reacts to own user's
messages (/ and ! both resolve), threaded m.in_reply_to replies, @quiet
marker, ai bridge with per-room session, interactive editor. Added to
INTERACTIVE_CMDS.
Docs: POS.md, howto/communication.md, HOWTO.md, usage EXAMPLES.
Verified against a mock homeserver (send shape, login, owner filter,
replies, exit codes, editor). make gen && make check green.
Each line of event.env is an independent rule: ["msg" if ] <check> <op> <thr>.
Check runs on every pass; first numeric output compared float-safe; op is the
rightmost 'op threshold' pair so checks with their own >/< parse fine. Alerts
once on false->true + one recovery message on true->false (no repeats while
the condition holds); per-rule state keyed by rule-line hash in
~/.local/share/linux_post_install/eventer/state/.
Subcommands: run (timer entrypoint), config (interactive add/remove/edit with
check-validation), list (rules + live values), enable [interval] (systemd
user timer pos-event-trigger.timer; 5m..weekly or OnCalendar; graceful without
a user manager, loginctl enable-linger attempt), disable, status. --dry-run
honors the DEV.md convention. Alerts via lib/notify.sh (Telegram default).
New: bin/pos-system-event-trigger, lib/eventer-lib.sh, config/event.env
template (no-clobber via postinstall), install.sh lib install, INTERACTIVE_CMDS
entry. Docs: POS.md system row, HOWTO.md index, howto/event-trigger.md.
make gen && make check green; functional tests cover trigger/recovery/no-repeat,
float+unit parsing, editor add/remove/edit + validation + dry-run, timer
enable/disable/status, dispatcher routing.
Both tools: yt-dlp calls via spawn (honor DRY_RUN; --dry-run prints the
command and skips dep checks), -o/--output, --no-playlist, --cookies (with
file check), clean ffmpeg/yt-dlp guards, POS_FLAGS for completion, full
embedded metadata (--embed-metadata --embed-chapters --embed-thumbnail
--no-overwrites; mp3 adds --convert-thumbnails jpg + artist-from-uploader
parse-metadata). mp3 gains --by-artist (~/Music/<artist>/<title>.mp3).
mp4: -f <id>/--best/--worst skip the prompt (mutual-exclusion validated),
and the interactive picker now shows a curated [audio]/[video]/[combo]
format table on stderr (stdout carries only the chosen id — ui_pick
lesson), validates the id against the real -F output, and defaults empty
input to best. Docs: howto/media.md rewritten.
pos ai gemini: --session gives ask/chat persistent memory
(~/.local/share/linux_post_install/ai/<name>.json, capped 40 turns),
new 'sessions' subcommand to list/clear; --system injects a Gemini
systemInstruction on every turn without storing it in the session file.
Telegram listener keeps one session per chat (telegram-<chat_id>), clears
on 'ai /reset', passes a Telegram-voice system prompt (emojis welcome),
and strips markdown from replies before sendMessage since messages are
sent as plain text. Docs: howto/ai.md flags/sessions/bridge behavior.
- bin/pos-ai-gemini: one-shot ask (stdout-only, pipe-friendly), interactive
multi-turn chat REPL (q/Ctrl+C, /reset), models list; --model override,
default gemini-2.5-flash; key via x-goog-api-key header, never printed.
- Config scope 'ai' (AI_GEMINI_API_KEY secret, AI_GEMINI_MODEL) in
~/.config/linux_post_install/ai.env via 'pos config ai'; config/ai.env
template installed no-clobber by postinstall.
- Telegram listener: non-command text starting with 'ai ' (case-insensitive)
is answered by Gemini via 'pos ai gemini ask'; owner-chat only, errors
reply with the pos config ai hint. Future intents slot in as case arms.
- ai-gemini added to INTERACTIVE_CMDS (chat reads stdin).
- Docs: POS.md ai section + listener bridge, howto/ai.md, HOWTO/README
index rows, bin/pos usage example; make gen refreshed context/completions.
pos tree derives the hierarchy live from bin/pos-* filenames + # POS:
/# POS_SUBCMDS: headers, so it always matches what the dispatcher can
run. Category-less tool (like pos-config); --depth N limits nesting.
Also sharpen the agent-facing docs that the work exposed:
- AGENTS.md: drop the stale "one category-less exception" claim; document
the category-less pattern (pos-config, pos-tree) and that gen only reads
the text after the first em-dash in # POS: headers
- DEV.md: category-less vs categorized rule + header extraction note
- AGENT_Context "Adding a New Tool": new step 0 — pin the exact CLI verb
and runtime context (dev/repo-only vs installed) before writing code
- POS.md: new config + tree sections; category-less tools get their own
sections
- README: pos tree bullet following the pos X convention
- Drop sender 'config'/'config set' subcommand — redundant with 'pos config
telegram' (same POS_CONFIG registry: masked token input/display, chat-id
validation, chmod 600); sender/listener error hints now point there.
- Listener: load_map now uses a \x1f delimiter, so mapped /commands
containing '|' are no longer truncated at the first pipe.
- entertainment send: forward extra plugin args (previously dropped); pass
'--' before the message so leading '-' plugin output isn't parsed as an
option.
- write_config_key/cfg_write: replace unescaped sed writes with grep-v+append
so values with &, |, \ round-trip safely.
- sync_systemd: daemon-reload after removing timer units.
- config-ui: 'digits' validation accepts negative group/supergroup chat ids.
Map entries may now carry a menu description (/cmd::desc=bash) shown in
the bot's "/" menu; it falls back to the bash command (~40 chars) when
omitted. The command list is pushed automatically after every map edit,
on --enable, and at daemon start, plus a manual --sync-commands flag
(wired into POS_FLAGS completion). 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; an empty map clears the menu.
Fixes found by unit-testing the sync path: map_has always returned false
(awk END{exit 1} overrode the match), map_set had a local cross-reference
that broke under set -u, and warn() leaked into the generated JSON (now
stderr). Docs + AGENT_TODO updated; make gen && make check pass.
_pos_complete_tool walks up the key looking for a tool that declares
POS_FLAGS/POS_SUBCMDS; "${k%-*}" on a dash-free key returns it
unchanged, so once the walk hit a bare category (e.g. 'system' from
system-nfs-server) the while loop spun at 100% CPU. Any TAB at an
argument position of a tool without flags/subcmds froze the shell.
Stop the walk-up when no dash remains.
Also refresh AGENTS.md quick facts and regenerate doc file table
(completions/pos.bash 278->279 lines).
pos config <TAB> re-scanned every tool's '# POS_CONFIG:' header per
keypress (dozens of subshells, greps, seds). On the loaded homelab box a
stuck completion wedged interactive shells at 69% CPU for ~an hour. make
gen now emits a static _pos_config_scopes array into completions/pos.bash
(no runtime scan; live-scan fallback if the cache is missing).
Add bin/pos-config + lib/config-ui.sh: reads the '# POS_CONFIG:' headers
across tools into a unified runtime config (~/.config/linux_post_install/
<scope>.env, chmod 600). Secret masking with show/hide, digits:/num:/url:
validation, '-' clears, blank keeps; '*plugins' expands entertainment
plugin vars from entertainment-lib.sh. pos config with no scope opens a
picker (scope list when not a TTY); completion offers scopes from the
registry at runtime.
gen-docs.sh now handles category-less tools (pos-config) instead of
emitting pos-config-config; fix set -e/pipefail abort that truncated the
header registry mid-read. install.sh ships config-ui.sh.
The --send flag duplicated the send subcommand — completion suggested both.
Now 'pos communication telegram <TAB>' completes to just 'sender' and
'listener'. send is the single canonical action.
- git mv bin/pos-communication-telegram bin/pos-communication-telegram-sender
- remove --send branch + POS_FLAGS entry; add --markdown to completion
- lib/notify.sh: notify_sender_name() maps platform telegram -> telegram-sender
(notify_send otherwise looks for bin/pos-communication-telegram)
- pos-system-health --send 'sent:' check + pos-entertainment-send use the new
name and the send subcommand
- completion: keys with nested tools but no direct tool complete to the group
suffixes (sender/listener); --type/--parse-mode value completion moved to
the new word positions
- docs updated (POS.md, AGENT_Context, DEV.md, SCRIPTS.md, notify.env,
HOWTO.md, postinstall.sh); removed phantom webhook/log/broadcast subcommands
from howto/communication.md