From 1e362c1a1466414bb040db83084915546f4e43a3 Mon Sep 17 00:00:00 2001 From: kxkm Date: Fri, 27 Mar 2026 03:44:22 +0100 Subject: [PATCH] docs+ci: update README features summary, fix intelligence TUI health probe, add KiCad exports workflow Co-Authored-By: Claude Sonnet 4.6 --- .github/workflows/kicad-exports.yml | 308 ++++++++++++++++++++++++++++ README.md | 5 + tools/cockpit/intelligence_tui.sh | 48 +++-- 3 files changed, 345 insertions(+), 16 deletions(-) create mode 100644 .github/workflows/kicad-exports.yml diff --git a/.github/workflows/kicad-exports.yml b/.github/workflows/kicad-exports.yml new file mode 100644 index 0000000..d0bf44b --- /dev/null +++ b/.github/workflows/kicad-exports.yml @@ -0,0 +1,308 @@ +name: KiCad Exports + +on: + push: + branches: + - main + paths: + - "hardware/**" + - "tools/hw/**" + - ".github/workflows/kicad-exports.yml" + pull_request: + paths: + - "hardware/**" + - "tools/hw/**" + - ".github/workflows/kicad-exports.yml" + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +env: + ARTIFACTS_DIR: artifacts/hw_exports + +jobs: + kicad-checks: + runs-on: ubuntu-latest + timeout-minutes: 20 + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Install KiCad 10 + run: | + sudo add-apt-repository -y ppa:kicad/kicad-10.0-releases + sudo apt-get update + sudo apt-get install -y --no-install-recommends kicad + kicad-cli version + + - name: Setup Python + uses: actions/setup-python@v5 + with: + python-version: "3.11" + + - name: Prepare output directories + run: | + mkdir -p "$ARTIFACTS_DIR/erc" + mkdir -p "$ARTIFACTS_DIR/drc" + mkdir -p "$ARTIFACTS_DIR/pdf" + mkdir -p "$ARTIFACTS_DIR/svg" + mkdir -p "$ARTIFACTS_DIR/bom" + mkdir -p "$ARTIFACTS_DIR/netlist" + + # ── Discover all KiCad projects ────────────────────────────── + - name: Discover schematics and PCBs + id: discover + run: | + # Find all .kicad_pro files (each is a project root) + SCHEMATICS=$(find hardware -name "*.kicad_sch" \ + -not -path "*/.kicad_blocks/*" \ + -not -path "*/kicad_blocks/*" \ + -not -path "*backup*" | sort) + PCBS=$(find hardware -name "*.kicad_pcb" | sort) + + echo "Found schematics:" + echo "$SCHEMATICS" + echo "" + echo "Found PCBs:" + echo "${PCBS:-}" + + # Also check tools/cad proof fixtures + PROOF_PCBS=$(find tools/cad -name "*.kicad_pcb" 2>/dev/null | sort || true) + if [[ -n "$PROOF_PCBS" ]]; then + echo "" + echo "Found proof-fixture PCBs:" + echo "$PROOF_PCBS" + PCBS=$(printf "%s\n%s" "$PCBS" "$PROOF_PCBS" | sed '/^$/d') + fi + + # Export for later steps + { + echo "schematics<> "$GITHUB_OUTPUT" + + # ── ERC (Electrical Rules Check) ───────────────────────────── + - name: Run ERC on all schematics + id: erc + run: | + EXIT_CODE=0 + while IFS= read -r SCH; do + [[ -z "$SCH" ]] && continue + BASENAME="$(basename "$SCH" .kicad_sch)" + echo "::group::ERC $SCH" + if kicad-cli sch erc "$SCH" \ + --format json \ + --output "$ARTIFACTS_DIR/erc/${BASENAME}_erc.json" 2>&1; then + # Parse error count + ERRORS=$(python3 -c " + import json, sys + try: + d = json.load(open('$ARTIFACTS_DIR/erc/${BASENAME}_erc.json')) + sheets = d.get('sheets', []) + errs = sum(1 for s in sheets for v in s.get('violations', []) if v.get('severity') == 'error') + print(errs) + except Exception as e: + print(f'parse-error: {e}', file=sys.stderr) + print(0) + ") + if [[ "$ERRORS" -gt 0 ]]; then + echo "::error file=$SCH::ERC found $ERRORS error(s)" + EXIT_CODE=1 + else + echo "ERC passed: $SCH" + fi + else + echo "::warning file=$SCH::ERC command failed (kicad-cli error)" + EXIT_CODE=1 + fi + echo "::endgroup::" + done <<< "${{ steps.discover.outputs.schematics }}" + echo "erc_exit=$EXIT_CODE" >> "$GITHUB_OUTPUT" + + # ── DRC (Design Rules Check) ───────────────────────────────── + - name: Run DRC on all PCBs + id: drc + run: | + EXIT_CODE=0 + while IFS= read -r PCB; do + [[ -z "$PCB" ]] && continue + BASENAME="$(basename "$PCB" .kicad_pcb)" + echo "::group::DRC $PCB" + if kicad-cli pcb drc "$PCB" \ + --format json \ + --output "$ARTIFACTS_DIR/drc/${BASENAME}_drc.json" 2>&1; then + ERRORS=$(python3 -c " + import json, sys + try: + d = json.load(open('$ARTIFACTS_DIR/drc/${BASENAME}_drc.json')) + errs = sum(1 for v in d.get('violations', []) if v.get('severity') == 'error') + print(errs) + except Exception as e: + print(f'parse-error: {e}', file=sys.stderr) + print(0) + ") + if [[ "$ERRORS" -gt 0 ]]; then + echo "::error file=$PCB::DRC found $ERRORS error(s)" + EXIT_CODE=1 + else + echo "DRC passed: $PCB" + fi + else + echo "::warning file=$PCB::DRC command failed (kicad-cli error)" + EXIT_CODE=1 + fi + echo "::endgroup::" + done <<< "${{ steps.discover.outputs.pcbs }}" + echo "drc_exit=$EXIT_CODE" >> "$GITHUB_OUTPUT" + + # ── PDF Schematic Export ───────────────────────────────────── + - name: Export schematic PDFs + run: | + while IFS= read -r SCH; do + [[ -z "$SCH" ]] && continue + BASENAME="$(basename "$SCH" .kicad_sch)" + echo "Exporting PDF: $SCH" + kicad-cli sch export pdf "$SCH" \ + --output "$ARTIFACTS_DIR/pdf/${BASENAME}.pdf" 2>&1 \ + || echo "::warning file=$SCH::PDF export failed" + done <<< "${{ steps.discover.outputs.schematics }}" + + # ── SVG Schematic Export ───────────────────────────────────── + - name: Export schematic SVGs + run: | + while IFS= read -r SCH; do + [[ -z "$SCH" ]] && continue + BASENAME="$(basename "$SCH" .kicad_sch)" + SVGDIR="$ARTIFACTS_DIR/svg/${BASENAME}" + mkdir -p "$SVGDIR" + echo "Exporting SVG: $SCH" + kicad-cli sch export svg "$SCH" \ + --output "$SVGDIR/" 2>&1 \ + || echo "::warning file=$SCH::SVG export failed" + done <<< "${{ steps.discover.outputs.schematics }}" + + # ── BOM Export ─────────────────────────────────────────────── + - name: Export BOMs + run: | + while IFS= read -r SCH; do + [[ -z "$SCH" ]] && continue + BASENAME="$(basename "$SCH" .kicad_sch)" + echo "Exporting BOM: $SCH" + kicad-cli sch export bom "$SCH" \ + --output "$ARTIFACTS_DIR/bom/${BASENAME}_bom.csv" \ + --fields 'Reference,Value,Footprint,Datasheet,Manufacturer,MPN,${QUANTITY},${DNP}' \ + --group-by "Value" \ + --sort-field "Reference" 2>&1 \ + || echo "::warning file=$SCH::BOM export failed" + done <<< "${{ steps.discover.outputs.schematics }}" + + # ── Netlist Export ─────────────────────────────────────────── + - name: Export netlists + run: | + while IFS= read -r SCH; do + [[ -z "$SCH" ]] && continue + BASENAME="$(basename "$SCH" .kicad_sch)" + echo "Exporting netlist: $SCH" + kicad-cli sch export netlist "$SCH" \ + --output "$ARTIFACTS_DIR/netlist/${BASENAME}_netlist.xml" 2>&1 \ + || echo "::warning file=$SCH::Netlist export failed" + done <<< "${{ steps.discover.outputs.schematics }}" + + # ── Summary ────────────────────────────────────────────────── + - name: Generate summary + if: always() + run: | + echo "## KiCad Export Summary" >> "$GITHUB_STEP_SUMMARY" + echo "" >> "$GITHUB_STEP_SUMMARY" + + # ERC results + echo "### ERC Reports" >> "$GITHUB_STEP_SUMMARY" + for f in "$ARTIFACTS_DIR"/erc/*.json; do + [[ -f "$f" ]] || continue + NAME="$(basename "$f" _erc.json)" + ERRORS=$(python3 -c " + import json + d = json.load(open('$f')) + sheets = d.get('sheets', []) + errs = sum(1 for s in sheets for v in s.get('violations', []) if v.get('severity') == 'error') + warns = sum(1 for s in sheets for v in s.get('violations', []) if v.get('severity') == 'warning') + print(f'{errs} errors, {warns} warnings') + " 2>/dev/null || echo "parse error") + echo "- **$NAME**: $ERRORS" >> "$GITHUB_STEP_SUMMARY" + done + + # DRC results + echo "" >> "$GITHUB_STEP_SUMMARY" + echo "### DRC Reports" >> "$GITHUB_STEP_SUMMARY" + for f in "$ARTIFACTS_DIR"/drc/*.json; do + [[ -f "$f" ]] || continue + NAME="$(basename "$f" _drc.json)" + ERRORS=$(python3 -c " + import json + d = json.load(open('$f')) + errs = sum(1 for v in d.get('violations', []) if v.get('severity') == 'error') + warns = sum(1 for v in d.get('violations', []) if v.get('severity') == 'warning') + print(f'{errs} errors, {warns} warnings') + " 2>/dev/null || echo "parse error") + echo "- **$NAME**: $ERRORS" >> "$GITHUB_STEP_SUMMARY" + done + + # Exports + echo "" >> "$GITHUB_STEP_SUMMARY" + echo "### Exported Artifacts" >> "$GITHUB_STEP_SUMMARY" + echo "- PDFs: $(find "$ARTIFACTS_DIR/pdf" -name "*.pdf" 2>/dev/null | wc -l | tr -d ' ') files" >> "$GITHUB_STEP_SUMMARY" + echo "- SVGs: $(find "$ARTIFACTS_DIR/svg" -name "*.svg" 2>/dev/null | wc -l | tr -d ' ') files" >> "$GITHUB_STEP_SUMMARY" + echo "- BOMs: $(find "$ARTIFACTS_DIR/bom" -name "*.csv" 2>/dev/null | wc -l | tr -d ' ') files" >> "$GITHUB_STEP_SUMMARY" + echo "- Netlists: $(find "$ARTIFACTS_DIR/netlist" -name "*.xml" 2>/dev/null | wc -l | tr -d ' ') files" >> "$GITHUB_STEP_SUMMARY" + + # ── Upload Artifacts ───────────────────────────────────────── + - name: Upload ERC/DRC reports + if: always() + uses: actions/upload-artifact@v4 + with: + name: kicad-reports + if-no-files-found: warn + path: | + artifacts/hw_exports/erc/ + artifacts/hw_exports/drc/ + + - name: Upload schematic exports + if: always() + uses: actions/upload-artifact@v4 + with: + name: kicad-schematics + if-no-files-found: warn + path: | + artifacts/hw_exports/pdf/ + artifacts/hw_exports/svg/ + + - name: Upload BOM and netlists + if: always() + uses: actions/upload-artifact@v4 + with: + name: kicad-bom-netlist + if-no-files-found: warn + path: | + artifacts/hw_exports/bom/ + artifacts/hw_exports/netlist/ + + # ── Fail if ERC or DRC had errors ──────────────────────────── + - name: Check ERC/DRC results + if: always() + run: | + ERC_EXIT="${{ steps.erc.outputs.erc_exit }}" + DRC_EXIT="${{ steps.drc.outputs.drc_exit }}" + if [[ "${ERC_EXIT:-0}" -ne 0 ]] || [[ "${DRC_EXIT:-0}" -ne 0 ]]; then + echo "::error::ERC or DRC checks failed. Review the uploaded reports." + exit 1 + fi + echo "All ERC/DRC checks passed." diff --git a/README.md b/README.md index 99f7fef..7ca95f6 100644 --- a/README.md +++ b/README.md @@ -13,6 +13,8 @@ Welcome to **Kill_LIFE**, the public control plane of the `Kill_LIFE` agentic program. The repo now concentrates the operator cockpit, the spec-first pipeline, runtime/MCP contracts, execution evidence, and the pilot batch that powers sister VS Code extensions `kill-life-studio`, `kill-life-mesh`, and `kill-life-operator`. +*Last updated: 2026-03-27* + The 2026-03-22 reading rule is simple: - this `README.md` describes the product/program and consolidation decisions @@ -104,6 +106,9 @@ The positioning chosen for this consolidation pass is: - **Workflow catalog**: JSON workflows editable by `crazy_life`, validated against a JSON schema. - **Mascarade LLM Router**: Fake Ollama API, Agentic RAG, Cody Gateway, 5-machine fleet (Tower/KXKM/Photon/CILS/Local). - **10 MCP servers**: kicad, freecad, openscad, github-dispatch, knowledge-base, apify, huggingface, mascarade-llm, opcua, mqtt. +- **RAG pipeline**: Agentic RAG with Qdrant hybrid search, LLM reranking, CRAG fallback, SearXNG web search. +- **KiCad CI exports**: SVG, ERC, DRC, BOM, netlist -- headless via KiCad 10 MCP. +- **Firmware Gate S1**: PlatformIO build gate for ESP32/STM32 targets with evidence pack generation. - **Factory 4.0**: Industrial agents (copilot, maintenance predictor, log analyst), OPC-UA/MQTT MCP servers. - **EDA AI tools**: PCBDesigner, Quilter, KiCadHappy providers for automated PCB design + fabrication. - **Project template**: `templates/kill-life-project/` scaffold for client repos (see `docs/PROJECT_TEMPLATE.md`). diff --git a/tools/cockpit/intelligence_tui.sh b/tools/cockpit/intelligence_tui.sh index 023263c..3332393 100755 --- a/tools/cockpit/intelligence_tui.sh +++ b/tools/cockpit/intelligence_tui.sh @@ -167,54 +167,70 @@ def parse_all_open_tasks(text: str) -> list[str]: def web_platform_health() -> dict[str, object]: - """Probe Next.js, Yjs realtime server, and Redis/BullMQ queue.""" + """Probe Next.js, Yjs realtime server, and Redis/BullMQ queue. + + Primary: calls /api/ops/platform on the running Next.js app (T-AI-323). + Fallback: direct socket probes when the app is not running. + """ + import json import urllib.request + platform_url = "http://localhost:3000/api/ops/platform" + try: + req = urllib.request.urlopen(platform_url, timeout=4) + data = json.loads(req.read()) + # Normalise probe keys for downstream consumers + probes = data.get("probes", {}) + return { + "status": data.get("status", "unknown"), + "probes": { + "nextjs": probes.get("next-js", {"status": "unknown"}), + "realtime": probes.get("yjs-realtime", {"status": "unknown"}), + "queue": probes.get("eda-queue", {"status": "unknown"}), + }, + "up_count": data.get("up_count", 0), + "total": data.get("total", 3), + "source": "platform-api", + } + except Exception: + pass # fall through to direct probes + + # --- Fallback: direct probes (app not running) --- probes: dict[str, object] = {} - # Next.js web app try: req = urllib.request.urlopen("http://localhost:3000/", timeout=3) probes["nextjs"] = {"status": "up", "http_status": req.status} except Exception as exc: probes["nextjs"] = {"status": "down", "error": str(exc)[:120]} - # Yjs realtime server (default port 1234, see web/realtime/server.mjs) try: req = urllib.request.urlopen("http://localhost:1234/", timeout=3) probes["realtime"] = {"status": "up", "http_status": req.status} except Exception as exc: probes["realtime"] = {"status": "down", "error": str(exc)[:120]} - # Redis / BullMQ queue depth try: import socket sock = socket.create_connection(("127.0.0.1", 6379), timeout=2) sock.sendall(b"LLEN bull:yiacad-eda:wait\r\n") data = sock.recv(256).decode("utf-8", errors="replace").strip() sock.close() - if data.startswith(":"): - depth = int(data[1:]) - probes["queue"] = {"status": "up", "queue_name": "yiacad-eda", "depth": depth} - else: - probes["queue"] = {"status": "up", "queue_name": "yiacad-eda", "depth": None, "raw": data[:80]} + depth = int(data[1:]) if data.startswith(":") else None + probes["queue"] = {"status": "up", "queue_name": "yiacad-eda", "depth": depth} except Exception as exc: probes["queue"] = {"status": "down", "error": str(exc)[:120]} - up_count = sum(1 for p in probes.values() if p.get("status") == "up") + up_count = sum(1 for p in probes.values() if isinstance(p, dict) and p.get("status") == "up") total = len(probes) - if up_count == total: - overall = "up" - elif up_count == 0: - overall = "down" - else: - overall = "degraded" + overall = "up" if up_count == total else ("down" if up_count == 0 else "degraded") return { "status": overall, "probes": probes, "up_count": up_count, "total": total, + "source": "direct-probes", }