···2323ac-os flash+upload # Build + flash + upload
2424```
25252626+`ac-os` is the preferred path for real devices. It layers user-local credentials
2727+and repo context into the initramfs on top of the base image:
2828+2929+- Claude OAuth state, when present locally
3030+- GitHub PAT, when `gh auth token` succeeds
3131+- Tangled SSH identity, when `ssh -G knot.aesthetic.computer` resolves a local key
3232+2633### Manual steps
27342835```bash
···4956 - Contains: `DO_SPACES_KEY`, `DO_SPACES_SECRET`
5057- **GPG private key**: `.tmp-key-jeffrey-private.asc` (in repo root, gitignored)
5158- **Handle colors API**: `https://aesthetic.computer/.netlify/functions/handle-colors` (public, no auth)
5959+6060+## Device Repo Access
6161+6262+- The on-device working tree lives at `/mnt/ac-repo`.
6363+- On WiFi connect, AC Native clones or pulls from the public GitHub HTTPS mirror:
6464+ - `https://github.com/whistlegraph/aesthetic-computer.git`
6565+- If a Tangled SSH key was baked at build time, the repo is also configured so:
6666+ - `origin` fetches from GitHub
6767+ - `origin` push mirrors to `git@knot.aesthetic.computer:aesthetic.computer/core`
6868+ - `origin` also pushes to the GitHub mirror
6969+ - `tangled` points directly at the knot remote
7070+- If no Tangled key is baked, the repo stays GitHub-only for pushes.
52715372## Update Signal Expectations
5473
+35
fedac/native/ac-os
···152152 sudo chmod 600 "${INITRAMFS_ROOT}/github-pat"
153153 log " github pat: baked (${#GH_PAT} chars)"
154154 fi
155155+ # Bake Tangled SSH identity if available so on-device git pushes can
156156+ # reach knot.aesthetic.computer as well as the GitHub mirror.
157157+ local TANGLED_KEY
158158+ TANGLED_KEY="$(ssh -G knot.aesthetic.computer 2>/dev/null | awk '/^identityfile / {print $2; exit}')"
159159+ case "${TANGLED_KEY}" in
160160+ "~/"*) TANGLED_KEY="${HOME}/${TANGLED_KEY#~/}" ;;
161161+ esac
162162+ if [ -z "${TANGLED_KEY}" ] && [ -f "${HOME}/.ssh/tangled" ]; then
163163+ TANGLED_KEY="${HOME}/.ssh/tangled"
164164+ fi
165165+ if [ -n "${TANGLED_KEY}" ] && [ -f "${TANGLED_KEY}" ]; then
166166+ sudo cp "${TANGLED_KEY}" "${INITRAMFS_ROOT}/tangled-key"
167167+ sudo chmod 600 "${INITRAMFS_ROOT}/tangled-key"
168168+ if [ -f "${HOME}/.ssh/known_hosts" ]; then
169169+ sudo cp "${HOME}/.ssh/known_hosts" "${INITRAMFS_ROOT}/tangled-known-hosts"
170170+ sudo chmod 600 "${INITRAMFS_ROOT}/tangled-known-hosts"
171171+ elif command -v ssh-keyscan >/dev/null 2>&1; then
172172+ ssh-keyscan -H knot.aesthetic.computer 2>/dev/null | sudo tee "${INITRAMFS_ROOT}/tangled-known-hosts" >/dev/null || true
173173+ sudo chmod 600 "${INITRAMFS_ROOT}/tangled-known-hosts" 2>/dev/null || true
174174+ fi
175175+ sudo tee "${INITRAMFS_ROOT}/tangled-ssh-config" > /dev/null << 'SSHCFG'
176176+Host knot.aesthetic.computer
177177+ HostName knot.aesthetic.computer
178178+ User git
179179+ IdentityFile ~/.ssh/tangled
180180+ IdentitiesOnly yes
181181+ BatchMode yes
182182+ StrictHostKeyChecking accept-new
183183+ UserKnownHostsFile ~/.ssh/known_hosts
184184+SSHCFG
185185+ sudo chmod 600 "${INITRAMFS_ROOT}/tangled-ssh-config"
186186+ log " tangled ssh: baked (${TANGLED_KEY})"
187187+ else
188188+ log " tangled ssh: not found (skipping knot push setup)"
189189+ fi
155190 # Bake CLAUDE.md for device context (init copies to /tmp/ac/CLAUDE.md)
156191 if [ -f "${SCRIPT_DIR}/device-claude.md" ]; then
157192 sudo cp "${SCRIPT_DIR}/device-claude.md" "${INITRAMFS_ROOT}/device-claude.md"
+14-6
fedac/native/device-claude.md
···2727### File Operations
2828- Read/write files in /tmp (lost on reboot)
2929- Read/write files in /mnt (persistent, VFAT — no symlinks, no permissions)
3030-- The main AC repo is NOT on this device — use git clone if needed
3030+- `/mnt/ac-repo` is the preferred working tree when WiFi has already done the background clone
3131+- If `/mnt/ac-repo` does not exist yet, clone manually or work in `/tmp/ac` as a scratch fallback
31323233### Network
3334- curl is available for HTTP requests
3434-- git is available (GitHub PAT is pre-configured via GH_TOKEN)
3535+- git is available
3636+- `ssh` is available for Tangled pushes when the image was built via `ac-os` with a local Tangled key
3537- DNS works (8.8.8.8 / 1.1.1.1 fallback)
36383739### Development
···5254| Path | Purpose |
5355|------|---------|
5456| /mnt/config.json | User identity + auth tokens |
5757+| /mnt/ac-repo | Persistent shallow clone of `aesthetic-computer` |
5558| /mnt/ac-native.log | System log (persistent) |
5659| /claude-token | Claude OAuth token (initramfs) |
5760| /github-pat | GitHub PAT (initramfs) |
5861| /tmp/.claude/ | Claude Code config directory |
6262+| /tmp/.ssh/ | Tangled SSH key/config restored on boot, when baked |
5963| /ac-native | Main system binary |
6064| /piece.mjs | Default JS piece |
6165| /bin/claude | Claude Code binary |
6666+| /bin/ssh | OpenSSH client for Tangled git push |
62676368## Architecture
6469···127132128133### Git operations
129134```sh
130130-cd /tmp
131131-git clone https://github.com/whistlegraph/aesthetic-computer.git
132132-cd aesthetic-computer
133133-# GH_TOKEN is pre-set — push works
135135+cd /mnt/ac-repo 2>/dev/null || {
136136+ git clone https://github.com/whistlegraph/aesthetic-computer.git /mnt/ac-repo
137137+ cd /mnt/ac-repo
138138+}
139139+git remote -v
140140+# fetch uses the GitHub mirror
141141+# if /tmp/.ssh/tangled exists, origin push mirrors to knot + GitHub
134142```
135143136144### OTA update
···636636 warn "dropbear not found — SSH remote access not available"
637637fi
638638639639-# ── Claude Code utilities (git, ripgrep, jq) ──
639639+# ── Claude Code utilities (git, ripgrep, jq, ssh) ──
640640# These tools make Claude Code significantly more capable on the device.
641641-for util in git rg jq; do
641641+for util in git rg jq ssh; do
642642 UTIL_PATH="$(command -v "$util" 2>/dev/null || true)"
643643 if [ -n "$UTIL_PATH" ] && [ -f "$UTIL_PATH" ]; then
644644- cp "$UTIL_PATH" "${INITRAMFS_DIR}/bin/"
644644+ cp "$UTIL_PATH" "${INITRAMFS_DIR}/bin/${util}"
645645 for lib in $(ldd "$UTIL_PATH" 2>/dev/null | grep -oP '/\S+'); do
646646 [ -f "$lib" ] && cp -n "$lib" "${INITRAMFS_DIR}/lib64/" 2>/dev/null || true
647647 done
+3-3
fedac/native/scripts/build-and-flash.sh
···537537 warn "dropbear not found — SSH remote access not available"
538538fi
539539540540-# ── Claude Code utilities (git, ripgrep, jq) ──
540540+# ── Claude Code utilities (git, ripgrep, jq, ssh) ──
541541# These tools make Claude Code significantly more capable on the device.
542542-for util in git rg jq; do
542542+for util in git rg jq ssh; do
543543 UTIL_PATH="$(command -v "$util" 2>/dev/null || true)"
544544 if [ -n "$UTIL_PATH" ] && [ -f "$UTIL_PATH" ]; then
545545- cp "$UTIL_PATH" "${INITRAMFS_DIR}/bin/"
545545+ cp "$UTIL_PATH" "${INITRAMFS_DIR}/bin/${util}"
546546 for lib in $(ldd "$UTIL_PATH" 2>/dev/null | grep -oP '/\S+'); do
547547 [ -f "$lib" ] && cp -nL "$lib" "${INITRAMFS_DIR}/lib64/" 2>/dev/null || true
548548 done
+31-9
fedac/native/src/ac-native.c
···39603960 }
39613961 // Clone (or pull) the aesthetic-computer repo to
39623962 // /mnt/ac-repo so Claude Code has a real project to
39633963- // work in. Runs in the background so boot isn't
39643964- // delayed. If /github-pat is present, git picks it up
39653965- // via the git-credential-ac helper. We use a shallow
39633963+ // work in. Fetch stays on the GitHub HTTPS mirror for
39643964+ // simple read access; if a Tangled SSH key was baked,
39653965+ // the repo also gets origin pushurls for knot + GitHub
39663966+ // and a dedicated `tangled` remote. Runs in the
39673967+ // background so boot isn't delayed. We use a shallow
39663968 // clone (--depth=50) to keep the size manageable
39673969 // (~200 MB) while still giving Claude enough history
39683970 // for basic blame/log work.
···39713973 // `git pull` runs instead (bounded by 30s timeout).
39723974 // Logs go to /tmp/ac-repo-clone.log.
39733975 {
39743974- char clone_cmd[1024];
39763976+ char clone_cmd[3072];
39753977 snprintf(clone_cmd, sizeof(clone_cmd),
39763976- "( if [ -d /mnt/ac-repo/.git ]; then "
39783978+ "( REPO=/mnt/ac-repo; "
39793979+ " FETCH_URL='https://github.com/whistlegraph/aesthetic-computer.git'; "
39803980+ " TANGLED_URL='git@knot.aesthetic.computer:aesthetic.computer/core'; "
39813981+ " if [ -d \"$REPO/.git\" ]; then "
39773982 " echo '[ac-repo] pulling latest' && "
39783978- " cd /mnt/ac-repo && timeout 30 git pull --ff-only 2>&1; "
39833983+ " cd \"$REPO\" && timeout 30 git pull --ff-only 2>&1; "
39793984 " else "
39803985 " echo '[ac-repo] cloning (shallow)' && "
39813981- " timeout 300 git clone --depth=50 --branch=main "
39823982- " https://github.com/whistlegraph/aesthetic-computer.git "
39833983- " /mnt/ac-repo 2>&1; "
39863986+ " timeout 300 git clone --depth=50 --branch=main \"$FETCH_URL\" \"$REPO\" 2>&1; "
39873987+ " fi; "
39883988+ " if [ -d \"$REPO/.git\" ]; then "
39893989+ " cd \"$REPO\"; "
39903990+ " git remote set-url origin \"$FETCH_URL\"; "
39913991+ " git config --unset-all remote.origin.pushurl 2>/dev/null || true; "
39923992+ " if [ -f /tmp/.ssh/tangled ]; then "
39933993+ " git config --add remote.origin.pushurl \"$TANGLED_URL\"; "
39943994+ " git config --add remote.origin.pushurl \"$FETCH_URL\"; "
39953995+ " if git remote | grep -qx tangled; then "
39963996+ " git remote set-url tangled \"$TANGLED_URL\"; "
39973997+ " else "
39983998+ " git remote add tangled \"$TANGLED_URL\"; "
39993999+ " fi; "
40004000+ " echo '[ac-repo] origin pushurl: tangled + github'; "
40014001+ " else "
40024002+ " git config --add remote.origin.pushurl \"$FETCH_URL\"; "
40034003+ " git remote remove tangled 2>/dev/null || true; "
40044004+ " echo '[ac-repo] origin pushurl: github only (no tangled key)'; "
40054005+ " fi; "
39844006 " fi "
39854007 ") >> /tmp/ac-repo-clone.log 2>&1 &");
39864008 system(clone_cmd);
+13-6
fedac/native/src/pty.c
···561561 setenv("SHELL", "/bin/bash", 1);
562562 setenv("USER", "root", 1);
563563 setenv("LOGNAME", "root", 1);
564564+ setenv("GIT_TERMINAL_PROMPT", "0", 1);
564565 setenv("CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC", "1", 1);
565566 // SSL certs for API connections
566567 setenv("SSL_CERT_FILE", "/etc/pki/tls/certs/ca-bundle.crt", 0);
567568 setenv("SSL_CERT_DIR", "/etc/ssl/certs", 0);
568569 setenv("CURL_CA_BUNDLE", "/etc/pki/tls/certs/ca-bundle.crt", 0);
569570 setenv("NODE_EXTRA_CA_CERTS", "/etc/pki/tls/certs/ca-bundle.crt", 0);
571571+ if (access("/bin/ssh", X_OK) == 0 && access("/tmp/.ssh/config", R_OK) == 0) {
572572+ setenv("GIT_SSH_COMMAND", "/bin/ssh -F /tmp/.ssh/config -o BatchMode=yes", 1);
573573+ setenv("GIT_SSH_VARIANT", "ssh", 1);
574574+ }
570575 // GitHub PAT (for git operations)
571576 {
572577 FILE *gf = fopen("/github-pat", "r");
···625630626631 // Working directory strategy:
627632 // 1. /mnt/ac-repo (persistent shallow clone of aesthetic-computer,
628628- // cloned on first boot or manually by the user) — PREFERRED so
629629- // Claude can actually `git commit` real project files
633633+ // auto-cloned on WiFi connect or manually by the user) — PREFERRED
634634+ // so Claude can actually `git commit` real project files
630635 // 2. /tmp/ac (tmpfs, persists per-session only) — FALLBACK for
631636 // when there's no repo yet. Claude can still edit scratch
632637 // files here. A placeholder git init makes it look like a
···665670 "- **No package manager** — binaries are baked into initramfs\n\n"
666671 "## Filesystem\n"
667672 "- `/mnt` — USB boot drive (FAT32, has config.json and logs)\n"
668668- "- `/mnt/ac-repo` — full aesthetic-computer git repo (persistent)\n"
673673+ "- `/mnt/ac-repo` — persistent shallow repo clone (GitHub fetch mirror)\n"
669674 "- `/mnt/tapes` — MP4 tapes recorded via PrintScreen\n"
670675 "- `/tmp` — tmpfs (lost on reboot)\n"
671671- "- `/bin` — busybox + baked binaries (bash, git, rg, jq)\n\n"
676676+ "- `/tmp/.ssh` — Tangled SSH key/config, when baked via `ac-os`\n"
677677+ "- `/bin` — busybox + baked binaries (bash, git, rg, jq, ssh)\n\n"
672678 "## Networking\n"
673679 "- WiFi auto-connects to saved networks\n"
674680 "- `curl` is available for HTTP requests\n"
675675- "- GitHub PAT is pre-configured for git push/pull\n\n"
681681+ "- `/mnt/ac-repo` fetches from the GitHub mirror\n"
682682+ "- If `/tmp/.ssh/tangled` exists, `git push origin` mirrors to knot + GitHub\n\n"
676683 "## What you can do\n"
677684 "- Write and run shell scripts (bash, not busybox)\n"
678685 "- Use curl to interact with APIs\n"
679686 "- Edit files in /mnt/ac-repo and `git commit` them\n"
680680- "- Use git (GitHub PAT is wired up for whistlegraph)\n"
687687+ "- Use git (GitHub PAT is wired up; Tangled push works when the SSH key is baked)\n"
681688 "- Read system logs at /mnt/ac-native.log\n",
682689 handle, handle);
683690 fclose(cm);
···11+# 2026-04-11 — notepat OTA build and USB flash report
22+33+Session focus: ship the async DJ USB mount check and Notepat live-percussion
44+work as a fresh OTA build, then get that exact OTA image onto the USB with
55+Jeffrey's inscribed credentials and preserved Wi-Fi seeds.
66+77+Final release published in oven:
88+99+- Release: `dynamic-milksnake`
1010+- Commit: `848d3231a`
1111+- Build stamp: `848d3231a-2026-04-11T21:30`
1212+- Oven job: `fff0bd10-0`
1313+1414+---
1515+1616+## 1. What shipped
1717+1818+This build includes the native change that makes the DJ music-USB probe
1919+non-blocking:
2020+2121+- `system.mountMusic()` now schedules its probe off-thread instead of blocking
2222+ the main JS/render path
2323+- JS callers now consume cached mount state via
2424+ `system.mountMusicMounted` / `system.mountMusicPending`
2525+- This closes the periodic crawl caused by synchronous USB polling during play
2626+2727+Work was committed and pushed as:
2828+2929+- Commit: `848d3231a5c5126cf2df1f731fd0d293d7a62350`
3030+- Message: `native: make dj usb mount check async`
3131+3232+---
3333+3434+## 2. OTA build result
3535+3636+Manual oven trigger was required because the native poller was not picking the
3737+build up automatically at the time of the session.
3838+3939+Published artifacts included:
4040+4141+- `os/releases.json`
4242+- `os/native-notepat-latest.vmlinuz`
4343+- `os/native-notepat-latest.vmlinuz-slim`
4444+- `os/native-notepat-latest.initramfs.cpio.gz`
4545+4646+OTA publication succeeded and `ac-os pull` was able to download and verify the
4747+new release payload.
4848+4949+---
5050+5151+## 3. Standard flash-path failure
5252+5353+The normal `./fedac/native/ac-os pull` flash path did not complete on this host.
5454+5555+Observed failure:
5656+5757+- partitioning completed, but kernel partition-table reread stayed busy
5858+- `mkfs.vfat` on `/dev/sda1` retried repeatedly and failed with
5959+ `Device or resource busy`
6060+- direct host-side reread attempts (`blockdev --rereadpt`, `partx -u`) were
6161+ also blocked
6262+6363+This meant the OTA payload was downloaded correctly, but the standard helper
6464+could not safely move from partitioning into filesystem creation on the live
6565+USB device.
6666+6767+---
6868+6969+## 4. Raw-image fallback
7070+7171+To bypass the host's live partition-reread problem, the USB was flashed through
7272+a raw-disk fallback:
7373+7474+1. Download OTA artifacts with `ac-os pull`
7575+2. Stage `config.json`, `wifi_creds.json`, kernel, initramfs, and EFI payloads
7676+3. Build full `ACBOOT`, `ACEFI`, and `AC-MAC` partition images offline
7777+4. Assemble a complete GPT raw USB image
7878+5. Write that raw image directly to `/dev/sda`
7979+6. Read files back from the physical device by partition offset to verify the
8080+ result
8181+8282+The bulk raw write completed successfully:
8383+8484+- bytes written: `15518924800`
8585+- elapsed: `742.457 s`
8686+- sustained rate at completion: `20.9 MB/s`
8787+8888+---
8989+9090+## 5. Device readback verification
9191+9292+Readback from the physical USB confirmed:
9393+9494+- `config.json` present on the device
9595+- `wifi_creds.json` present on the device
9696+- handle: `jeffrey`
9797+- AC token: present
9898+- Claude token: present
9999+- GitHub PAT: present
100100+- `claudeCreds`: present
101101+- `claudeState`: present
102102+- Wi-Fi networks preserved: `4`
103103+104104+Readback SSIDs:
105105+106106+- `aesthetic.computer`
107107+- `ATT2AWTpcr`
108108+- `GettyLink`
109109+- `Tondo_Guest`
110110+111111+Partition content checks:
112112+113113+- `ACBOOT` contained `EFI/BOOT/BOOTX64.EFI` and `BOOTIA32.EFI`
114114+- `ACEFI` contained `EFI/BOOT/BOOTX64.EFI`, `LOADER.EFI`, and `KERNEL.EFI`
115115+116116+---
117117+118118+## 6. Caveat
119119+120120+Immediately after the raw write, host-side `lsblk` still showed `/dev/sda1-3`
121121+without refreshed filesystem metadata. That appears to be the same host/kernel
122122+partition-refresh limitation that broke the standard flash path.
123123+124124+Important distinction:
125125+126126+- host metadata refresh was stale
127127+- direct readback from the physical USB contents succeeded
128128+129129+So the USB image itself verified cleanly even though the host did not fully
130130+refresh its partition view until replug.
131131+132132+---
133133+134134+## 7. Outcome
135135+136136+The `dynamic-milksnake` OTA build for commit `848d3231a` was successfully
137137+published and successfully written to the USB, with credentials and Wi-Fi data
138138+inscribed and verified from the device itself.