[codex] harden dual-hw autonomy defaults and valid pio env CI

This commit is contained in:
Clément SAILLANT
2026-02-21 02:58:21 +01:00
parent 0cb4c57e0f
commit 94e4da0e67
10 changed files with 158 additions and 26 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
+10 -4
View File
@@ -27,9 +27,10 @@ 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
@@ -37,13 +38,15 @@ Format:
- AC: au moins un port detecte pour carte RTC.
- Evidence: `tools/ai/zeroclaw_dual_chat.sh rtc --hardware`
- [ ] T-102 - Build firmware RTC
- [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 - 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.
@@ -55,13 +58,15 @@ Format:
- AC: au moins un port detecte pour carte Zacus.
- Evidence: `tools/ai/zeroclaw_dual_chat.sh zacus --hardware`
- [ ] T-202 - Build firmware Zacus
- [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 - 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.
@@ -73,9 +78,10 @@ Format:
- 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.
+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`
+3 -1
View File
@@ -21,11 +21,13 @@ Last updated: 2026-02-21
- `tools/ai/zeroclaw_dual_chat.sh rtc --hardware`
- `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`
- `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
@@ -36,7 +38,7 @@ Last updated: 2026-02-21
## 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`).
+25 -4
View File
@@ -112,11 +112,32 @@ try:
arr=json.loads(raw) if raw else []
except Exception:
arr=[]
best=None
best_score=-10**9
for d in arr:
p=d.get("port")
if p:
print(p)
raise SystemExit(0)
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)' || true)"
fi
+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"