Confirmed live: `docker compose pull` failed with "yaml: line 44, column
29: mapping values are not allowed in this context" during the "Starting
Stack" phase of local-ai-setup.sh. Root cause: two healthcheck blocks
(ollama, chromadb) crammed interval/timeout/retries onto one
semicolon-separated line —
interval: 30s; timeout: 10s; retries: 5
— which isn't valid YAML; a scalar value can't contain a second `key:`
token like that unless quoted. Split each into three separate properly
indented keys, matching how every other multi-key block in this same file
is written.
Verified by generating the actual docker-compose.yml via the real heredoc
(same one docker-stack.md's variables would produce) and parsing the
result with PyYAML — line 44 is exactly the fixed `interval: 30s` line,
and the full file now parses as valid YAML.
Pre-existing bug in the vendored source, unrelated to this session's
earlier ai-stack.sh/local-ai-setup.sh changes (those only touched the
pull-models.sh heredoc and the cloud-provider prompt, both well before
this point in the install) — first surfaced now because this is the first
run in this session to actually reach the "Starting Stack" step rather
than stopping earlier.
None of local-ai-setup.sh's tier-selected models (CHAT_MODEL/CODE_MODEL/
EMBED_MODEL) can read an image — there was no way to get vision support out
of this stack at all before now. Added a numbered pick-list to the
generated pull-models.sh, right after the existing DeepSeek-R1 optional
pull, matching that same read -rp pattern:
1) moondream ~1.7 GB by Moondream AI — tiny, built for
CPU-only or weak/old-GPU hardware
2) llava:7b ~4.7 GB general-purpose vision
3) qwen2.5vl:7b ~6 GB stronger accuracy, more RAM/VRAM
4) llama3.2-vision:11b ~7.9 GB heaviest of the four
moondream is the recommended default — sized for exactly the "6 vCPU, 8GB
RAM, no GPU" case this was asked for, unlike the other three which assume
real GPU/RAM headroom.
Verified by actually running the heredoc that generates pull-models.sh
(with EMBED_MODEL/CHAT_MODEL/CODE_MODEL stood in) and syntax-checking the
resulting output script, not just the source — the outer heredoc is
unquoted so $-escaping mistakes wouldn't show up as a bash -n failure on
local-ai-setup.sh itself, only on what it generates.
services/ai-stack.md gets a matching "Vision models" section (sizes, the
manual pull command, and how to point an app's OPENAI_MODEL at one).
laptop_full_setup.sh's separate, non-interactive pull-models.sh generator
is untouched — it's not invoked anywhere in this repo's own install flow
(only local-ai-setup.sh is, from install_ai-stack()), so it's out of
scope here.
Confirmed live: a repo's local bare mirror clone got stuck on a stale
commit indefinitely even though every sync run reported [ok] — fetch
never failed, it just wasn't updating refs/heads/* the way this script
assumes. GitHub had the real current commit; the bare clone (and
therefore what got pushed to Gitea) stayed frozen on an old one.
`fetch --all --prune` trusts remote.origin.fetch as stored in the bare
repo's own git config rather than asserting what that mapping actually
is — if it drifted from the +refs/heads/*:refs/heads/* convention a
fresh `git clone --bare` sets up (for whatever reason — this specific
repo's local clone directory's history is unclear), fetch would
"successfully" land new commits somewhere this script never reads
(refs/remotes/origin/*) while refs/heads/* — the ref that actually gets
mirrored — never moves. Deleting and re-cloning the affected repo's
local directory fixed it immediately, consistent with a refspec-drift
theory, though the exact original cause wasn't pinned down further.
Now pins `+refs/heads/*:refs/heads/*` explicitly on every fetch instead
of relying on `--all` plus whatever's configured, in both sync
directions. Also stopped redirecting stderr to /dev/null on every
clone/fetch/push call — a real auth or network failure now shows up in
the log instead of a bare "Failed to X" with no reason, which is what
made this bug take three rounds of manual ls-remote/rev-parse forensics
across two machines to actually pin down.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YEQNc4NfBST1m9NtCZVYa8
((pull_count++)) evaluates to the PRE-increment value — 0 on the very
first successful pull/push — and under this script's `set -euo pipefail`,
an arithmetic command evaluating to 0 counts as a failing command and
kills the script immediately. Confirmed live: a real, fully successful
GitHub -> Gitea pull (visible in sync.log as "PULL ... OK") still made
the whole run exit non-zero and get reported as "Sync run failed", purely
because it was the first repo to sync (0 -> 1). Any subsequent repo in
the same run would have been fine, but most real installs only have a
handful of repos, so this could look like sync is just broken.
Switched all three counters (pull_count, push_count, fail_count) to
assignment form (`count=$((count + 1))`), which always exits 0 regardless
of the resulting value. page++ elsewhere in the file starts at 1, not 0,
so it isn't affected by this and was left as-is.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YEQNc4NfBST1m9NtCZVYa8
"Failed to reach GitHub API. Check GITHUB_TOKEN." (and the equivalent
Gitea message) pointed at the token every time, even when the real cause
was something else entirely — confirmed live twice in one debugging
session: once a GitHub-side 503 outage, once a Gitea account locked
behind a must-change-password 403. Both times the fix was to run the
same curl by hand to see the actual status/response.
Fold that same probe into the script itself: on failure, re-request with
-i and print the HTTP status and response body directly, so the failure
mode (bad token vs. remote outage vs. account lock vs. network/DNS) is
visible immediately instead of requiring a manual curl round-trip.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YEQNc4NfBST1m9NtCZVYa8
Matches the existing vendor/easy-asterisk convention (used by
services/asterisk.sh) instead of two one-off top-level directories that
cluttered the repo root and didn't look like anything else next to
setup.sh, lib/, services/, extras/. Only the two services' own SRC_DIR
path resolution and header comments needed updating — nothing else in
the repo referenced the old ./ai-stack / ./paintplus paths.
Also documents vendor/ in README.md's Layout section.