Remove 123 temp/process files so the repo tracks only deliverable code
and docs:
- AgentsReport/ (86) + reportAgents/ (28): per-round agent handoff
reports — shared memory for the workflow, not project deliverables.
AgentsReport/ is now gitignored so future reports stay local-only.
- tmp_request.md: stale task ledger (R1-R7 all landed; file's own
instruction was 'delete when all are implemented').
- reports/bug-report-smb-server-access-denied.md: old bug writeup; the
fix landed in the stabilization round.
- FINAL_SUMMARY.md, IMPLEMENTATION_PLAN.md, AUDIT.md, AUDIT_TABLE.md,
Design-and-implement-a-self-describing-command-registry-for-POS.md:
unreferenced phase artifacts, superseded by code.
- Stray fragments: 'To' (empty), '.n' (stray redirect).
- AGENT_TODO.md: cleanup entry moved to Done.
Kept: DOC/ (19), tools-docs/ytsync.md (live-referenced), AGENT_TODO.md,
AGENTS.md, MAINTENANCE.md, bin/lib/apps/tests/config/scripts/templates/
systemd/entertainment/features/completions/, x64_bin/ (installer).
Nothing lost — git history retains every removed file.
Verified: make gen idempotent, make check OK, make lint 0 FAIL/0 WARN,
make test 17 files / 299 checks green, git diff --check clean.
User hit 'API error 402: ... You requested up to 131072 tokens, but can
only afford 4511' on the assist alias: no provider ever sent max_tokens,
so OpenRouter's credit pre-check billed the routed model's full
worst-case output; user also asked to bound session history to the last
5 requests/responses.
Architect decisions:
- AI_MAX_TOKENS (num, default 2048): sent as max_tokens on OpenRouter
and generationConfig.maxOutputTokens on Gemini — a real per-request
cost ceiling. llamacpp unchanged (local/free, no pre-check).
- AI_SESSION_TURNS (num, default 40 kept back-compat; messages, 2 per
exchange — 10 = last 5 conversations): resolved lazily in session_push
because config loads after the hardcoded line-25 default.
- Both registered in the bin/pos-ai POS_CONFIG @General section, so they
appear in 'pos config ai' with num: validation.
Reviewer hardening (CHANGES_REQUIRED -> fixed): unguarded env input could
reach jq tonumber (0/-5/010/abc all savable via config-ui's ^-?[0-9]+$)
and abort the CLI; both providers and session_push now guard with
^[1-9][0-9]*$ and fall back to the default.
Verified: fake-curl shim smoke (16 provider-body + 12 session-window
checks incl. the 010-regression proof), make gen idempotent, make check
OK, make lint 0 FAIL/0 WARN, make test 17 files / 299 checks / 0 fail
(~49s), bash -n clean, git diff --check clean. Reviewer ACCEPT (twice).
Tester regression round (permanent provider-body + session-pruning
coverage) intentionally not run this cycle — user's call; remains a
documented follow-up.
User report: pressing Enter on 'System prompt (empty = use built-in)'
silently returned to the menu — no alias created, and step labels read
[1/4] [2/4] in a 5-step flow.
Detective (pre-existing, not a 2026-09-06 regression): menu_ask_value's
documented contract is 'rc 1 = cancel, or empty answer with no default';
the step-4 call passed an empty default so the advertised empty answer
hit rc 1 and '|| return 0' aborted the flow. Same latent trap at the
alias-name step (empty-name warn/re-prompt was dead code). 11 other
call sites are correct (6 external rely on empty=cancel, 4 pass
defaults) — no global semantic change allowed.
Architect: opt-in --allow-empty flag on menu_ask_value (backward
compatible; empty+no-default -> rc 0 + empty value; genuine cancel/EOF
stays rc 1; default still wins). Builder: implemented in lib/menu-lib.sh
+ bin/pos-ai-alias (steps 1-2 relabeled /5, flag at the two approved
sites); 7-case smoke matrix PASS.
Tests: tests/t-menu-allow-empty.sh (30 checks) — semantics matrix
against the real menu_ask_value via non-TTY stdin, reader-contract
probes (empty-Enter rc 0 vs EOF rc 1), static guards on step labeling,
the exactly-2 flag call sites, edit-flow untouched, and a scope fence
over all pos-* tools. Pty E2E proven feasible (script -qec, 3 scenarios)
and documented in the Tester report; the E2E file itself remains a
follow-up.
Verified: make gen idempotent; make check OK; make lint 0 FAIL, 0 WARN;
make test 17 files / 299 checks / 0 fail / 0 skip (~49s); bash -n clean.
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.
The HF tree API returns entries shaped {oid,path,size,type} with no
rfilename field, so every downstream .rfilename read was null: the
--gguf filter crashed with 'jq: endswith() requires string inputs' and
single-file/all-files/meta modes silently built 'null' URLs. hf_repo_files
now normalizes tree entries to the {rfilename,size} shape the fallback
already emits (object-guarded; error-object bodies degrade to [] instead
of jq 5). The --gguf filter is type-guarded and empty results get
mode-aware messages. Verified: 12/12 fixture harness, live API 13->10
gguf, tiny real download OK, gates green. User confirmed the real
--gguf command now downloads [1/10].
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.
Generalizes the Telegram listener with a configurable text-prefix map
(telegram_prefixes.env): any non-command message '<word> <text>' runs
the mapped command with <text> appended as ONE quoted argument — e.g.
opencode=opencode turns 'opencode check cpu' into opencode "check cpu".
Routing order per message: text-prefix map → built-in Gemini ai bridge →
/command map → Unknown command. A mapped word shadows the Gemini bridge.
The prefix verb is reworked: bare = list map + bridge word; 'prefix
<word> <cmd...>' = map; 'prefix <word>' = show; 'prefix -r <word>' = remove.
The Gemini trigger word itself is now set via 'pos config telegram'
(TELEGRAM_AI_PREFIX).
Also extracted run_and_reply() to share the /command-map (60s) and
prefix (120s) execution semantics; fixed a latent set -e abort on
invalid templates in prefix_map_set's check_syntax call.
Verified: 27/27 routing-harness assertions, full CLI verb suite,
dispatch smoke, pos config telegram render, bash -n, make gen && make check,
make lint 0 FAIL / 0 WARN, shellcheck -S style (0 new findings).
menu_ask_value used line-oriented read -rp: a multiline Ctrl+V paste
flooded the tty queue, read consumed only the first line, and the rest
executed as commands later (or were eaten by a later prompt).
- lib/menu-lib.sh: new menu_read_value() raw-mode bracketed-paste
reader (stty -icanon -echo -isig, \e[?2004h/l, literal newlines inside
[200~..[201~, Enter submits outside paste, edit keys, cancel on
Ctrl-D-empty/Ctrl-C/Z/\, terminal restored via trap). Bytes via
dd|od|tr chunks, not bash read: read self-interrupts on ETX from a
tty even with ISIG disabled.
- bin/pos-ai-alias: prompt encode/decode (backslash, newline) with
load/save wiring; newline-safe truncate; edit wizard Enter keeps the
full original prompt (no more silent >80-char truncation).
Verified via pty harnesses: multiline + single-line paste captured
verbatim with nothing executed, Ctrl-D/Ctrl-C cancel cleanly, full
create/list/show/edit E2E, round-trips byte-exact. 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.
usb_detect offered a Ventoy stick's 32M VTOYEFI ESP as a sync/backup
target: with the data partition unmounted it was the only mounted
candidate, and cp died mid-copy with 'No space left on device'.
Detection now reads FSTYPE/PARTTYPENAME and drops EFI system
partitions from both the mounted list and the mount-offer list;
USB_MOUNTED entries carry mp|label|size|model|fs and usb_pick_root
shows that in the single-stick confirm and the multi-stick/partition
picker (USB_ROOT stays a bare mountpoint). pos-media-sync pre-flights
the exact payload size vs df free space (err, or warn under --dry-run)
before any copy.
GNU find (default -P) does not descend a command-line symlink to a
directory, so `find $HOME/Music ...` returned zero files and the tool
reported '0 added, 0 updated, 0 unchanged' without creating the target.
Switch to `find -H` (follows only command-line symlinks; inner-symlink
semantics unchanged).
CI is now live (act_runner on the Gitea host), so the docs stop saying a
runner 'needs to be registered' and record how the gate works:
- AGENTS.md: CI bullet now notes the live runner, red run = merge-blocker,
and the byte-order deterministic generator rule (LC_ALL=C, learned when the
CI container's locale reordered the category-less pos-config/pos-tree keys
and the gen-drift gate caught it).
- DEV.md: definition-of-done mentions the live CI re-run; new 'CI: Gitea
Actions Gate' section (runner location, CONFIG_FILE run.sh gotcha,
--add-host pin, one-time tokens, runnerv1 status enum 1=success/2=failure,
deterministic-generator convention, static-only limits).
- AGENT_Context step 7: pushing re-runs the gates, red run blocks.
- AGENT_TODO: dropped a duplicated 'gate added' Done entry and fixed its
stale 'runner pending' phrasing.
Plain sort in scripts/gen-docs.sh is locale-dependent: category-less tool
keys start with '|', which collates after letters under the CI container's
locale (pos-config/pos-tree moved to the end of the tree/dispatch/filetable
blocks). The new Gitea Actions gate caught this as a gen-drift failure on a
clean checkout. Force LC_ALL=C for byte-order sort and regenerate the doc
(config/tree now deterministically sort after the letter categories).
- .gitea/workflows/lint.yml: on push + pull_request runs make gen, then
git diff --exit-code (gen-drift check), then make check, then make lint
- requires a registered act_runner (ubuntu-latest label) to execute
- docs: AGENTS.md Quick facts 'no CI' -> CI bullet (runner required, gates
still run locally); DEV.md stub-harness note clarifies CI is static-gates
only; AGENT_TODO Done entry + Next item for runner registration
- verified locally: gen idempotent (0 gen-managed files changed), check OK,
lint 0 FAIL / 0 WARN
features/usb-automount.sh (installed via ./install.sh --feature, flag
usb-automount) mounts every unmounted removable block device at
/media/<label> — world-writable via -o umask=000 (fallback plain mount),
label-collision bump -2/-3, no-label -> usb-<name>. First root run
self-installs the hotplug udev rule (/etc/udev/rules.d/99-usb-automount.rules,
SYSTEMD_WANTS=usb-automount.service) + udevadm reload/trigger; an existing
rule is never overwritten. systemd/usb-automount.service (Type=oneshot,
WantedBy=multi-user.target) covers boot + hotplug + manual start, gated in
postinstall.sh's systemd loop exactly like autostart. Purpose: a plugged-in
stick is ready for pos system backup's post-verify USB copy without manual
mounting. Docs: SYSTEMD/SCRIPTS/README/AGENT_Context (tree, filetable —
postinstall.sh count corrected 152->163, it was already stale). Verified:
stub suite /tmp/opencode/usb-automount-test 47/47 green; make gen && make check.
Once the archive verifies, USB detection runs (so a stick plugged in while
the backup ran is found): mounted removable storage is auto-detected via
lsblk -J + a recursive jq filter (rm, mounted, type part|disk — JSON makes
spacey mountpoints safe), or BACKUP_USB_ROOT pins a fixed stick and skips
detection. None mounted → one re-scan prompt ('s' skips, EOF from cron
skips silently, rc stays 0); one stick → y/N confirm; several → numbered
pick (0 = skip). The copy lands in <usb>/backups/ (mkdir -p, chmod 600
best-effort — a vfat chmod failure warns, never fails the copy) and the
transfer is proven 100% by sha256 source-vs-copy before any success is
announced; a mismatch warns with both hashes, notifies 'USB copy FAILED',
and exits 1. The ERR trap is re-armed before the USB phase so a copy
failure no longer notifies 'Backup FAILED'. Docs: usage() Environment,
POS.md backup row, howto/system.md (USB section + env table + mismatch
troubleshooting), DEV.md system.env list. Stub suite
(/tmp/opencode/backup-test, HOME-isolated, sudo/gpg/lsblk/sender stubs,
corrupting-cp + vfat-chmod overrides, per-test lsblk JSON fixtures):
40/40 green.
Session-learned hardening (sole-developer project — terse, actionable):
- §7 env-override precedent list gains USER_SYSTEMD_DIR (network-download,
communication listeners, scheduler-lib) + the scheduler's SCHEDULE_* seams,
plus the missing-:-guard gotcha that silently writes to the real $HOME
under stub runs.
- New-tool test checklist gains an env-seam review step (grep for unguarded
config writes, prove with VAR=/tmp/x).
- §7 notes stub harnesses are throwaway by design: build in
/tmp/opencode/<tool>-test/, leave there, keep only the pattern.
- howto/schedule.md: migrate copies the rule LHS verbatim as COMMAND — the
old tool never had disk root/loadavg shorthands; rewrite those jobs.
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.