[codex] follow-up: valid pio env CI + dual-hw loop hardening #9

Merged
electron-rare merged 6 commits from codex/autonomy-plan-todo-hw-local into main 2026-02-21 02:03:06 +00:00
14 changed files with 596 additions and 47 deletions
@@ -35,11 +35,15 @@ jobs:
run: |
shellcheck tools/ai/zeroclaw_dual_bootstrap.sh
shellcheck tools/ai/zeroclaw_dual_chat.sh
shellcheck tools/ai/zeroclaw_hw_firmware_loop.sh
shellcheck tools/ai/zeroclaw_stack_up.sh
shellcheck tools/ai/zeroclaw_stack_down.sh
shellcheck tools/ai/zeroclaw_watch_1min.sh
shellcheck tools/ai/zeroclaw_webhook_send.sh
- name: Verify spec files
run: |
test -s specs/03_plan.md
test -s specs/04_tasks.md
test -s specs/zeroclaw_dual_hw_orchestration_spec.md
test -s specs/zeroclaw_dual_hw_todo.md
@@ -36,7 +36,7 @@ Then commit.
## Verify required check contexts
After your first PR, open **PR → Checks** and note the exact names, e.g.
- `Firmware CI / pio (native)`
- `Firmware CI / pio`
- `Scope Guard / guard`
Use those exact names in branch protection (the provided `setup_repo.sh` uses the defaults from the template).
@@ -9,10 +9,6 @@ on:
jobs:
pio:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
env: [esp32s3_arduino, esp32_arduino, native]
steps:
- uses: actions/checkout@v6
@@ -32,7 +28,7 @@ jobs:
- name: No relevant changes (fast pass)
if: steps.changes.outputs.fw != 'true'
run: |
echo "No firmware/spec/standards changes detected. Skipping build/test for ${{ matrix.env }}."
echo "No firmware/spec/standards changes detected. Skipping build/test."
- uses: actions/setup-python@v5
if: steps.changes.outputs.fw == 'true'
@@ -45,7 +41,7 @@ jobs:
path: |
~/.cache/pip
~/.platformio
key: pio-${{ runner.os }}-${{ matrix.env }}-${{ hashFiles('firmware/platformio.ini') }}
key: pio-${{ runner.os }}-${{ hashFiles('firmware/platformio.ini') }}
- name: Install PlatformIO
if: steps.changes.outputs.fw == 'true'
@@ -53,12 +49,58 @@ jobs:
python -m pip install -U pip
python -m pip install -U platformio
- name: Build
- name: Discover PlatformIO envs
id: envs
if: steps.changes.outputs.fw == 'true'
working-directory: firmware
run: pio run -e ${{ matrix.env }}
run: |
envs="$(pio project config --json-output | python3 -c 'import json,sys
arr=json.load(sys.stdin)
names=[]
for item in arr:
name=item.get("name","")
if name.startswith("env:"):
names.append(name.split("env:",1)[1])
print(",".join(names))')"
if [[ -z "$envs" ]]; then
echo "has_envs=false" >>"$GITHUB_OUTPUT"
echo "No PlatformIO env found in firmware/platformio.ini."
else
echo "has_envs=true" >>"$GITHUB_OUTPUT"
echo "envs=$envs" >>"$GITHUB_OUTPUT"
echo "Detected envs: $envs"
fi
- name: Test
if: steps.changes.outputs.fw == 'true' && matrix.env == 'native'
- name: Build all declared envs
if: steps.changes.outputs.fw == 'true' && steps.envs.outputs.has_envs == 'true'
working-directory: firmware
run: pio test -e native
run: |
IFS=',' read -ra env_arr <<<"${{ steps.envs.outputs.envs }}"
for env_name in "${env_arr[@]}"; do
echo "==> pio run -e ${env_name}"
pio run -e "${env_name}"
done
- name: Unit tests (valid env only)
if: steps.changes.outputs.fw == 'true' && steps.envs.outputs.has_envs == 'true'
working-directory: firmware
run: |
if [[ ! -d test ]]; then
echo "No firmware/test directory. Skipping pio test."
exit 0
fi
IFS=',' read -ra env_arr <<<"${{ steps.envs.outputs.envs }}"
test_env=""
for env_name in "${env_arr[@]}"; do
if [[ "$env_name" == "esp32dev" ]]; then
test_env="$env_name"
break
fi
done
if [[ -z "$test_env" ]]; then
test_env="${env_arr[0]}"
fi
echo "==> pio test -e ${test_env} --without-uploading"
pio test -e "${test_env}" --without-uploading
+1 -3
View File
@@ -141,9 +141,7 @@ setup_branch_protection() {
contexts+=("Scope Guard / guard")
if [[ "${REQUIRE_BUILD_CHECKS}" == "1" ]]; then
contexts+=("Firmware CI / pio (esp32s3_arduino)")
contexts+=("Firmware CI / pio (esp32_arduino)")
contexts+=("Firmware CI / pio (native)")
contexts+=("Firmware CI / pio")
contexts+=("Hardware CI (KiCad) / hw")
contexts+=("Compliance Gate / validate")
fi
+13 -7
View File
@@ -29,7 +29,8 @@ avec:
- Local only (loopback `127.0.0.1`), pas d'exposition publique.
- Secrets hors git (`~/.zeroclaw/env`, mode `600`).
- Cout modele borne par quotas webhook locaux.
- Pas de flash/upload firmware sans detection hardware prealable.
- Flash/upload/serial monitor forces par defaut apres detection hardware valide.
- Jamais de commande `pio` sur un env non declare dans `platformio.ini`.
## Plan par phases
@@ -73,8 +74,10 @@ Actions:
2. Hardware discover: `tools/ai/zeroclaw_dual_chat.sh rtc --hardware`
3. Build/test firmware repo RTC:
- `cd /Users/cils/Documents/Lelectron_rare/RTC_BL_PHONE`
- `pio run`
- `pio test -e native` (si environnement present)
- `pio run -e esp32dev`
- `pio run -e esp32dev -t upload`
- `pio device monitor -p <port> -b 115200` (capture 60s)
- ou commande unique: `tools/ai/zeroclaw_hw_firmware_loop.sh rtc`
4. Trace webhook:
- `tools/ai/zeroclaw_webhook_send.sh --repo-hint rtc "rtc loop status ..."`
@@ -91,8 +94,11 @@ Actions:
2. Hardware discover: `tools/ai/zeroclaw_dual_chat.sh zacus --hardware`
3. Build/test firmware repo Zacus:
- `cd /Users/cils/Documents/Lelectron_rare/le-mystere-professeur-zacus/hardware/firmware`
- `pio run`
- `pio test -e native` (si environnement present)
- `pio run -e esp32dev`
- `pio run -e esp32dev -t upload`
- `pio device monitor -p <port> -b 115200` (capture 60s)
- ou commande unique: `tools/ai/zeroclaw_hw_firmware_loop.sh zacus`
- statut tests: `blocked` tant que dossier `test/` absent
4. Trace webhook:
- `tools/ai/zeroclaw_webhook_send.sh --repo-hint zacus "zacus loop status ..."`
@@ -122,7 +128,7 @@ Sortie attendue:
1. Preflight auth + hardware.
2. Stack up + smoke endpoints.
3. RTC loop (dev/test + webhook trace).
4. Zacus loop (dev/test + webhook trace).
3. RTC loop (build + upload + monitor + webhook trace).
4. Zacus loop (build + upload + monitor + webhook trace).
5. Review logs + correction ciblée.
6. Commit/PR petit lot.
+22 -15
View File
@@ -27,23 +27,26 @@ Format:
- AC: commande `prometheus --version` disponible.
- Evidence: sortie shell `prometheus, version ...`
- [ ] T-005 - Stabiliser pairing bearer auto
- [x] T-005 - Stabiliser pairing bearer auto
- AC: `artifacts/zeroclaw/pair_token.txt` utilisable pour webhook sans override manuel.
- Evidence: `tools/ai/zeroclaw_webhook_send.sh --repo-hint rtc "pairing check"`
- Status: `2026-02-21` done (401 resolved; gateway now accepts bearer, downstream model may still return HTTP 500).
## Boucle hardware RTC
- [ ] T-101 - Discover hardware RTC
- [x] T-101 - Discover hardware RTC
- AC: au moins un port detecte pour carte RTC.
- Evidence: `tools/ai/zeroclaw_dual_chat.sh rtc --hardware`
- [ ] T-102 - Build firmware RTC
- AC: `pio run` termine sans erreur bloquante.
- [x] T-102 - Build firmware RTC
- AC: `pio run -e esp32dev` termine sans erreur bloquante.
- Evidence: logs build RTC.
- Status: `2026-02-21` done.
- [ ] T-103 - Test firmware RTC
- AC: `pio test` (ou fallback test local) execute.
- Evidence: rapport test RTC.
- [ ] T-103 - Upload + monitor RTC (forced default)
- AC: `pio run -e esp32dev -t upload` puis monitor 60s executes.
- Evidence: logs upload/monitor RTC.
- Blocker: `esptool` connect timeout (`No serial data received`) on selected USB serial port.
- [ ] T-104 - Trace webhook RTC
- AC: une ligne JSONL avec `repo_hint=rtc` apparait.
@@ -51,17 +54,19 @@ Format:
## Boucle hardware Zacus
- [ ] T-201 - Discover hardware Zacus
- [x] T-201 - Discover hardware Zacus
- AC: au moins un port detecte pour carte Zacus.
- Evidence: `tools/ai/zeroclaw_dual_chat.sh zacus --hardware`
- [ ] T-202 - Build firmware Zacus
- AC: `pio run` (dans `hardware/firmware`) termine sans erreur bloquante.
- [x] T-202 - Build firmware Zacus
- AC: `pio run -e esp32dev` (dans `hardware/firmware`) termine sans erreur bloquante.
- Evidence: logs build Zacus.
- Status: `2026-02-21` done.
- [ ] T-203 - Test firmware Zacus
- AC: `pio test` (ou fallback test local) execute.
- Evidence: rapport test Zacus.
- [ ] T-203 - Upload + monitor Zacus (forced default)
- AC: `pio run -e esp32dev -t upload` puis monitor 60s executes.
- Evidence: logs upload/monitor Zacus.
- Blocker: `esptool` connect timeout (`No serial data received`) on selected USB serial port.
- [ ] T-204 - Trace webhook Zacus
- AC: une ligne JSONL avec `repo_hint=zacus` apparait.
@@ -69,13 +74,14 @@ Format:
## Observabilite et cout
- [ ] T-301 - Stack endpoints smoke
- [x] T-301 - Stack endpoints smoke
- AC: `3000/health`, `8788`, `9090/-/ready` tous OK.
- Evidence: captures `curl`.
- [ ] T-302 - Dry-run webhook budget
- [x] T-302 - Dry-run webhook budget
- AC: `--dry-run` passe sans ecriture execution JSONL.
- Evidence: sortie script + diff JSONL.
- Status: `2026-02-21` done.
- [ ] T-303 - Quota call limiter
- AC: depassement quota bloque avec code non-zero.
@@ -91,3 +97,4 @@ Format:
- [ ] Dashboard live exploitable pour suivi continu.
- [ ] Prometheus disponible avec target gateway scrapee.
- [ ] Logs et preuves archives dans `artifacts/zeroclaw/`.
- [ ] Aucune commande documentee n'utilise `-e native` ou `-e test`.
+1 -1
View File
@@ -4,7 +4,7 @@ project:
targets:
- esp32s3
- esp32
- native
- esp32dev
ai:
triggers:
+31 -1
View File
@@ -9,7 +9,7 @@ Run one orchestration layer that can:
- converse against `RTC_BL_PHONE` and `le-mystere-professeur-zacus` independently,
- keep workspace boundaries strict per repo,
- run low-cost autonomous loops with guarded command allowlists,
- stay ready for connected hardware checks before any upload/flash action.
- enforce upload/flash/serial-monitor loops by default on connected hardware.
## 2) Scope
@@ -18,6 +18,7 @@ In scope:
- local ZeroClaw profile bootstrap for both repos,
- deterministic workspace switch (`rtc` vs `zacus`) from one CLI entrypoint,
- hardware discovery/introspection preflight,
- forced-by-default firmware loop (build + upload + monitor) per repo target,
- lightweight CI validation for orchestration scripts/spec.
Out of scope:
@@ -76,6 +77,7 @@ This keeps `autonomy.workspace_only = true` effective on a per-repo boundary.
- generates live follow dashboard at `http://127.0.0.1:8788/`,
- dashboard includes live polling panels for `/conversations.jsonl` and `/gateway.log` (1s polling),
- preserves direct raw links: `/conversations.jsonl` and `/gateway.log`,
- starts `tools/ai/zeroclaw_watch_1min.sh` watcher and exposes `/realtime_1min.log`,
- writes `artifacts/zeroclaw/prometheus.yml` scrape config,
- supports local Prometheus startup via `ZEROCLAW_PROM_MODE` (`off`, `auto`, `binary`, `docker`) with `auto` fallback `binary -> docker`,
- on macOS, auto-attempts Docker Desktop startup before Prometheus docker mode,
@@ -83,7 +85,19 @@ This keeps `autonomy.workspace_only = true` effective on a per-repo boundary.
- `tools/ai/zeroclaw_stack_down.sh`
- stops local gateway/follow processes,
- stops local Prometheus process/container if managed by the stack,
- stops `tools/ai/zeroclaw_watch_1min.sh` watcher process,
- confirms logs remain in `artifacts/zeroclaw/`.
- `tools/ai/zeroclaw_watch_1min.sh`
- supports `start|stop|status|run|once`,
- appends one status line every 60s by default to `artifacts/zeroclaw/realtime_1min.log`,
- line format:
- `<ts> | paired=<...> | uptime=<...> | prom=<...> | convo=<last line> | gateway=<last line>`
- `tools/ai/zeroclaw_hw_firmware_loop.sh`
- target switch `rtc|zacus`,
- validates `platformio.ini` env exists before running,
- forces `build -> upload -> serial monitor` by default,
- auto-detects serial port when not specified,
- monitor timeout default 60s.
- `tools/ai/zeroclaw_webhook_send.sh`
- sends webhook by default (no mandatory allow flag),
- keeps `--allow-model-call` as backward-compatible legacy option,
@@ -148,6 +162,7 @@ Raw URLs:
- `http://127.0.0.1:8788/conversations.jsonl`
- `http://127.0.0.1:8788/gateway.log`
- `http://127.0.0.1:8788/realtime_1min.log`
- `http://127.0.0.1:8788/prometheus.yml`
Conversation JSONL line schema (append-only):
@@ -203,3 +218,18 @@ Suggested contents:
Behavior:
- `zeroclaw_dual_chat.sh`, `zeroclaw_stack_up.sh`, and `zeroclaw_webhook_send.sh` auto-load this file if present.
## 11) Firmware Loop Defaults (local hardware)
Default execution policy:
- with hardware connected, upload/flash and serial monitor are forced by default.
- if no serial port is detected, loop fails fast (non-zero) instead of silently skipping upload.
- only declared PlatformIO envs are allowed; no `-e native` or `-e test` shortcuts.
Default commands:
- RTC:
- `tools/ai/zeroclaw_hw_firmware_loop.sh rtc`
- Zacus:
- `tools/ai/zeroclaw_hw_firmware_loop.sh zacus`
+8 -6
View File
@@ -11,32 +11,34 @@ Last updated: 2026-02-21
## Daily autonomous sequence
- [ ] D-001 - `tools/ai/zeroclaw_stack_down.sh` then `ZEROCLAW_PROM_MODE=auto tools/ai/zeroclaw_stack_up.sh`.
- [ ] D-002 - Smoke endpoints:
- [x] D-001 - `tools/ai/zeroclaw_stack_down.sh` then `ZEROCLAW_PROM_MODE=auto tools/ai/zeroclaw_stack_up.sh`.
- [x] D-002 - Smoke endpoints:
- `curl -fsS http://127.0.0.1:3000/health`
- `curl -fsS http://127.0.0.1:8788/`
- `curl -fsS http://127.0.0.1:9090/-/ready`
- [ ] D-003 - RTC loop:
- `tools/ai/zeroclaw_dual_chat.sh rtc --provider-check`
- `tools/ai/zeroclaw_dual_chat.sh rtc --hardware`
- repo build/test
- `tools/ai/zeroclaw_hw_firmware_loop.sh rtc` (build+upload+monitor forced default)
- webhook trace with `--repo-hint rtc`
- current blocker: serial upload timeout (`No serial data received`)
- [ ] D-004 - Zacus loop:
- `tools/ai/zeroclaw_dual_chat.sh zacus --provider-check`
- `tools/ai/zeroclaw_dual_chat.sh zacus --hardware`
- repo build/test
- `tools/ai/zeroclaw_hw_firmware_loop.sh zacus` (build+upload+monitor forced default)
- webhook trace with `--repo-hint zacus`
- current blocker: serial upload timeout (`No serial data received`)
- [ ] D-005 - Review `artifacts/zeroclaw/gateway.log` + `conversations.jsonl`.
## Hardware safety gates
- [ ] H-001 - No flash/upload if `--hardware` detect returns no board.
- [ ] H-001 - Flash/upload/monitor are forced by default when a board is detected.
- [ ] H-002 - Resolve stable serial target before upload.
- [ ] H-003 - Keep per-run logs under `artifacts/zeroclaw/`.
## Cost/control gates
- [ ] C-001 - Validate `tools/ai/zeroclaw_webhook_send.sh --dry-run`.
- [x] C-001 - Validate `tools/ai/zeroclaw_webhook_send.sh --dry-run`.
- [ ] C-002 - Validate hourly quota guard (`ZEROCLAW_WEBHOOK_MAX_CALLS_PER_HOUR`).
- [ ] C-003 - Validate message length guard (`ZEROCLAW_WEBHOOK_MAX_CHARS`).
+198
View File
@@ -0,0 +1,198 @@
#!/usr/bin/env bash
set -euo pipefail
RTC_REPO="${ZEROCLAW_RTC_REPO:-/Users/cils/Documents/Lelectron_rare/RTC_BL_PHONE}"
ZACUS_REPO="${ZEROCLAW_ZACUS_REPO:-/Users/cils/Documents/Lelectron_rare/le-mystere-professeur-zacus}"
RTC_FW_DIR="$RTC_REPO"
ZACUS_FW_DIR="$ZACUS_REPO/hardware/firmware"
RTC_ENV_DEFAULT="${ZEROCLAW_RTC_PIO_ENV:-esp32dev}"
ZACUS_ENV_DEFAULT="${ZEROCLAW_ZACUS_PIO_ENV:-esp32dev}"
MONITOR_SECS_DEFAULT="${ZEROCLAW_MONITOR_SECS:-60}"
BAUD_DEFAULT="${ZEROCLAW_SERIAL_BAUD:-115200}"
usage() {
cat <<USAGE
Usage:
$(basename "$0") <rtc|zacus> [--env <platformio_env>] [--port <serial_port>] [--baud <baud>] [--monitor-secs <n>]
Behavior:
- Build is always executed.
- Upload/flash is forced by default.
- Serial monitor is forced by default for monitor-secs (default: ${MONITOR_SECS_DEFAULT}s).
USAGE
}
if [[ $# -lt 1 ]]; then
usage
exit 1
fi
target="$1"
shift
FW_DIR=""
ENV_NAME=""
case "$target" in
rtc)
FW_DIR="$RTC_FW_DIR"
ENV_NAME="$RTC_ENV_DEFAULT"
;;
zacus)
FW_DIR="$ZACUS_FW_DIR"
ENV_NAME="$ZACUS_ENV_DEFAULT"
;;
*)
echo "[fail] unsupported target: $target (expected rtc|zacus)" >&2
exit 1
;;
esac
PORT="${ZEROCLAW_UPLOAD_PORT:-}"
BAUD="$BAUD_DEFAULT"
MONITOR_SECS="$MONITOR_SECS_DEFAULT"
while [[ $# -gt 0 ]]; do
case "$1" in
--env)
ENV_NAME="${2:-}"
shift 2
;;
--port)
PORT="${2:-}"
shift 2
;;
--baud)
BAUD="${2:-}"
shift 2
;;
--monitor-secs)
MONITOR_SECS="${2:-}"
shift 2
;;
-h|--help)
usage
exit 0
;;
*)
echo "[fail] unknown option: $1" >&2
usage
exit 1
;;
esac
done
if [[ ! -d "$FW_DIR" ]]; then
echo "[fail] firmware directory not found: $FW_DIR" >&2
exit 1
fi
if [[ ! -f "$FW_DIR/platformio.ini" ]]; then
echo "[fail] platformio.ini not found in: $FW_DIR" >&2
exit 1
fi
if ! command -v pio >/dev/null 2>&1; then
echo "[fail] platformio (pio) not found in PATH." >&2
exit 1
fi
if ! rg -q "^\[env:${ENV_NAME}\]" "$FW_DIR/platformio.ini"; then
echo "[fail] invalid PlatformIO env '${ENV_NAME}' for $target." >&2
echo "[info] valid envs:" >&2
rg "^\[env:" "$FW_DIR/platformio.ini" >&2 || true
exit 1
fi
if [[ -z "$PORT" ]]; then
if ! PORT="$(
cd "$FW_DIR"
pio device list --json-output 2>/dev/null | python3 -c 'import json,sys
raw=sys.stdin.read().strip()
try:
arr=json.loads(raw) if raw else []
except Exception:
arr=[]
best=None
best_score=-10**9
for d in arr:
p=str(d.get("port",""))
desc=str(d.get("description",""))
hwid=str(d.get("hwid",""))
if not p:
continue
score=0
low=f\"{p} {desc} {hwid}\".lower()
if hwid and hwid.lower() != \"n/a\":
score += 50
if \"usb\" in low:
score += 30
if \"cp210\" in low or \"ch340\" in low or \"uart\" in low:
score += 20
if \"bluetooth\" in low:
score -= 100
if \"/cu.urt\" in p.lower() and hwid.lower() == \"n/a\":
score -= 120
if score > best_score:
best_score=score
best=p
if best:
print(best)
raise SystemExit(0)
raise SystemExit(1)'
)"; then
PORT=""
fi
fi
if [[ -z "$PORT" ]]; then
echo "[fail] no serial port detected. Upload/monitor are forced by default." >&2
exit 2
fi
UPLOAD_ARGS=()
MONITOR_ARGS=()
if [[ -n "$PORT" ]]; then
UPLOAD_ARGS+=(--upload-port "$PORT")
MONITOR_ARGS+=(-p "$PORT")
fi
if [[ -n "$BAUD" ]]; then
MONITOR_ARGS+=(-b "$BAUD")
fi
echo "[info] target=$target fw_dir=$FW_DIR env=$ENV_NAME port=$PORT baud=$BAUD monitor_secs=$MONITOR_SECS"
echo "[step] build"
(cd "$FW_DIR" && pio run -e "$ENV_NAME")
echo "[step] upload/flash (forced default)"
(cd "$FW_DIR" && pio run -e "$ENV_NAME" -t upload "${UPLOAD_ARGS[@]}")
echo "[step] serial monitor (forced default)"
python3 - "$FW_DIR" "$MONITOR_SECS" "${MONITOR_ARGS[@]}" <<'PY'
import os
import signal
import subprocess
import sys
import time
fw_dir = sys.argv[1]
monitor_secs = int(sys.argv[2])
monitor_args = sys.argv[3:]
cmd = ["pio", "device", "monitor", *monitor_args]
print(f"[info] monitor command: {' '.join(cmd)} (timeout={monitor_secs}s)")
proc = subprocess.Popen(cmd, cwd=fw_dir, preexec_fn=os.setsid)
try:
time.sleep(monitor_secs)
finally:
if proc.poll() is None:
os.killpg(proc.pid, signal.SIGTERM)
try:
proc.wait(timeout=3)
except subprocess.TimeoutExpired:
os.killpg(proc.pid, signal.SIGKILL)
proc.wait(timeout=2)
PY
echo "[ok] loop complete for $target ($ENV_NAME)."
+5
View File
@@ -2,6 +2,7 @@
set -euo pipefail
ART_DIR="${ZEROCLAW_ART_DIR:-/Users/cils/Documents/Lelectron_rare/Kill_LIFE/artifacts/zeroclaw}"
ROOT_DIR="/Users/cils/Documents/Lelectron_rare/Kill_LIFE"
GW_PID_FILE="$ART_DIR/gateway.pid"
FW_PID_FILE="$ART_DIR/follow.pid"
PROM_PID_FILE="$ART_DIR/prometheus.pid"
@@ -9,6 +10,7 @@ PROM_CONTAINER="${ZEROCLAW_PROM_CONTAINER:-zeroclaw-prometheus}"
GW_MANAGED_FILE="$ART_DIR/gateway.managed"
FW_MANAGED_FILE="$ART_DIR/follow.managed"
PROM_MANAGED_FILE="$ART_DIR/prometheus.managed"
WATCHER_SCRIPT="$ROOT_DIR/tools/ai/zeroclaw_watch_1min.sh"
stop_pid_file() {
local pid_file="$1"
@@ -42,4 +44,7 @@ if [[ -f "$PROM_MANAGED_FILE" ]]; then
fi
fi
rm -f "$GW_MANAGED_FILE" "$FW_MANAGED_FILE" "$PROM_MANAGED_FILE"
if [[ -x "$WATCHER_SCRIPT" ]]; then
"$WATCHER_SCRIPT" stop >/dev/null 2>&1 || true
fi
echo "ZeroClaw local stack stopped. Logs preserved in artifacts/zeroclaw/."
+107 -2
View File
@@ -27,6 +27,7 @@ PROM_SCRAPE_INTERVAL="${ZEROCLAW_PROM_SCRAPE_INTERVAL:-15s}"
PROM_CONTAINER="${ZEROCLAW_PROM_CONTAINER:-zeroclaw-prometheus}"
DOCKER_WAIT_SECS="${ZEROCLAW_DOCKER_WAIT_SECS:-90}"
PROM_READY_WAIT_SECS="${ZEROCLAW_PROM_READY_WAIT_SECS:-15}"
AUTO_REPAIR_ON_INVALID_TOKEN="${ZEROCLAW_AUTO_REPAIR_ON_INVALID_TOKEN:-1}"
GW_PID_FILE="$ART_DIR/gateway.pid"
FW_PID_FILE="$ART_DIR/follow.pid"
@@ -40,13 +41,17 @@ PROM_LOG="$ART_DIR/prometheus.log"
PROM_CONFIG_FILE="$ART_DIR/prometheus.yml"
PROM_DATA_DIR="$ART_DIR/prometheus-data"
PROM_STATUS="disabled"
ZEROCLAW_CONFIG_FILE="${ZEROCLAW_CONFIG_FILE:-$HOME/.zeroclaw/config.toml}"
GW_MANAGED_FILE="$ART_DIR/gateway.managed"
FW_MANAGED_FILE="$ART_DIR/follow.managed"
PROM_MANAGED_FILE="$ART_DIR/prometheus.managed"
WATCHER_SCRIPT="$ROOT_DIR/tools/ai/zeroclaw_watch_1min.sh"
RT_LOG="$ART_DIR/realtime_1min.log"
mkdir -p "$ART_DIR"
touch "$CONVO_FILE"
touch "$GW_LOG"
touch "$RT_LOG"
if [[ -f "$HOME/.zeroclaw/config.toml" ]]; then
chmod 600 "$HOME/.zeroclaw/config.toml" >/dev/null 2>&1 || true
@@ -211,8 +216,44 @@ sys.exit(0 if obj.get("paired") else 1)' >/dev/null 2>&1
}
read_pair_token() {
[[ -f "$TOKEN_FILE" ]] || return 1
cat "$TOKEN_FILE" 2>/dev/null
if [[ -f "$TOKEN_FILE" ]]; then
cat "$TOKEN_FILE" 2>/dev/null
return 0
fi
if [[ -f "$ZEROCLAW_CONFIG_FILE" ]]; then
local token_from_config
token_from_config="$(python3 - "$ZEROCLAW_CONFIG_FILE" <<'PY'
import sys
from pathlib import Path
cfg = Path(sys.argv[1])
try:
import tomllib
except Exception:
print("", end="")
raise SystemExit(0)
try:
obj = tomllib.loads(cfg.read_text(encoding="utf-8"))
except Exception:
print("", end="")
raise SystemExit(0)
tokens = obj.get("gateway", {}).get("paired_tokens", [])
if isinstance(tokens, list) and tokens:
print(str(tokens[0]), end="")
PY
)"
if [[ -n "$token_from_config" ]]; then
printf '%s\n' "$token_from_config" >"$TOKEN_FILE"
chmod 600 "$TOKEN_FILE"
printf '%s\n' "$token_from_config"
return 0
fi
fi
return 1
}
token_is_usable() {
@@ -231,6 +272,34 @@ token_is_usable() {
return 0
}
wait_for_gateway_health() {
local waited=0
while (( waited < 20 )); do
if curl -fsS "http://$GATEWAY_HOST:$GATEWAY_PORT/health" >/dev/null 2>&1; then
return 0
fi
sleep 0.25
waited=$((waited + 1))
done
return 1
}
clear_config_paired_tokens() {
[[ -f "$ZEROCLAW_CONFIG_FILE" ]] || return 1
python3 - "$ZEROCLAW_CONFIG_FILE" <<'PY'
import re
import sys
from pathlib import Path
cfg = Path(sys.argv[1])
text = cfg.read_text(encoding="utf-8")
updated, n = re.subn(r'(?m)^paired_tokens\s*=\s*\[.*\]\s*$', 'paired_tokens = []', text)
if n == 0:
raise SystemExit(1)
cfg.write_text(updated, encoding="utf-8")
PY
}
extract_pair_code() {
grep -Eo 'X-Pairing-Code: [0-9]{6}' "$GW_LOG" 2>/dev/null | tail -1 | awk '{print $2}' || true
}
@@ -281,6 +350,36 @@ ensure_gateway_pairing() {
current_token="$(read_pair_token || true)"
if ! token_is_usable "$current_token"; then
rm -f "$TOKEN_FILE"
if [[ "$AUTO_REPAIR_ON_INVALID_TOKEN" == "1" && -f "$GW_MANAGED_FILE" ]]; then
if clear_config_paired_tokens; then
local gw_pid=""
gw_pid="$(cat "$GW_PID_FILE" 2>/dev/null || true)"
if [[ -n "$gw_pid" ]] && kill -0 "$gw_pid" >/dev/null 2>&1; then
kill "$gw_pid" >/dev/null 2>&1 || true
sleep 0.3
if kill -0 "$gw_pid" >/dev/null 2>&1; then
kill -9 "$gw_pid" >/dev/null 2>&1 || true
fi
fi
nohup "$ZEROCLAW_BIN" gateway --port "$GATEWAY_PORT" --host "$GATEWAY_HOST" >"$GW_LOG" 2>&1 &
echo "$!" >"$GW_PID_FILE"
wait_for_gateway_health || true
for _ in $(seq 1 40); do
pair_code="$(extract_pair_code)"
if [[ -n "$pair_code" ]]; then
break
fi
sleep 0.25
done
if [[ -n "${pair_code:-}" ]]; then
write_pair_token_from_code "$pair_code" || true
fi
fi
fi
fi
}
@@ -461,6 +560,7 @@ cat >"$INDEX_FILE" <<EOF
<div class="links">
<a href="/conversations.jsonl">/conversations.jsonl</a>
<a href="/gateway.log">/gateway.log</a>
<a href="/realtime_1min.log">/realtime_1min.log</a>
<a href="/prometheus.yml">/prometheus.yml</a>
<a href="http://$GATEWAY_HOST:$GATEWAY_PORT/health">/health</a>
<a href="http://$GATEWAY_HOST:$GATEWAY_PORT/metrics">/metrics</a>
@@ -576,9 +676,14 @@ cat >"$INDEX_FILE" <<EOF
</html>
EOF
if [[ -x "$WATCHER_SCRIPT" ]]; then
"$WATCHER_SCRIPT" start >/dev/null 2>&1 || true
fi
echo "Gateway: http://$GATEWAY_HOST:$GATEWAY_PORT/health"
echo "Follow : http://127.0.0.1:$FOLLOW_PORT/"
echo "Prom : http://$PROM_HOST:$PROM_PORT/targets ($PROM_STATUS)"
echo "RT 1m : http://127.0.0.1:$FOLLOW_PORT/realtime_1min.log"
echo "Logs : $GW_LOG"
echo "Token : $TOKEN_FILE"
if [[ ! -s "$TOKEN_FILE" ]]; then
+123
View File
@@ -0,0 +1,123 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT_DIR="/Users/cils/Documents/Lelectron_rare/Kill_LIFE"
ART_DIR="${ZEROCLAW_ART_DIR:-$ROOT_DIR/artifacts/zeroclaw}"
HOST="${ZEROCLAW_GATEWAY_HOST:-127.0.0.1}"
PORT="${ZEROCLAW_GATEWAY_PORT:-3000}"
PROM_HOST="${ZEROCLAW_PROM_HOST:-127.0.0.1}"
PROM_PORT="${ZEROCLAW_PROM_PORT:-9090}"
INTERVAL_SECS="${ZEROCLAW_RT_INTERVAL_SECS:-60}"
PID_FILE="$ART_DIR/realtime_1min.pid"
LOG_FILE="$ART_DIR/realtime_1min.log"
CONVO_FILE="$ART_DIR/conversations.jsonl"
GW_LOG_FILE="$ART_DIR/gateway.log"
mkdir -p "$ART_DIR"
touch "$LOG_FILE" "$CONVO_FILE" "$GW_LOG_FILE"
is_running() {
[[ -f "$PID_FILE" ]] || return 1
local pid
pid="$(cat "$PID_FILE" 2>/dev/null || true)"
[[ -n "$pid" ]] || return 1
kill -0 "$pid" >/dev/null 2>&1
}
collect_once() {
local ts health paired uptime prom last_convo last_gw
ts="$(date -u +"%Y-%m-%dT%H:%M:%SZ")"
health="$(curl -sS "http://$HOST:$PORT/health" || true)"
if [[ -n "$health" ]]; then
paired="$(printf '%s' "$health" | python3 -c 'import json,sys
raw=sys.stdin.read().strip()
try:
obj=json.loads(raw) if raw else {}
except Exception:
print("unknown")
raise SystemExit
print(str(obj.get("paired","unknown")).lower())' 2>/dev/null || echo "unknown")"
uptime="$(printf '%s' "$health" | python3 -c 'import json,sys
raw=sys.stdin.read().strip()
try:
obj=json.loads(raw) if raw else {}
except Exception:
print("unknown")
raise SystemExit
print(obj.get("runtime",{}).get("uptime_seconds","unknown"))' 2>/dev/null || echo "unknown")"
else
paired="down"
uptime="down"
fi
if curl -fsS "http://$PROM_HOST:$PROM_PORT/-/ready" >/dev/null 2>&1; then
prom="ready"
else
prom="down"
fi
last_convo="$(tail -n 1 "$CONVO_FILE" 2>/dev/null || true)"
last_gw="$(tail -n 1 "$GW_LOG_FILE" 2>/dev/null || true)"
last_convo="${last_convo//$'\t'/ }"
last_gw="${last_gw//$'\t'/ }"
printf '%s | paired=%s | uptime=%s | prom=%s | convo=%s | gateway=%s\n' \
"$ts" "$paired" "$uptime" "$prom" "$last_convo" "$last_gw" >>"$LOG_FILE"
}
run_loop() {
while true; do
collect_once
sleep "$INTERVAL_SECS"
done
}
start() {
if is_running; then
echo "[info] watcher already running (pid $(cat "$PID_FILE"))."
return 0
fi
nohup "$0" run >>"$LOG_FILE" 2>&1 &
echo "$!" >"$PID_FILE"
echo "[ok] watcher started (pid $(cat "$PID_FILE"))."
}
stop() {
if ! is_running; then
rm -f "$PID_FILE"
echo "[info] watcher not running."
return 0
fi
local pid
pid="$(cat "$PID_FILE")"
kill "$pid" >/dev/null 2>&1 || true
sleep 0.2
if kill -0 "$pid" >/dev/null 2>&1; then
kill -9 "$pid" >/dev/null 2>&1 || true
fi
rm -f "$PID_FILE"
echo "[ok] watcher stopped."
}
status() {
if is_running; then
echo "[ok] watcher running (pid $(cat "$PID_FILE"))."
else
echo "[info] watcher not running."
fi
tail -n 5 "$LOG_FILE" 2>/dev/null || true
}
case "${1:-start}" in
start) start ;;
stop) stop ;;
status) status ;;
run) run_loop ;;
once) collect_once ;;
*)
echo "Usage: $(basename "$0") [start|stop|status|run|once]" >&2
exit 1
;;
esac
+29
View File
@@ -17,6 +17,7 @@ ART_DIR="${ZEROCLAW_ART_DIR:-/Users/cils/Documents/Lelectron_rare/Kill_LIFE/arti
HOST="${ZEROCLAW_GATEWAY_HOST:-127.0.0.1}"
PORT="${ZEROCLAW_GATEWAY_PORT:-3000}"
TOKEN_FILE="$ART_DIR/pair_token.txt"
ZEROCLAW_CONFIG_FILE="${ZEROCLAW_CONFIG_FILE:-$HOME/.zeroclaw/config.toml}"
CONVO_FILE="$ART_DIR/conversations.jsonl"
BUDGET_FILE="$ART_DIR/webhook_budget.json"
MAX_CALLS_PER_HOUR="${ZEROCLAW_WEBHOOK_MAX_CALLS_PER_HOUR:-40}"
@@ -93,6 +94,34 @@ TOKEN="${ZEROCLAW_BEARER:-}"
if [[ -z "$TOKEN" && -f "$TOKEN_FILE" ]]; then
TOKEN="$(cat "$TOKEN_FILE")"
fi
if [[ -z "$TOKEN" && -f "$ZEROCLAW_CONFIG_FILE" ]]; then
TOKEN="$(python3 - "$ZEROCLAW_CONFIG_FILE" <<'PY'
import sys
from pathlib import Path
cfg = Path(sys.argv[1])
try:
import tomllib
except Exception:
print("", end="")
raise SystemExit(0)
try:
obj = tomllib.loads(cfg.read_text(encoding="utf-8"))
except Exception:
print("", end="")
raise SystemExit(0)
tokens = obj.get("gateway", {}).get("paired_tokens", [])
if isinstance(tokens, list) and tokens:
print(str(tokens[0]), end="")
PY
)"
if [[ -n "$TOKEN" ]]; then
printf '%s\n' "$TOKEN" >"$TOKEN_FILE"
chmod 600 "$TOKEN_FILE"
fi
fi
mkdir -p "$ART_DIR"
touch "$CONVO_FILE"