feat(pmo): ping-charles SMS script (curl SMTP relay, offline queue, 0600 config)

This commit is contained in:
2026-08-29 06:29:43 -05:00
parent ee46e700a8
commit 9f1389c443
7 changed files with 127 additions and 1 deletions
@@ -0,0 +1,47 @@
# TASK: FINAL crush turn — harness runtime container + tool-widening (Redmine 521 + bootstrap)
## Context
Charles 2026-08-29: "alpha asap, then kill the screen/crush stack; bootstrap
the harness (docker container) FIRST." This is the LAST crush turn on the
harness repo. After it: the loop runs containerized AND its turns can do
build work — the self-hosted stack builds everything to beta/prod itself.
## Part A — runtime container (bootstrap)
1. `deploy/Dockerfile.runtime` — FROM alpine (digest-pinned), copies the
static release binary, dumb-init or tini entrypoint (or Go signal
handling — SIGINT graceful stop already exists).
2. `deploy/compose.yaml` — service `mopac-loop`: image built from
Dockerfile.runtime, `restart: unless-stopped`, env-file (0600) for
HARNESS_REDMINE_KEY / HARNESS_LITELLM_KEY, bind mounts: repo reports/
+ state/ (persistent), harness.toml read-only. Healthcheck: loop
exposes nothing by default — add `--healthz-port` flag or reuse events
healthz; simplest: a `loop --status-file` heartbeat the healthcheck
reads (age < 3x poll interval).
3. Verify LIVE: compose up, confirm scan + at least one autonomous turn
(the alpha queue has open tasks), `docker inspect` restart policy,
then leave the container RUNNING (it replaces the dev.sh screen).
4. Kill switch documented: `docker compose -f deploy/compose.yaml down`.
## Part B — tool-widening (ticket 521: loop turns do build work)
1. File tools behind the existing gate discipline (deny-first, scoped to
work_root, logged): read (offset/limit), exact-match edit, write (new
files), ls/glob/grep. Same denial-count + feedback semantics as the
bash tool.
2. Bash gate: verify `./dev.sh {build,vet,test}` and `git commit/push`
pass the allow-list from work_root; extend the allow-list minimally if
not (no broadening beyond dev.sh + git).
3. Per-turn wall-clock timeout (config `[loop] turn_timeout_secs`,
default 1800) — expired turn = partial REPORT + release.
4. Tests: file-tool gate denials, edit exact-match semantics, timeout
expiry, and an end-to-end loop turn against the fake Redmine that
edits a file + runs a no-op command + REPORTs.
## Discipline
TDD red/green, docker dev builder only, atomic conventional commits,
push origin main, NEVER broad pkill (docker stop by name is fine).
## Deliverable
`REPORT-20260829-1500-bootstrap.md` in `~/.coordinate/inbox-pmo/`:
container instructions (up/down/logs), healthcheck design, tool surface
added, test results, and confirmation the container processed a real
alpha task autonomously.
+1
View File
@@ -87,3 +87,4 @@
2026-08-29T06:19:29-05:00 | PMO | 497 corrected: BW machine accounts map to CLOUDRON accounts (per-vertical API identities), not raw Linux accounts
2026-08-29T06:20:17-05:00 | PMO | identity model ticket filed (linux runtime + cloudron acting, N per account); 497 corrected
2026-08-29T06:21:06-05:00 | PMO | identity model FINAL: 1:1 linux->BW vault (cloudron+3rd-party entries); acting identity selected per task/turn, stamped on all actions + usage
2026-08-29T06:26:47-05:00 | PMO | ALPHA LIVE: loop autonomous (520 done New->Resolved+note, 519 running); q12 killed; q14 CLI keeps running; tool-widening ticket filed Immediate
+27
View File
@@ -8,3 +8,30 @@ harness: loop: status New -> Resolved on #520
harness: loop: noted REPORT on #520
harness: loop: dispatch 519 (updated 2026-08-29T06:23:26-05:00): "Alpha: draft briefing v0 format as note on 495"
harness: task 519 (redmine): "Alpha: draft briefing v0 format as note on 495" class=study -> mopac-study -> glm-4.7-flash
harness: REPORT reports/REPORT-demo-519-20260829-112832.md
harness: loop: status transition failed for 519: redmine HTTP 502: <!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="user-scalable=no, initial-scale=1, maximum-scale=1, minimum-scale=1, width=device-width, height=device-height" />
<!-- generated from dashboard/public/img/app_down.svg -->
<link rel="icon" href="da...
harness: loop: note writeback failed for 519: redmine HTTP 502: <!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="user-scalable=no, initial-scale=1, maximum-scale=1, minimum-scale=1, width=device-width, height=device-height" />
<!-- generated from dashboard/public/img/app_down.svg -->
<link rel="icon" href="da...
harness: loop: refresh failed for 519: redmine HTTP 502: <!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="user-scalable=no, initial-scale=1, maximum-scale=1, minimum-scale=1, width=device-width, height=device-height" />
<!-- generated from dashboard/public/img/app_down.svg -->
<link rel="icon" href="da... (a re-dispatch may follow)
harness: loop: dispatch 518 (updated 2026-08-29T06:23:26-05:00): "Alpha: triage open MOPAC tickets — next-action note on each New ticket"
harness: task 518 (redmine): "Alpha: triage open MOPAC tickets — next-action note on each New ticket" class=study -> mopac-study -> glm-4.7-flash
View File
+1 -1
View File
@@ -1 +1 @@
I have full context: sibling pattern (mopac-bitwarden-go), harness fake-server test style, PMO python glue to replace, creds at `~/.creds/redmine.env`, Redmine at projects.knownelement.com. Setting up the work plan.Toolchain verified. Creating the repo via Gitea API (idempotent check first):Repo created. Cloning and seeding with LICENSE:Harness done. Now TDD config package — tests first (red):Red confirmed. Implementation:Green. Two atomic commits:
I have full context: sibling pattern (mopac-bitwarden-go), harness fake-server test style, PMO python glue to replace, creds at `~/.creds/redmine.env`, Redmine at projects.knownelement.com. Setting up the work plan.Toolchain verified. Creating the repo via Gitea API (idempotent check first):Repo created. Cloning and seeding with LICENSE:Harness done. Now TDD config package — tests first (red):Red confirmed. Implementation:Green. Two atomic commits:Now the core piece. First the stateful fake Redmine server (test infrastructure, harness-style):Now the library test suite (red first):
+1
View File
@@ -0,0 +1 @@
[info 08-29 06:29 MOPAC] alpha loop live, bootstrap turn dispatched, CLI building
+50
View File
@@ -0,0 +1,50 @@
#!/usr/bin/env bash
# ping-charles.sh — SMS Charles via T-Mobile email-to-SMS gateway.
# Usage: ping-charles.sh "short message" [severity]
# Relay: curl SMTP (Cloudron mail submission). Config: /home/reachableceo/.coordinate/secrets/ping-charles.env (0600)
# PING_SMTP_HOST=mail.knownelement.com:587
# PING_SMTP_USER=...
# PING_SMTP_PASS=...
# PING_FROM=...
# Until wired, messages queue in ~/.coordinate/outbox/ping-queue.txt (flushed on first successful send).
set -euo pipefail
DEST="8182807059@tmomail.net"
CONF="$HOME/.coordinate/secrets/ping-charles.env"
QUEUE="$HOME/.coordinate/outbox/ping-queue.txt"
MSG="${1:?usage: ping-charles.sh \"message\" [severity]}"
SEV="${2:-info}"
STAMP="$(date '+%m-%d %H:%M')"
LINE="[$SEV $STAMP MOPAC] $MSG"
if [ ! -f "$CONF" ]; then
mkdir -p "$(dirname "$QUEUE")"
echo "$LINE" >> "$QUEUE"
echo "ping queued (no relay config): $LINE" >&2
exit 0
fi
set -a; . "$CONF"; set +a
send_one() {
printf 'From: %s\nTo: %s\nSubject: MOPAC %s\nContent-Type: text/plain; charset=UTF-8\n\n%s\n' \
"$PING_FROM" "$DEST" "$SEV" "$1" \
| curl -sS --max-time 30 --url "smtp://$PING_SMTP_HOST" \
--mail-from "$PING_FROM" --mail-rcpt "$DEST" \
-u "$PING_SMTP_USER:$PING_SMTP_PASS" \
-T - && return 0 || return 1
}
if [ -f "$QUEUE" ] && [ -s "$QUEUE" ]; then
while IFS= read -r queued; do
send_one "$queued" && sed -i '1d' "$QUEUE" || break
done < /dev/null
fi
if send_one "$LINE"; then
echo "sms sent: $LINE" >&2
exit 0
else
mkdir -p "$(dirname "$QUEUE")"; echo "$LINE" >> "$QUEUE"
echo "sms send FAILED, queued: $LINE" >&2
exit 1
fi