diff --git a/.github/workflows/zeroclaw_dual_orchestrator.yml b/.github/workflows/zeroclaw_dual_orchestrator.yml index 219675e..d6a1f09 100644 --- a/.github/workflows/zeroclaw_dual_orchestrator.yml +++ b/.github/workflows/zeroclaw_dual_orchestrator.yml @@ -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 diff --git a/gh_setup_and_patches/REQUIRED_CHECKS_STRATEGY.md b/gh_setup_and_patches/REQUIRED_CHECKS_STRATEGY.md index 0e9351e..59ced4a 100644 --- a/gh_setup_and_patches/REQUIRED_CHECKS_STRATEGY.md +++ b/gh_setup_and_patches/REQUIRED_CHECKS_STRATEGY.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). diff --git a/gh_setup_and_patches/patched_workflows/firmware_ci.yml b/gh_setup_and_patches/patched_workflows/firmware_ci.yml index 76cae16..526d38e 100644 --- a/gh_setup_and_patches/patched_workflows/firmware_ci.yml +++ b/gh_setup_and_patches/patched_workflows/firmware_ci.yml @@ -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 diff --git a/gh_setup_and_patches/setup_repo.sh b/gh_setup_and_patches/setup_repo.sh index 6532c11..f9dfcb9 100755 --- a/gh_setup_and_patches/setup_repo.sh +++ b/gh_setup_and_patches/setup_repo.sh @@ -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 diff --git a/specs/03_plan.md b/specs/03_plan.md index 08686e4..3811105 100644 --- a/specs/03_plan.md +++ b/specs/03_plan.md @@ -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 -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 -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. diff --git a/specs/04_tasks.md b/specs/04_tasks.md index a01ae8f..3800c0c 100644 --- a/specs/04_tasks.md +++ b/specs/04_tasks.md @@ -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`. diff --git a/specs/constraints.yaml b/specs/constraints.yaml index a58fbf7..3e05918 100644 --- a/specs/constraints.yaml +++ b/specs/constraints.yaml @@ -4,7 +4,7 @@ project: targets: - esp32s3 - esp32 - - native + - esp32dev ai: triggers: diff --git a/specs/zeroclaw_dual_hw_orchestration_spec.md b/specs/zeroclaw_dual_hw_orchestration_spec.md index d6f03e8..636653e 100644 --- a/specs/zeroclaw_dual_hw_orchestration_spec.md +++ b/specs/zeroclaw_dual_hw_orchestration_spec.md @@ -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: + - ` | paired=<...> | uptime=<...> | prom=<...> | convo= | gateway=` +- `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` diff --git a/specs/zeroclaw_dual_hw_todo.md b/specs/zeroclaw_dual_hw_todo.md index 2b68c67..817a67e 100644 --- a/specs/zeroclaw_dual_hw_todo.md +++ b/specs/zeroclaw_dual_hw_todo.md @@ -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`). diff --git a/tools/ai/zeroclaw_hw_firmware_loop.sh b/tools/ai/zeroclaw_hw_firmware_loop.sh new file mode 100755 index 0000000..60fe812 --- /dev/null +++ b/tools/ai/zeroclaw_hw_firmware_loop.sh @@ -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 < [--env ] [--port ] [--baud ] [--monitor-secs ] + +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)." diff --git a/tools/ai/zeroclaw_stack_down.sh b/tools/ai/zeroclaw_stack_down.sh index 605ef0a..93e9bc0 100755 --- a/tools/ai/zeroclaw_stack_down.sh +++ b/tools/ai/zeroclaw_stack_down.sh @@ -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/." diff --git a/tools/ai/zeroclaw_stack_up.sh b/tools/ai/zeroclaw_stack_up.sh index 3296069..ee6ce68 100755 --- a/tools/ai/zeroclaw_stack_up.sh +++ b/tools/ai/zeroclaw_stack_up.sh @@ -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" < /conversations.jsonl /gateway.log + /realtime_1min.log /prometheus.yml /health /metrics @@ -576,9 +676,14 @@ cat >"$INDEX_FILE" < 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 diff --git a/tools/ai/zeroclaw_watch_1min.sh b/tools/ai/zeroclaw_watch_1min.sh new file mode 100755 index 0000000..ffa9f13 --- /dev/null +++ b/tools/ai/zeroclaw_watch_1min.sh @@ -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 diff --git a/tools/ai/zeroclaw_webhook_send.sh b/tools/ai/zeroclaw_webhook_send.sh index 65fb78e..de2d2b9 100755 --- a/tools/ai/zeroclaw_webhook_send.sh +++ b/tools/ai/zeroclaw_webhook_send.sh @@ -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"