2026-09-12 21:47 · 2 deliverable(s) · auto-closeout
Read-only audit. Nothing sent, nothing patched. Scope: M2 local only (~/bin, ~/.aios/state,
~/AI_OS/ops/scripts, ~/AI_OS/ops/launchagents + ~/Library/LaunchAgents, ~/AI_OS/skills/*/scripts,
~/AI_OS/agents). .bak-* files excluded from counts (dead weight, not live code paths).
~/bin/tg — the canonical helper. Takes --class <name>, defaults to manual. Checks ${AIOS_TG_MUTE:-$HOME/.aios/state/tg-mute.conf} with grep -qxF "$TG_CLASS" "$mute"; a match
suppresses the send (logs to ~/.aios/state/tg-muted.log) and exits 0 before touching the
network. Sanitizes to plain text via tg-sanitize.py, never sets parse_mode.
~/AI_OS/ops/scripts/send-tg.sh — a second, independent helper (relay-first, direct-API fallback) used by agent scripts. It re-implements the same class/mute gate (tg_class_gate(),
lines 20-27) before either send path runs. Correct, but it is a second copy of the logic, not a
call into tg itself.
~/.aios/state/tg-mute.conf:watcher heartbeat progress
Both gates above grep -qxF this file. The mute mechanism itself works for any caller that
(a) goes through tg or send-tg.sh, and (b) passes the correct --class.
~/AI_OS/ops/scripts/tg-publish.sh — the packet channel. Gates: HTML-only for review artifacts (rejects .md/.txt), SHA-256 dedup (24h), title validation, mobile parse-before-paint budget,
live-link probe, and (new 2026-09-12) HTML→PDF render so nothing unopenable ships. This is the
correct path for anything that should "open on the phone" and it enforces that well.
The conf line-matching and both gate implementations are correct and verified working. But the
mute conf only protects traffic that **voluntarily routes through tg or send-tg.sh AND self-labels
correctly**. Two independent leaks bypass it entirely, both currently live:
1. Direct curl to api.telegram.org — 13 scripts do this (list below). None of them can be
muted by editing tg-mute.conf; the file is never read.
2. Unclassed calls through tg/send-tg.sh — TG_CLASS defaults to manual, which is never
muted. a0-inbox-watcher.sh and cleif-a6-orchestrator.sh call tg/send-tg.sh with no
--class for content that is functionally progress/status chatter, so it always reaches the
phone regardless of the mute list.
3. telegram-alert-lib.sh — a third, fully independent send path (own cooldown/repeat-cap
guard, aios_telegram_send_guarded()) with no concept of classes or the mute conf at all.
Sourced by 9 scripts.
So: editing tg-mute.conf today changes behavior only for nano-dispatch-watch.sh's two lines
(classed progress/alert — the one fully-correct example in the fleet) and any future caller
that copies that pattern. It does nothing for the other ~20 sender call-sites.
tg / send-tg.sh (mute-conf-aware)| File:line | Class | Scheduled? |
|---|---|---|
ops/scripts/nano-dispatch-watch.sh:58 | progress (correctly muted) | LaunchAgent com.aios.nano-dispatch-watch, every 1200s |
ops/scripts/nano-dispatch-watch.sh:61 | alert (correctly always-on) | same |
ops/scripts/a0-inbox-watcher.sh:84 (bash "$SEND" --msg "$summary", no --class) | unclassed → manual (always sends) | LaunchAgent com.aios.a0-inbox-watcher, every 300s |
ops/scripts/cleif-a6-orchestrator.sh:35,48,56,66,165,179,181 (tg "...", no --class) | unclassed → manual | not in any LaunchAgent/cron/ps right now — interactive-run only |
curl/requests.post to api.telegram.org (mute conf NEVER consulted)| File:line | Payload note | Scheduled? |
|---|---|---|
ops/scripts/news-digest.py:593 | parse_mode: HTML (violates plaintext rule) and message body ends Full list: comms/outputs/intel/<date>-digest.md — a bare .md path | LaunchAgent com.aios.news-digest, 09:03 + 21:03 daily |
ops/scripts/claude-inbox-executor.sh:203 (telegram_notify()) | "Starting task: …" / "TIMEOUT" / "FAILED" / "Done: …" — the Starting-task line is pure progress chatter, sent every task cycle, unmuteable | LaunchAgent com.aios.claude-inbox-executor, every 300s |
ops/scripts/email_triage/email_triage.py:409 | parse_mode: HTML (violates plaintext rule); "Email Triage — N processed" digest | not scheduled/running currently (no LaunchAgent, no ps hit) |
ops/scripts/telegram-alert-lib.sh:219 (aios_telegram_send_guarded) | shared lib, own cooldown/cap, no class awareness | not itself scheduled, but sourced by 9 other scripts (below) |
ops/scripts/watchkeeper.sh:529 | — | not scheduled/running currently |
ops/scripts/watchkeeper-dispatch.sh:135 | — | not scheduled/running currently |
ops/scripts/claude-session-close.sh:150 | "[Claude Session End] …" | not wired to the live Stop hook (closeout-stop-hook.sh does not call it) — orphaned unless run manually |
ops/scripts/fleet-status.sh:102 | markdown parse_mode=Markdown | not scheduled/running currently |
ops/scripts/screen-preview.sh:32,41 | — | not scheduled/running currently |
ops/scripts/dual-host-sync.sh:95 | — | not scheduled/running currently |
ops/scripts/telegram-autopilot-control-daemon.sh:598 | ack replies to inbound commands | not scheduled/running currently |
ops/scripts/cleif-a6-orchestrator.sh:29,117 | test/heredoc example lines, same file also uses tg elsewhere | not scheduled |
ops/scripts/sally-relay/sally_relay.py:146 | separate bot (Sally), own token | not scheduled/running currently |
telegram-alert-lib.sh (inherits the no-class, direct-curl behavior above)ops/bin/notify, ops/scripts/mini-claude-tab-autopilot.sh, ops/scripts/heartbeat-notify.sh,
ops/scripts/claude-capacity-watcher.sh, ops/scripts/license-check.sh,
ops/scripts/auto-model-guard.sh, ops/scripts/mission-control.sh — none currently in a
LaunchAgent plist or running process on M2 (checked launchctl list + ps aux), so dormant unless
run by hand. heartbeat-notify.sh and claude-capacity-watcher.sh are exactly the name-pattern
Sean flagged ("heartbeat", "watcher") and would bypass the mute conf entirely the moment either is
re-enabled.
.md/.txt path or "saved to …" as the payload itselfnews-digest.py — literal .md path in the message body (see above).a0-inbox-watcher.sh:75 — summary="$summary"$'\n'"• ${subj:-$f}": when an inbox note has no subject: frontmatter line, the message falls back to the bare .md filename.
scheduled senders embed one inside an otherwise-fine message.
tg via direct curl (ignore mute conf) — consolidated13 files: news-digest.py, claude-inbox-executor.sh, email_triage.py, telegram-alert-lib.sh,
watchkeeper.sh, watchkeeper-dispatch.sh, claude-session-close.sh, fleet-status.sh,
screen-preview.sh, dual-host-sync.sh, telegram-autopilot-control-daemon.sh,
cleif-a6-orchestrator.sh (partial), sally_relay.py.
nano-dispatch-watch.sh — scheduled (1200s) — correctly classed, not an offender.news-digest.py — scheduled (2x/day) — offender (HTML + path payload).heartbeat-notify.sh, claude-capacity-watcher.sh — named matches, not currently scheduled.lane-staleness-digest.sh — exists, no Telegram send found in it (grep clean) and not scheduled;false lead, ignore.
a0-steer-listener — LaunchAgent present and loaded (pid 16260 in launchctl list) but notelegram string match; out of scope for this audit.
.bak-*): ~23 (3 via tg/send-tg.sh, 13 via direct curl, 7 more via sourcing telegram-alert-lib.sh)
manual): ~20 (13 direct-curl + 7 lib-based; a0-inbox-watcher.sh and cleif-a6-orchestrator.sh also
effectively unclassed since they never pass --class)
nano-dispatch-watch.sh).md/.txt path or bare "saved to" payload: 2 live senders (news-digest.py, a0-inbox-watcher.sh fallback case)
launchctl list confirmed): 4 senders (a0-inbox-watcher.sh 300s, claude-inbox-executor.sh 300s, nano-dispatch-watch.sh 1200s,
news-digest.py 2x/day)
1. ops/scripts/news-digest.py:593 — scheduled 2x/day, parse_mode=HTML (plaintext-rule
violation) AND ships a bare .md path as part of the payload. Worst of the set: hits three
separate rules at once (HTML, unclassed/uncontrolled, path-as-payload).
2. ops/scripts/claude-inbox-executor.sh:203 (telegram_notify) — scheduled every 300s, direct
curl, zero class awareness; "Starting task" sends are pure progress chatter with no way to mute
without editing the script.
3. ops/scripts/telegram-alert-lib.sh:219 — shared library with its own independent guard/cooldown
system, completely outside the class/mute contract; 9 scripts source it, 2 of which
(heartbeat-notify.sh, claude-capacity-watcher.sh) are named exactly what Sean asked to stop
seeing, dormant only because nothing currently schedules them.
4. ops/scripts/a0-inbox-watcher.sh:84 — scheduled every 300s via send-tg.sh --msg with no
--class; always-sends by default, and falls back to a bare .md filename in the body when a
note lacks frontmatter (line 75).
5. ops/scripts/email_triage/email_triage.py:409 — direct curl, parse_mode=HTML; not currently
scheduled, but it's A8's own triage output path and the next thing wired to a cron/LaunchAgent
would ship HTML-formatted text straight past tg's sanitizer.
| File | One-line change |
|---|---|
ops/scripts/news-digest.py | Drop "parse_mode": "HTML" from the _telegram_send payload; route the send through ~/bin/tg --class digest "<message>" instead of a raw requests/urllib POST, and replace the Full list: <path> line with a tg-publish.sh-delivered link (or drop it) so no .md path ships. |
ops/scripts/claude-inbox-executor.sh | Replace the local telegram_notify() curl with "$HOME/bin/tg" --class progress "$msg" for the Starting-task line and --class done/--class alert for Done/Failed/Timeout, so the mute conf actually governs it. |
ops/scripts/telegram-alert-lib.sh | Add a TG_CLASS param to aios_telegram_send_guarded() and check tg-mute.conf before the curl call (same one-liner gate tg already uses), so every one of its 9 callers inherits muting for free. |
ops/scripts/a0-inbox-watcher.sh | Call bash "$SEND" --class progress --msg "$summary" (line 84) instead of unclassed --msg, and change line 75's fallback from ${subj:-$f} to ${subj:-"(untitled note)"} so a bare filename never ships. |
ops/scripts/email_triage/email_triage.py | Drop "parse_mode": "HTML" and swap the raw requests.post for a subprocess.run(["$HOME/bin/tg", "--class", "digest", text]) call so the sanitizer and mute conf both apply. |
ops/scripts/cleif-a6-orchestrator.sh | Add --class progress to the intermediate status lines (35,48,56,66,165) and keep the final complete/exit lines (179,181) as --class done/--class alert. |
ops/scripts/watchkeeper.sh, watchkeeper-dispatch.sh, fleet-status.sh, screen-preview.sh, dual-host-sync.sh, telegram-autopilot-control-daemon.sh | Replace each direct curl .../sendMessage with a call to ~/bin/tg [--class <appropriate>] so none of these re-enabled-later scripts can silently bypass the mute conf again. |
ops/scripts/claude-session-close.sh | Either delete (it's orphaned — the live Stop hook closeout-stop-hook.sh doesn't call it) or, if intentionally kept as a manual tool, route its send through ~/bin/tg --class done. |
claude-session-close.sh looks orphaned (not called by the live Stop hook). Confirm it's deadbefore anyone deletes it — it may still be invoked by a device other than M2.
sally_relay.py is a second bot (Sally) with its own token, entirely outside the tg/mute-confsystem by design. Worth deciding whether Sally should ever inherit class discipline, or stays
separate on purpose.
Resume from here. Everything below is verified with a command unless marked PENDING.
1. skills/computer-use/ built and live on M2 + Nano (23 files md5-identical). Router v1, predict.mjs, ICM v2 all resolve it; icm/blocks-lint.py clean on 101 blocks. Memory reference_computer_use_skill. Summary: https://claude.ai/code/artifact/7802b182-6fd8-4e3e-bd0f-b43431a58d93
2. Handoff audit (Sean's ask). Five cards were handed M2→Nano 23:23–23:44 PT:
| slug | state at audit | now |
|---|---|---|
| overnight-council-to-nano | DONE 23:46 (3 receivers, tg done sent) | HTML summary sent (artifact 797c8fc3) |
| stray-rsync-dir-audit | DONE 23:45 (tg done sent) | HTML summary sent (artifact 6bc1b07d) |
| scanbot-L1-L4 | partially started (a first receiver had launched Codex detached at 23:52; the pool launch itself exited 2) | RELAUNCHED 23:55 → DONE 00:20 PT. Codex built core+L1+L2 then hit its usage window (reopens 03:53 PT); Claude builders finished L3/L4/CLI/tests. Independently verified by A3: 212 tests OK, compileall clean, 71 files in build/, old ops/scripts/scanbot/ untouched. Two FINAL blocks in the packet — "FINAL v2" supersedes. Cross-model review of L3/L4 NOT done. HTML summary relayed as artifact (see TG #3) |
| telegram-ingress-proof | never started | RELAUNCHED 23:55 → DONE 00:00 PT. Bridge PASS (26 s synthetic turn); phone ingress UNFED (no poller loaded). Result in comms/handoffs/20260912T062747Z__… (not the inbox card). HTML summary relayed as artifact 8117ae06 |
| video-cutroom-v06 | never started | RELAUNCHED 23:55 → DONE 23:59 PT. Lane dirs + READY.md; NOT render-ready; 3 installs need GO; npm name is hyperframes. HTML summary relayed as artifact 0cce8335 |
Receivers deliver via tg-publish = HTML attachment (unopenable on iOS, B65) — coordinator relays each as an Artifact link; 2 TG messages sent so far (links 1-3, then 4-5), 1 reserved for scanbot.
Root cause of the three stalls: the per-handoff runner copy shipped by handoff.sh (stdin < resume-runner.sh → /tmp/aios-resume-runner-<TS>-<slug>.sh) arrived syntax-broken at the --disallowedTools line; nano-run reported __NANO_RUN_DONE__ exit=2. The canonical runner on Nano is intact (bash -n OK) — relaunched with it via pool.sh run.
3. HTML-summary rule encoded (Sean 09-12: every finished task sends an HTML summary to Telegram, whoever finishes it): added to ops/scripts/handoff/handoff.sh card template, resume-runner.sh receiver tail, and the three staged cards on Nano. Delivery form = hosted link (Artifact), never an HTML attachment (B65); tg-publish --no-send ledgers each file.
4. One tg --class done sent with the three links + relaunch status.
| summary | link |
|---|---|
| overnight-council-to-nano | https://claude.ai/code/artifact/797c8fc3-9ba9-46be-b637-2dd9806749d8 |
| stray-rsync-dir-audit | https://claude.ai/code/artifact/6bc1b07d-6214-4604-bd94-37419e39f688 |
| computer-use-skill (A3) | https://claude.ai/code/artifact/7802b182-6fd8-4e3e-bd0f-b43431a58d93 |
| video-cutroom-v06 | https://claude.ai/code/artifact/0cce8335-b416-4978-a80f-e2b1b71fe3c2 |
| telegram-ingress-proof | https://claude.ai/code/artifact/8117ae06-8242-46b0-a626-3bac4898dfed |
| scanbot-L1-L4 | https://claude.ai/code/artifact/eece4f23-be25-46b3-bdc1-c2576ac7b78e |
Receiver monitor ran to completion and timed out cleanly; no receiver is still running (pool.sh status nano = idle).
/tmp/mini-broker-sync.sh on Nano; gates: block-id subset + pre-edit hashes; 32 files md5-identical; Mini now 110 blocks). Mini's tailnet key is still expired — re-auth is Sean's.ops/scripts/tg-publish.sh now renders any .html input to PDF with headless Chrome (isolated temp profile, exits when the PDF is complete, ~8 s), sends the PDF (sent: attached-pdf), keeps the HTML staged, ledgers the PDF path; falls back to the old attachment with a loud warning if the render fails. Backups tg-publish.sh.bak-2026091*. Pushed to Nano + Mini. First real send = ingress-proof PDF (Sean's "starting point"). Receivers' SUMMARY-<slug>.html deliveries now open on the phone without a coordinator relay. Inbound: poller owner is still Sean's pick (ingress proof, artifact 8117ae06).handoff.sh runner-shipping defect (line ~155–157) — not fixed; next handoff will stall the same way unless it ships the runner with tar or pool.sh run uses the canonical path. Owner: whoever next touches handoff.sh.com.aios.overnight-council.plist + unload M2's copy.comms/outputs/seanvargas@100.116.58.44I_OS/ on Nano (928 K, lossless dup).comms/outputs/2026-09-12-handoff-summaries/ (synced Nano)skills/computer-use/SKILL.md · laws references/LAWS.md · preflight scripts/preflight.shnano:~/AI_OS/comms/inbox/task-20260912T06.md · packets comms/handoffs/20260912T06