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.
29 KiB
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 spawnprintf | sed | tailper line (uses_stdinL67 and the top-level-localscan 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 deniestime/make/bashexecution); 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 installedlib/*.shhave 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-dlpsurvive, 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.shcfg_value/cfg_writeandentertainment-lib.shconfig_value/write_config_key) are duplicates of each other. CRLF-strip behavior splits 5-and-5; theCONFIG_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-cdbugs found inbin/; the flagged hotspots arebin/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_regex → grep -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_line → grep -nE … | while read |
F (1 proc/tool) | same first-match read loop as #4/#6 |
| 9 | top-level local scan |
L137–157 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 | L59–80 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 | L174–180 | — | 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 (1–2 procs/file each) | single-read first-match loop per file |
| 14 | wrapper line count | L221–222 wc -l < "$f" twice |
F (2 procs/wrapper) | mapfile -t lines < "$f"; ${#lines[@]} |
| 15 | secret-literal scan | L229–243 grep -nE per file + L235 grep -qE … <<<"$body" per matched line |
F + L (heredoc-string greps) | `[[ $body =~ (TOKEN |
| 16 | system-path write scan | L245–258 grep -nE per file + L251 two grep -qE <<<"$body" per matched line |
F + L | `[[ $body =~ (> |
| 17 | last_line() |
L39–42 | 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 ≈ 15–20k. At 3 forks/line × 2 scans ≈ 90k–120kprintf|sed|tailsubprocess forks per lint run just from those two rules. - Remaining rules add ≈ 10–15 forks per tool ≈ 500–700 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 -nper file, exec-bit loop, doc-sync viagen-docs.sh --check, 3 dispatch smokes. Per-file spawns are inherent tobash -n(must run bash anyway); no obvious perf bug.- Correctness note: L14–16 glob list misses
features/*.sh? — actually it includesfeatures/*.sh(line 15features/*.sh). It does NOT includecompletions/*other thancompletions/pos.bash(fine) and does notbash -ninstall.sh's sourced libs beyond the list — libs are covered. No hotspot.
1.5 scripts/gen-docs.sh (254 lines) — brief
sedper header per tool (L42–47: 6 sed calls/tool) — minor; only runs onmake gen, not per commit.- L207–210 check mode:
sedblock extract +cat+diffper block — fine for check. - Correctness hotspot: docmap convergence loop (L244–252) re-runs
regen_block docmapup to 5 times by design; each iteration re-does a full-filesed+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)
- Merge the per-line
printf|sed|taildelimiter extraction into a single bash-native pass using[[ $line =~ <<-?[[:space:]]*([A-Za-z0-9_]+) ]]— used by the top-level-localscan (L143) anduses_stdin(L67). Highest ROI; removes the ~90–120k fork estimate. - Replace per-line heredoc-string
grep -q <<<"$body"with[[ $body =~ … ]]in the secret (L235) and system-path (L251) scans. - Replace per-file
grep/sed|head|cut/wcwith a single read of the first ~6 lines per tool (covers L92/95/110/115/118/128) plusmapfileline counts for wrappers (L221). - Replace
grep -q <basename> DOC/POS.md(L169) with one preloaded POS.md content check. - Delete the dead
last_line()(L39–42).
Task 2 — Install/uninstall symmetry
2.1 What exists
- Installer:
install.shphases 1–4 + optional apps. Uninstall path documented ininstall.sh:68–71(apps only) and provided as a CLI toolbin/pos-system-uninstall(not amake uninstall, not a scripts/ uninstaller — grep ofMakefile,scripts/,README.mdshows nomake uninstall;apps/install.sh --uninstallhandles 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:136–140) | Tier 1: /usr/local/bin/pos + compgen -G /usr/local/bin/pos-* + legacy names (L53–83, 229–257) |
SYMMETRIC |
lib/common.sh, lib/menu-lib.sh, lib/share-lib.sh → /usr/local/bin/ |
Phase 2 (install.sh:143–144) | Tier 1 (pos-system-uninstall:62–64) | 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:143–144) | none | INSTALL-ONLY (9 of 12 libs) |
lib/ai-providers/*.sh → /usr/local/bin/ai-providers/ |
Phase 2 (install.sh:154–161) | Tier 1 rm -rf /usr/local/bin/ai-providers (L244) |
SYMMETRIC |
entertainment/*.sh → /usr/local/bin/ |
Phase 2 (install.sh:167–173) | 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:179–194) | Tier 1 hardcoded wihotspot wihotspot-gui create_ap (L86, 260) |
features/* → /usr/local/bin/ (--feature) |
Phase 2 (install.sh:197–222) + 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:237–243) | 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:100–107 | Tier 1 (L108, 284) | SYMMETRIC |
systemd *.service/*.timer → /etc/systemd/system/ + enable |
postinstall.sh:139–166 | Tier 1: 3 known + find -name '*linux_post_install*' -o -name 'pos-*' (L287–311) |
SYMMETRIC (system units) |
config/authorized_keys → ~/.ssh/authorized_keys |
postinstall.sh:110–137 | none (tier 2 only targets ~/.config/linux_post_install) |
INSTALL-ONLY (by design — user data) |
config/rclone.conf → ~/.config/rclone/rclone.conf |
postinstall.sh:10–17 | none (tier 2 path is linux_post_install only) |
INSTALL-ONLY |
config/{entertainment,system,notify,ai}.env → ~/.config/linux_post_install/ |
postinstall.sh:22–50 | Tier 2 (--config) find over the dir (L147–155) |
SYMMETRIC (opt-in tier) |
config/schedule.d/*.env → ~/.config/linux_post_install/schedule.d/ |
postinstall.sh:57–77 | Tier 2 + rmdir schedule.d (L351–355) | SYMMETRIC (opt-in tier) |
PATH line + completion line in ~/.bashrc |
postinstall.sh:80–98 | Tier 1 sed removals (L320–322) | SYMMETRIC |
apt packages (25+) + yt-dlp → /usr/local/bin/yt-dlp + cpufreq |
preinstall.sh:28–75 | 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:172–190,633–669), telegram-listener unit (pos-communication-telegram-listener:471–521), matrix-listener unit (pos-communication-matrix-listener:315–364), pos-ai-server.service (pos-ai-server:500–522), 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 (L287–311); 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 L209–211) |
~/.local/bin/pos-ai-hook.sh + ai-aliases.sh wrappers |
pos-ai-alias | Tier 1 pos-ai-hook + marker-managed alias scan (L96–105, 270–281) | SYMMETRIC |
~/.config/rclone/rclone.conf (from postinstall) |
postinstall.sh:10–17 | none | INSTALL-ONLY |
Task 2 — Ranked change list (no implementation)
- 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 (hardcodedcommon.sh menu-lib.sh share-lib.shonly, pos-system-uninstall:62–64). - Remove ScaleTail templates
/usr/local/share/linux_post_install/scale-tailand the feature-flag store/usr/local/share/linux_post_install/flags/in Tier 1 (documented install outputs in AGENT_Context §3/§10, no removal). - Add a user-unit sweep to Tier 1: disable+remove matching units in
~/.config/systemd/user/(prefixespos-*,pos-entertainment-*,pos-schedule-*etc.), or document that per-toolstopis the supported path. - 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.
- Decide (and document) whether
~/.config/rclone,~/.ssh/authorized_keysadditions, apt packages, and/usr/local/bin/yt-dlpare 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:146–159 | 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:311–344 | 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:28–54 | 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:
- bin/pos-communication-telegram-sender:61–74 (
load_config) - bin/pos-communication-matrix-listener:65–77 (
load_config) - bin/pos-communication-telegram-listener:86–98 (
load_config) - bin/pos-communication-matrix-sender:44–57 (
load_config) - bin/pos-communication-scrcpy:14–28 (
load_config) - bin/pos-ai:130–160 (
load_config, plus legacy-file loop) - bin/pos-ai-server:21–35 (
load_config) - bin/pos-ai-hf:30–44 (
load_hf_config) - bin/pos-media-grab:10–23 (
load_grab_config)
Bespoke parsers (4+):
- bin/pos-network-download:32–37
load_secret— single-keygrep'^RPC_SECRET=' | head -1 | cut -d= -f2-; also duplicated inline at L153–154 - bin/pos-docker-compose:9–10 + layered strategy (
template < global compose.env < per-service .env, documented L31–48) — reads global config viaCONFIG_ENVand per-service envs - bin/pos-share-smb-server:97
reload_config— Samba-specific - bin/pos-media-ytsync:28–30
_YTSYNC_CFGviapos config ytsyncscope; 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 > defaultseverywhere the hand-rolled loaders are used (if [ -z "${!k:-}" ]before export; defaults applied later via${VAR:-default}). CLI-vs-config precedence is declaredCLI > environment > config filein 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, andload_system_envin common.sh:154). A Windows-edited.envparses 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 definesCONFIG_DIRat 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. SoCONFIG_DIR/XDG_CONFIG_HOMEoverrides work for some tools and are silently ignored by others. - Key-value writers duplicated —
cfg_write(config-ui.sh:323) andwrite_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 thePOS_CONFIGscope registry thatpos configconsumes, has secret masking/validation helpers, and is installed to/usr/local/binalongside the tools. - Add one generic loader there, e.g.
load_env_file <file>(env-wins export loop with quote + CRLF strip parsed consistently) and haveload_system_envdelegate to it. - Merge
entertainment-lib.shconfig_value/write_config_keyintocfg_value/cfg_write(keepconfig_valueas a thin alias for the entertainment tools, or migrate the 5 call sites). - Migration targets: the 9 hand-rolled loaders →
load_env_file(sourceconfig-ui.shin the 5 self-contained communication tools, replacing their guarded inline copies and CONFIG_DIR blocks);pos-network-download→cfg_value "$CONFIG_FILE" RPC_SECRET;pos-docker-composeconfig show→cfg_value/cfg_writefor the global config. - Unify CRLF-strip and the
CONFIG_DIRpath source across every loader during the migration.
Task 3 — Ranked change list (no implementation)
- Add
load_env_filetolib/config-ui.sh; makecommon.sh load_system_envdelegate; fix the CRLF split in the process. - Migrate the 9 hand-rolled loaders (list in 3.1) to it; make the 5 self-contained communication tools source
config-ui.shinstead of the inline CONFIG_DIR+load_config copies. - Fold
entertainment-lib.shread/write helpers intocfg_value/cfg_write(alias or migrate the 5 entertainment call sites). - Route
pos-network-downloadload_secretandpos-docker-composeglobal-config reads throughcfg_value. - Replace hardcoded
$HOME/.config/linux_post_install/...in the common.sh-sourcing tools with the sourcedCONFIG_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,403run eval "$cmd"._prompt_run_commandexecutes a command string extracted from AI output. Interactive path prompts on/dev/tty; the--trustpath (L385–388) auto-executes without confirmation. Deliberate feature, but any AI-output-derived command executed throughevalis a shell-injection-relevant surface — recommend keeping, but it deserves explicit review of whattrusted=1callers feed it. Classification: FACT (code), design concern. - H-002 (WARN) —
bin/pos-system-uninstall:333sed -i '/pos/d' "$HOME/.bash_completion". Deletes every line containing the substringposfrom a user-owned file, not just pos-managed lines (unlike thepos-ai-hookmarker check at L277). A line likecomplete -F _git checkoutis 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:320–322sed -ion~/.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 mentioningpos.bashis 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. Underset -euo pipefail, thegrep | while readand... | tail -1 | cutpipelines are only safe because of the|| true/2>/dev/nullguards and the non-final elements' exit codes. The per-lineprintf | sed | tail -1inside the read loop (L67/L143) is the perf hotspot from Task 1 AND a correctness risk: ifsedever exits non-zero for a given line underpipefail, the surroundingwhile readloop 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 -finbin/are consistently quoted (pos-ai-hf:644,698,835,989;pos-system-uninstallall lines;pos-docker-vbox:1115; app scripts). No unquoted/emptyrm -rf $VARfound.- Unquoted
[ $x … ]comparisons: none found inbin/(only"$var"forms). for x in $listsites (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.cdsites are guarded (pos-docker-vbox:1029,1040use(cd "$d" 2>/dev/null && pwd) || …;pos-docker-compose:221–260wrap in subshells withset -econtext).pos-share-smb-client:457sudo rm -f "$SMB_CREDS_DIR/$(basename "$where")"— properly quoted.find /etc/systemd/system/ -name '*linux_post_install*' -o -name 'pos-*'(pos-system-uninstall:311) —-obinds both predicates to the stated path; matches both patterns as intended. Not a bug.bin/poslogging tee pipes: INTERACTIVE_CMDS handling verified by lint rule and existing registrations — no new finding.
Task 4 — Ranked change list (no implementation)
- Restrict
pos-system-uninstall.bash_completion/.bashrcremoval to anchored, marker-based patterns (e.g. only lines the installer itself added, or apply thegrep -q 'Managed by pos…'-style marker check used for alias wrappers). - Review
bin/pos-ai_prompt_run_commandtrust boundaries: confirm everytrusted=1caller is user-flagged and document the eval surface (or re-run throughbash -cwith validation). - Convert lint L67/L143 per-line
printf|sed|tailto[[ =~ ]](also removes the pipefail-mid-loop abort risk). - Optional hardening: quote the
for x in $listsites 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, 15–20k 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|tailis at least 3 forks; actual exec cost depends on PATH lookup and filesystem state). - Whether apt packages / yt-dlp /
~/.ssh/authorized_keys/~/.config/rcloneare 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,137–157,162,169,221–222,235,251; deadlast_lineL39–42)scripts/check-sync.sh,scripts/gen-docs.sh— gates; convergence loop L244–252install.sh— phases,should_run(L100–117), copy targets (L136–222), ScaleTail (L237–243)preinstall.sh— apt PACKAGES (L28–43), yt-dlp (L65–68) — no uninstall counterpartpostinstall.sh— rclone/entertainment/system/notify/ai env templates, schedule.d, .bashrc, completion, systemdbin/pos-system-uninstall— tiers, lib list L62–64, user-unit gap, H-002/H-003, find L311bin/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 candidatesDOC/DEV.md:182–213— 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.