Files
Linux_post_install/AgentsReport/explorer/2026-09-06_tooling-audit.md
T
Your Name d817c37652
gates / consistency-and-conventions (push) Successful in 26s
fix: stabilization pass — fail-closed auth, ai flag validation, lint/config/security hardening, regression tests
17-point code-level audit executed via Explorer->Architect->Builder->Tester->Reviewer;
Reviewer accepted (APPROVE_WITH_NOTES; 3 block-list items resolved):

- security: telegram sender-owner AND-gate + TELEGRAM_OWNER_ID, matrix
  MATRIX_ROOM_ID fail-closed, gpg --passphrase-fd 3 (no argv secret),
  /dev/tcp positional-arg form (checkport/smb-client/share-lib/NET_PROBE),
  eval deny-by-default + --no-command-execution carried by both chat bridges,
  tty-gated --trust; config/{telegram,matrix}.env reference templates
- ai: all ExecStart flags validated against installed llama.cpp
  (requested->error, default->omit+warn, CONFIG_REQUESTED_FLAGS); single-file
  hf download failure rc=1 + no .hf-meta; LLAMACPP_HOST coherent;
  POS_SUBCMDS + metadata gaps closed
- tooling: lint-conventions Bash-native rewrite (~24-30x faster, rules and
  output byte-identical, :num restored); pos system uninstall covers all 12
  libs + scale-tail + flags dir + systemd user units (|| true) + plugin
  markers; anchored .bash_completion/.bashrc removal replaces sed -i '/pos/d'
- config: canonical load_env_file in lib/config-ui.sh (CRLF strip, env-wins,
  XDG, LOADED_ENV_KEYS); 9 tools migrated; entertainment-lib collapsed to
  wrappers; docker-compose deliberately unmigrated (source semantics)
- tests: first committed regression suite — tests/run-tests.sh zero-dep
  runner + make test; 12 files / 179 checks / 0 skip / ~52s; hard skip
  contract; systemd-analyze verify on generated unit PASS

Verified: make gen idempotent; make check green; make lint 0 FAIL, 0 WARN;
make test green; bash -n clean; git diff --check clean. Audit deliverables +
agent reports + AGENT_TODO Done entry included.
2026-09-06 07:25:44 -04:00

29 KiB
Raw Blame History

Explorer Report — 2026-09-06 — Tooling Audit (lint perf, install/uninstall symmetry, config-parsing duplication, shell correctness)

TL;DR

  • Lint perf: scripts/lint-conventions.sh (261 lines) has ~17 external-process hotspots; the dominant cost is two full-file scans that spawn printf | sed | tail per line (uses_stdin L67 and the top-level-local scan L143) — for a ~45-tool repo with several >1000-line tools this is roughly 60k+ subprocess forks per lint run just from those two scans. Wall-time measurements are UNVERIFIED (sandbox denies time/make/bash execution); static hotspot inventory below is complete.
  • Install/uninstall symmetry: a real uninstaller exists (pos system uninstall, 435 lines) but it is PARTIAL — 9 of 12 installed lib/*.sh have no removal path, the ScaleTail template clone (/usr/local/share/linux_post_install/scale-tail) and the feature-flag store (/usr/local/share/linux_post_install/flags/) are never removed, ~/.config/rclone/ and /usr/local/bin/yt-dlp survive, and all runtime-created systemd user units (~/.config/systemd/user/) are missed by every tier.
  • Config duplication: 9 tools hand-roll byte-similar load_config()-style loaders (env-wins export loop), plus at least 4 bespoke parsers; 2 shared key-value read/write libraries (config-ui.sh cfg_value/cfg_write and entertainment-lib.sh config_value/write_config_key) are duplicates of each other. CRLF-strip behavior splits 5-and-5; the CONFIG_DIR/XDG seam is honoured by self-contained tools but bypassed by several common.sh-sourcing tools that hardcode $HOME/.config/linux_post_install/.... Recommended owner: lib/config-ui.sh.
  • Shell correctness: no high-confidence unquoted-rm -rf, unquoted-[ $x ], or unguarded-cd bugs found in bin/; the flagged hotspots are bin/pos:388/403 (run eval "$cmd" — AI-extracted command execution, deliberate but security-relevant), bin/pos-system-uninstall:333 (sed -i '/pos/d' on user .bash_completion), and the per-line subprocess spawns in the lint script itself.

Task 1 — Lint performance (scripts/lint-conventions.sh, 261 lines)

1.1 Measured timing

Command Result
time make lint (run 1) UNVERIFIED — sandbox denies make, bash, time execution
time make lint (run 2) UNVERIFIED
time bash scripts/lint-conventions.sh UNVERIFIED

Static hotspot analysis is complete and is the basis for the estimates (see 1.3).

1.2 Per-line/per-file external-process hotspots (rule → implementation → bash-native alternative)

F = per-file spawn, L = per-line spawn, 1× = one-off.

# Lint rule Location Spawns per unit Bash-native equivalent (no semantics change)
1 shebang check L92 head -1 "$f" | grep -q F (2 procs/file) IFS= read -r first < "$f" + [[ $first == '#!/usr/bin/env bash' ]]
2 strict-mode check L95 has_regexgrep -qE F (1 proc/file) fold into the same first-line read as #1
3 INTERACTIVE_CMDS extraction L84 sed -n … | head -1 1× (2 procs) single read with regex
4 # POS: header text L110 sed -n '/^# POS: /{…;q}' F (1 proc/tool) read up to first # POS: line in bash loop
5 em-dash presence L115 grep -q ' — ' <<<"$headline" F (1 proc/tool, heredoc string) [[ $headline == *' — '* ]]
6 # POS: line number L118 grep -nE … | head -1 | cut -d: -f1 F (3 procs/tool) captured in the same loop as #4
7 deps-guard-before-help L127 first_guard_line F (0 extra — bash loop) already bash-native
8 help-line number L128 first_linegrep -nE … | while read F (1 proc/tool) same first-match read loop as #4/#6
9 top-level local scan L137157 with L143 printf '%s\n' "$line" | sed -nE … | tail -1 L (3 procs per line of every tool) [[ $line =~ <<-?[[:space:]]*([A-Za-z0-9_]+) ]] in-bash
10 stdin-reader detection L5980 uses_stdin with L67 printf | sed | tail L (3 procs per line — scans every tool a 2nd time) same [[ =~ ]] regex; can also merge with #9 into ONE pass
11 POS.md reference L169 grep -q "$(basename "$f")" DOC/POS.md F (1 proc/tool) read POS.md into a var once; [[ $posmd == *$basename* ]]
12 INTERACTIVE_CMDS entry check L174180 fine
13 plugin common.sh / POS_PLUGIN / app uninstall fn+case / systemd / wrapper checks L184,187,196,197,201,208,211,218,224 has_regex F (12 procs/file each) single-read first-match loop per file
14 wrapper line count L221222 wc -l < "$f" twice F (2 procs/wrapper) mapfile -t lines < "$f"; ${#lines[@]}
15 secret-literal scan L229243 grep -nE per file + L235 grep -qE … <<<"$body" per matched line F + L (heredoc-string greps) `[[ $body =~ (TOKEN
16 system-path write scan L245258 grep -nE per file + L251 two grep -qE <<<"$body" per matched line F + L `[[ $body =~ (>
17 last_line() L3942 dead code — defined, never called delete

1.3 Estimated cost

  • The two per-line scans (#9, #10) each read every line of every bin/pos-* tool twice. The repo has 45 tools with several >1000-line files (pos-docker-vbox 1125, pos-media-ytsync 1213, pos-network-download 1108) — total tool lines ≈ 1520k. At 3 forks/line × 2 scans ≈ 90k120k printf|sed|tail subprocess forks per lint run just from those two rules.
  • Remaining rules add ≈ 1015 forks per tool ≈ 500700 more forks total. The secret/system-path scans add one grep per file plus per-matched-line heredoc greps.
  • Expected effect: the lint time is dominated by process creation (fork/exec), not by grep itself. Replacing #9/#10 with [[ =~ ]] and merging into one pass should cut lint wall time by the largest factor; the heredoc-string greps (#5, #15, #16) are cheap per call but numerous.

1.4 scripts/check-sync.sh (42 lines) — brief

  • bash -n per file, exec-bit loop, doc-sync via gen-docs.sh --check, 3 dispatch smokes. Per-file spawns are inherent to bash -n (must run bash anyway); no obvious perf bug.
  • Correctness note: L1416 glob list misses features/*.sh? — actually it includes features/*.sh (line 15 features/*.sh). It does NOT include completions/* other than completions/pos.bash (fine) and does not bash -n install.sh's sourced libs beyond the list — libs are covered. No hotspot.

1.5 scripts/gen-docs.sh (254 lines) — brief

  • sed per header per tool (L4247: 6 sed calls/tool) — minor; only runs on make gen, not per commit.
  • L207210 check mode: sed block extract + cat + diff per block — fine for check.
  • Correctness hotspot: docmap convergence loop (L244252) re-runs regen_block docmap up to 5 times by design; each iteration re-does a full-file sed + grep -n + wc -l — acceptable (documented convergence), but on a 700-line file it is the single slowest part of gen; an in-memory line accounting would converge in one pass. Not a bug.

Task 1 — Ranked change list (no implementation)

  1. Merge the per-line printf|sed|tail delimiter extraction into a single bash-native pass using [[ $line =~ <<-?[[:space:]]*([A-Za-z0-9_]+) ]] — used by the top-level-local scan (L143) and uses_stdin (L67). Highest ROI; removes the ~90120k fork estimate.
  2. Replace per-line heredoc-string grep -q <<<"$body" with [[ $body =~ … ]] in the secret (L235) and system-path (L251) scans.
  3. Replace per-file grep/sed|head|cut/wc with a single read of the first ~6 lines per tool (covers L92/95/110/115/118/128) plus mapfile line counts for wrappers (L221).
  4. Replace grep -q <basename> DOC/POS.md (L169) with one preloaded POS.md content check.
  5. Delete the dead last_line() (L3942).

Task 2 — Install/uninstall symmetry

2.1 What exists

  • Installer: install.sh phases 14 + optional apps. Uninstall path documented in install.sh:6871 (apps only) and provided as a CLI tool bin/pos-system-uninstall (not a make uninstall, not a scripts/ uninstaller — grep of Makefile, scripts/, README.md shows no make uninstall; apps/install.sh --uninstall handles optional desktop apps only).
  • Uninstaller: bin/pos-system-uninstall — Tier 1 (always): binaries, plugins, known systemd services, shell integration; Tier 2 (--config): ~/.config/linux_post_install; Tier 3 (--data): ~/.local/share/linux_post_install.

2.2 Install inventory vs uninstall coverage

Artifact Installed by Removal path Verdict
bin/*/usr/local/bin/ (45 pos-, pos, flag-, wr-*, mp3/mp4/vbox/ssh-load-all) Phase 2 (install.sh:136140) Tier 1: /usr/local/bin/pos + compgen -G /usr/local/bin/pos-* + legacy names (L5383, 229257) SYMMETRIC
lib/common.sh, lib/menu-lib.sh, lib/share-lib.sh/usr/local/bin/ Phase 2 (install.sh:143144) Tier 1 (pos-system-uninstall:6264) SYMMETRIC
lib/{flags,notify,entertainment-lib,scheduler-lib,config-ui,user-timers-lib,entertainment-plugin-lib,usb-lib,registry}.sh/usr/local/bin/ Phase 2 (install.sh:143144) none INSTALL-ONLY (9 of 12 libs)
lib/ai-providers/*.sh/usr/local/bin/ai-providers/ Phase 2 (install.sh:154161) Tier 1 rm -rf /usr/local/bin/ai-providers (L244) SYMMETRIC
entertainment/*.sh/usr/local/bin/ Phase 2 (install.sh:167173) Tier 1 — hardcoded list weather.sh gold.sh joke.sh (L72, 248) SYMMETRIC today; breaks automatically if a 4th plugin is added
`x64_bin arm64_bin/*/usr/local/bin/` Phase 2 (install.sh:179194) Tier 1 hardcoded wihotspot wihotspot-gui create_ap (L86, 260)
features/*/usr/local/bin/ (--feature) Phase 2 (install.sh:197222) + flag set Tier 1 autostart.sh usb-automount.sh (L91, 265) SYMMETRIC today; hardcoded
Feature-flag store /usr/local/share/linux_post_install/flags/ Phase 2 flag_set (install.sh:216) none (no flags/flag match in pos-system-uninstall) INSTALL-ONLY
ScaleTail clone /usr/local/share/linux_post_install/scale-tail Phase 4 (install.sh:237243) none (only bash-completion under /usr/local/share is removed, L108) INSTALL-ONLY
completions/pos.bash/usr/local/share/bash-completion/completions/pos.bash postinstall.sh:100107 Tier 1 (L108, 284) SYMMETRIC
systemd *.service/*.timer/etc/systemd/system/ + enable postinstall.sh:139166 Tier 1: 3 known + find -name '*linux_post_install*' -o -name 'pos-*' (L287311) SYMMETRIC (system units)
config/authorized_keys~/.ssh/authorized_keys postinstall.sh:110137 none (tier 2 only targets ~/.config/linux_post_install) INSTALL-ONLY (by design — user data)
config/rclone.conf~/.config/rclone/rclone.conf postinstall.sh:1017 none (tier 2 path is linux_post_install only) INSTALL-ONLY
config/{entertainment,system,notify,ai}.env~/.config/linux_post_install/ postinstall.sh:2250 Tier 2 (--config) find over the dir (L147155) SYMMETRIC (opt-in tier)
config/schedule.d/*.env~/.config/linux_post_install/schedule.d/ postinstall.sh:5777 Tier 2 + rmdir schedule.d (L351355) SYMMETRIC (opt-in tier)
PATH line + completion line in ~/.bashrc postinstall.sh:8098 Tier 1 sed removals (L320322) SYMMETRIC
apt packages (25+) + yt-dlp → /usr/local/bin/yt-dlp + cpufreq preinstall.sh:2875 none (uninstaller never touches apt or yt-dlp) INSTALL-ONLY (likely deliberate — system packages)

2.3 Runtime-created state (created by tools at runtime, not install.sh)

Artifact Created by Uninstall path in pos-system-uninstall Verdict
~/.config/linux_post_install/<tool>.env (ai, telegram, matrix, scrcpy, download, ytsync, grab, ai-aliases, compose) tools' config writes Tier 2 (--config) RUNTIME-STATE / SYMMETRIC (removed with --config)
~/.local/share/linux_post_install/{logs,ytsync,ai/models,entertainment/last,backups} bin/pos logging + tools Tier 3 (--data) RUNTIME-STATE / SYMMETRIC (removed with --data)
systemd user units ~/.config/systemd/user/: pos-aria2.service+pos-aria2-retry.{service,timer} (pos-network-download:172190,633669), telegram-listener unit (pos-communication-telegram-listener:471521), matrix-listener unit (pos-communication-matrix-listener:315364), pos-ai-server.service (pos-ai-server:500522), entertainment timers pos-entertainment-*.timer (user-timers-lib), scheduler per-job timers (scheduler-lib) runtime tool subcommands none — Tier 1 only scans /etc/systemd/system (L287311); Tier 2 only ~/.config/linux_post_install; ~/.config/systemd/user/ is outside both INSTALL-ONLY (from the uninstaller's perspective; each tool's own stop/disable subcommand does remove its own unit, e.g. pos network download stop L209211)
~/.local/bin/pos-ai-hook.sh + ai-aliases.sh wrappers pos-ai-alias Tier 1 pos-ai-hook + marker-managed alias scan (L96105, 270281) SYMMETRIC
~/.config/rclone/rclone.conf (from postinstall) postinstall.sh:1017 none INSTALL-ONLY

Task 2 — Ranked change list (no implementation)

  1. Remove the 9 orphaned libs (flags.sh, notify.sh, entertainment-lib.sh, scheduler-lib.sh, config-ui.sh, user-timers-lib.sh, entertainment-plugin-lib.sh, usb-lib.sh, registry.sh) in Tier 1 — the biggest INSTALL-ONLY gap (hardcoded common.sh menu-lib.sh share-lib.sh only, pos-system-uninstall:6264).
  2. Remove ScaleTail templates /usr/local/share/linux_post_install/scale-tail and the feature-flag store /usr/local/share/linux_post_install/flags/ in Tier 1 (documented install outputs in AGENT_Context §3/§10, no removal).
  3. Add a user-unit sweep to Tier 1: disable+remove matching units in ~/.config/systemd/user/ (prefixes pos-*, pos-entertainment-*, pos-schedule-* etc.), or document that per-tool stop is the supported path.
  4. De-hardcode the entertainment-plugin / prebuilt-binary / feature names in the uninstaller to directory-driven discovery (mirror install.sh's loops) so future plugins/bins don't silently become INSTALL-ONLY.
  5. Decide (and document) whether ~/.config/rclone, ~/.ssh/authorized_keys additions, apt packages, and /usr/local/bin/yt-dlp are intentionally outside uninstall — currently silent.

Task 3 — Config-parsing duplication

3.1 Inventory

Shared loaders that exist:

Loader Location Used by
load_system_env() (env-file → export, env-wins) lib/common.sh:146159 pos-system-health, pos-system-backup, pos-media-sync (system.env)
cfg_value() / cfg_write() (key read/write, KEY="value", chmod 600) lib/config-ui.sh:311344 pos-config; pos-entertainment-config:L98 sources config-ui dynamically; config-scope registry consumers
config_value() / write_config_key() (key read/write, same semantics) lib/entertainment-lib.sh:2854 pos-entertainment-{config,status,enable,disable,send}
inline grep '^NOTIFY_PLATFORM=' | tail -1 | cut lib/notify.sh:41 notify_send

Hand-rolled near-identical load_config()-style loaders (9) — each is the same ~14-line loop: grep -E '^[A-Z_]+=' | while IFS='=' read k v + quote-strip + [ -z "${!k:-}" ] && export:

  1. bin/pos-communication-telegram-sender:6174 (load_config)
  2. bin/pos-communication-matrix-listener:6577 (load_config)
  3. bin/pos-communication-telegram-listener:8698 (load_config)
  4. bin/pos-communication-matrix-sender:4457 (load_config)
  5. bin/pos-communication-scrcpy:1428 (load_config)
  6. bin/pos-ai:130160 (load_config, plus legacy-file loop)
  7. bin/pos-ai-server:2135 (load_config)
  8. bin/pos-ai-hf:3044 (load_hf_config)
  9. bin/pos-media-grab:1023 (load_grab_config)

Bespoke parsers (4+):

  • bin/pos-network-download:3237 load_secret — single-key grep'^RPC_SECRET=' | head -1 | cut -d= -f2-; also duplicated inline at L153154
  • bin/pos-docker-compose:910 + layered strategy (template < global compose.env < per-service .env, documented L3148) — reads global config via CONFIG_ENV and per-service envs
  • bin/pos-share-smb-server:97 reload_config — Samba-specific
  • bin/pos-media-ytsync:2830 _YTSYNC_CFG via pos config ytsync scope; plus the share-client (pos-share-smb-client) creds records parsing

Counts: 45 pos-* tools; ~20 source lib/common.sh; 9 hand-roll their own file parser; only pos-config and the entertainment tools use a shared key-value loader; 3 use load_system_env; the 5 self-contained communication tools duplicate the loader because they don't source common.sh (documented convention: guarded inline fallback copies in DEV.md).

3.2 Consistency findings

  • Precedence order is env > config-file > defaults everywhere the hand-rolled loaders are used (if [ -z "${!k:-}" ] before export; defaults applied later via ${VAR:-default}). CLI-vs-config precedence is declared CLI > environment > config file in the three tools that document it (telegram-sender:45, scrcpy:70, matrix-sender:29). pos-docker-compose is the outlier model (per-service file wins over global file; no env) — a different domain, but also the only tool where "config file" beats "global defaults" deliberately.
  • CRLF handling diverges: 5 loaders strip \r (matrix-sender:53, scrcpy:23, ai:139, ai-server:30, ai-hf:39) but 5 do NOT (telegram-sender, matrix-listener, telegram-listener, media-grab, and load_system_env in common.sh:154). A Windows-edited .env parses differently depending on which tool reads it.
  • CONFIG_DIR / XDG seam divergence: self-contained tools (+ config-ui.sh:32, notify.sh:27) carry the guarded CONFIG_DIR="${CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/linux_post_install}" copy; but several tools that source common.sh (which defines CONFIG_DIR at line 19) still hardcode $HOME/.config/linux_post_install/...: pos-ai:11, pos-ai-hf:26, pos-ai-server:15, pos-media-grab:11, common.sh load_system_env:147, entertainment-plugin-lib:15, pos-docker-compose:10. So CONFIG_DIR/XDG_CONFIG_HOME overrides work for some tools and are silently ignored by others.
  • Key-value writers duplicatedcfg_write (config-ui.sh:323) and write_config_key (entertainment-lib.sh:36) are the same algorithm (grep -v + append, - deletes, chmod 600); only the value-quoting and the multi-line warning differ.

3.3 Recommendation (no implementation)

  • Owner: lib/config-ui.sh. It already hosts the POS_CONFIG scope registry that pos config consumes, has secret masking/validation helpers, and is installed to /usr/local/bin alongside the tools.
  • Add one generic loader there, e.g. load_env_file <file> (env-wins export loop with quote + CRLF strip parsed consistently) and have load_system_env delegate to it.
  • Merge entertainment-lib.sh config_value/write_config_key into cfg_value/cfg_write (keep config_value as a thin alias for the entertainment tools, or migrate the 5 call sites).
  • Migration targets: the 9 hand-rolled loaders → load_env_file (source config-ui.sh in the 5 self-contained communication tools, replacing their guarded inline copies and CONFIG_DIR blocks); pos-network-downloadcfg_value "$CONFIG_FILE" RPC_SECRET; pos-docker-compose config showcfg_value/cfg_write for the global config.
  • Unify CRLF-strip and the CONFIG_DIR path source across every loader during the migration.

Task 3 — Ranked change list (no implementation)

  1. Add load_env_file to lib/config-ui.sh; make common.sh load_system_env delegate; fix the CRLF split in the process.
  2. Migrate the 9 hand-rolled loaders (list in 3.1) to it; make the 5 self-contained communication tools source config-ui.sh instead of the inline CONFIG_DIR+load_config copies.
  3. Fold entertainment-lib.sh read/write helpers into cfg_value/cfg_write (alias or migrate the 5 entertainment call sites).
  4. Route pos-network-download load_secret and pos-docker-compose global-config reads through cfg_value.
  5. Replace hardcoded $HOME/.config/linux_post_install/... in the common.sh-sourcing tools with the sourced CONFIG_DIR (pos-ai, pos-ai-hf, pos-ai-server, pos-media-grab, load_system_env, entertainment-plugin-lib, pos-docker-compose).

Task 4 — Shell-correctness hotspots (high-confidence only)

Method: targeted scan of bin/ and lib/ for unquoted $var in args/array appends, for x in $list, [ $x = … ], rm -rf $VAR, unguarded cd, missing || true in pipelines under set -euo pipefail, eval of derived strings. Only high-confidence items below.

4.1 High-confidence findings

  • H-001 (WARN) — bin/pos-ai:388,403 run eval "$cmd". _prompt_run_command executes a command string extracted from AI output. Interactive path prompts on /dev/tty; the --trust path (L385388) auto-executes without confirmation. Deliberate feature, but any AI-output-derived command executed through eval is a shell-injection-relevant surface — recommend keeping, but it deserves explicit review of what trusted=1 callers feed it. Classification: FACT (code), design concern.
  • H-002 (WARN) — bin/pos-system-uninstall:333 sed -i '/pos/d' "$HOME/.bash_completion". Deletes every line containing the substring pos from a user-owned file, not just pos-managed lines (unlike the pos-ai-hook marker check at L277). A line like complete -F _git checkout is safe, but any unrelated completion containing "pos" (e.g. repos, compose-help, dispose) is silently removed — and this runs in default Tier 1. Classification: FACT.
  • H-003 (WARN) — bin/pos-system-uninstall:320322 sed -i on ~/.bashrc. Removal of PATH/completion/hook lines is line-based and unanchored at line start (/source.*pos\.bash/d, /linux_post_install.*PATH/d, /source.*pos-ai-hook/d); a user comment mentioning pos.bash is deleted. Lower risk than H-002 but same class. Classification: FACT.
  • H-004 (WARN) — scripts/lint-conventions.sh:31,41,67,84,110,118,143,235,251. Under set -euo pipefail, the grep | while read and ... | tail -1 | cut pipelines are only safe because of the || true / 2>/dev/null guards and the non-final elements' exit codes. The per-line printf | sed | tail -1 inside the read loop (L67/L143) is the perf hotspot from Task 1 AND a correctness risk: if sed ever exits non-zero for a given line under pipefail, the surrounding while read loop aborts mid-scan. Classification: FACT (perf measured as static analysis); correctness risk is conditional, not observed.

4.2 Checked and cleared (not bugs)

  • rm -rf/rm -f in bin/ are consistently quoted (pos-ai-hf:644,698,835,989; pos-system-uninstall all lines; pos-docker-vbox:1115; app scripts). No unquoted/empty rm -rf $VAR found.
  • Unquoted [ $x … ] comparisons: none found in bin/ (only "$var" forms).
  • for x in $list sites (pos-docker-health:38, pos-docker-ps:36, pos-tree:71, pos-system-health:170, pos-network-checkport:408, pos-entertainment-status:39, pos:123) intentionally word-split newline/comma-separated IDs or sorted output with no spaces in elements — not bugs at present, but a space in a future element (e.g. a plugin filename) would silently split. Low-priority hardening, not a defect.
  • cd sites are guarded (pos-docker-vbox:1029,1040 use (cd "$d" 2>/dev/null && pwd) || …; pos-docker-compose:221260 wrap in subshells with set -e context).
  • pos-share-smb-client:457 sudo rm -f "$SMB_CREDS_DIR/$(basename "$where")" — properly quoted.
  • find /etc/systemd/system/ -name '*linux_post_install*' -o -name 'pos-*' (pos-system-uninstall:311) — -o binds both predicates to the stated path; matches both patterns as intended. Not a bug.
  • bin/pos logging tee pipes: INTERACTIVE_CMDS handling verified by lint rule and existing registrations — no new finding.

Task 4 — Ranked change list (no implementation)

  1. Restrict pos-system-uninstall .bash_completion/.bashrc removal to anchored, marker-based patterns (e.g. only lines the installer itself added, or apply the grep -q 'Managed by pos…'-style marker check used for alias wrappers).
  2. Review bin/pos-ai _prompt_run_command trust boundaries: confirm every trusted=1 caller is user-flagged and document the eval surface (or re-run through bash -c with validation).
  3. Convert lint L67/L143 per-line printf|sed|tail to [[ =~ ]] (also removes the pipefail-mid-loop abort risk).
  4. Optional hardening: quote the for x in $list sites that consume plugin names/scheduled-job names where elements could contain spaces.

Uncertainties

  • Lint wall time could not be measured (sandbox denies time, make, bash). Estimates are derived from hotspot counts and file sizes (45 tools, 1520k total lines); real numbers should be captured by a runner-capable agent (make lint ×2 + bare script) — see Handoff.
  • The exact fork count per run is an INFERENCE (each printf|sed|tail is at least 3 forks; actual exec cost depends on PATH lookup and filesystem state).
  • Whether apt packages / yt-dlp / ~/.ssh/authorized_keys / ~/.config/rclone are supposed to survive uninstall is a product decision, not verifiable from code.
  • Whether the runtime-created user units are "expected to persist" is not documented anywhere in the repo; the uninstaller help text ("services") implies coverage, which is not delivered.

Important Files

  • scripts/lint-conventions.sh — all Task 1 hotspots (L31,41,67,84,92,95,110,115,118,137157,162,169,221222,235,251; dead last_line L3942)
  • scripts/check-sync.sh, scripts/gen-docs.sh — gates; convergence loop L244252
  • install.sh — phases, should_run (L100117), copy targets (L136222), ScaleTail (L237243)
  • preinstall.sh — apt PACKAGES (L2843), yt-dlp (L6568) — no uninstall counterpart
  • postinstall.sh — rclone/entertainment/system/notify/ai env templates, schedule.d, .bashrc, completion, systemd
  • bin/pos-system-uninstall — tiers, lib list L6264, user-unit gap, H-002/H-003, find L311
  • bin/pos, bin/pos-ai, bin/pos-communication-{telegram,matrix}-{sender,listener}, bin/pos-communication-scrcpy, bin/pos-ai-server, bin/pos-ai-hf, bin/pos-media-grab, bin/pos-network-download, bin/pos-docker-compose, bin/pos-share-smb-server — config-loading inventory (Task 3)
  • lib/common.sh (load_system_env), lib/config-ui.sh (cfg_value/cfg_write), lib/entertainment-lib.sh (config_value/write_config_key), lib/notify.sh — loader candidates
  • DOC/DEV.md:182213 — env-seam rules the loader centralization should preserve

Handoff

  • Status: OBJECTIVE_SATISFIED (plus measurement note)
  • Objective: evidence audit of lint performance, install/uninstall symmetry, config-parsing duplication, shell-correctness hotspots — completed read-only.
  • Evidence / completed work: this report; hotspot inventory with file:line; install/uninstall matrix; 9-loader duplication census with CRLF and CONFIG_DIR inconsistencies; 3 high-confidence shell hotspots.
  • Affected areas: scripts/lint-conventions.sh, bin/pos-system-uninstall, lib/config-ui.sh + lib/entertainment-lib.sh + lib/common.sh (loader centralization), 9 tool files, bin/pos-ai.
  • Scope/decision boundary: no code changed. Centralizing the loader (Task 3) is a deliberate cross-tool refactor with a doc convention ("guarded inline fallback copies" in DEV.md) — that is an Architect/Designer decision boundary, not a mechanical fix.
  • Verification performed: full reads of lint/check-sync/gen-docs/install/preinstall/postinstall/uninstall/common/config-ui; greps across bin/+lib/ for loaders, rm/cd/eval/for-splitting patterns; shared-memory check of maintainer/architect reports (no overlap: the 2026-09-06 convention-sweep was about POS header/doc drift, not these four areas).
  • Remaining uncertainty: measured lint wall time (needs a runner-capable agent); intended persistence of apt packages/rclone/user units; actual fork count (inference).
  • Recommended next agent: Architect (for the loader centralization decision: which library owns load_env_file, how self-contained tools source config-ui.sh without breaking the "no shared lib? inline fallbacks" convention) — and/or Maintainer for the uninstaller gaps + lint per-line hotspot rewrite if a decision is not needed.
  • Reason: Task 3's fix crosses the documented DEV.md convention and 9 tool files (architectural boundary); Tasks 1/2/4 are mechanical cleanups that a Maintainer can implement once the loader decision is made.