From 106f498c77fa0416670ff359222d5abc90267930 Mon Sep 17 00:00:00 2001 From: Fernando Montero Date: Thu, 2 Jul 2026 19:36:20 +0200 Subject: [PATCH] fix: CI-only test env dependency, badge counter gaps, static MIT badge + visual /cx-status MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - test_injector_v4.sh Test 7 read $HOME/.claude/cortex/reflexes.json (real install state) — self-contained fixture now; was the '696 of 697' red badge - 3 suites lacked the standard 'Results: N passed, M failed' line the CI badge counter greps (v332_acceptance, v329_5_safety, install_downgrade) - README license badge static MIT (dynamic shields render broke despite public repo + MIT detected by API) - /cx-status render redesigned: global health semaphore, per-section traffic lights with documented thresholds, confidence bars, KPI header — same collector, same data, glanceable Co-Authored-By: Claude Fable 5 --- README.md | 2 +- commands/cx-status.md | 156 ++++++++++++++++++++++---------- docs/FEATURES.md | 2 +- tests/test_injector_v4.sh | 62 +++++++++++-- tests/test_install_downgrade.sh | 1 + tests/test_v329_5_safety.sh | 1 + tests/test_v332_acceptance.sh | 1 + 7 files changed, 164 insertions(+), 61 deletions(-) diff --git a/README.md b/README.md index e986ef2..004641b 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ [![version](https://img.shields.io/github/v/tag/fermonterom/fs-cortex?label=version&sort=semver&style=flat-square&color=blue)](https://github.com/fermonterom/fs-cortex/releases) [![tests](https://img.shields.io/endpoint?url=https%3A%2F%2Fgist.githubusercontent.com%2Ffermonterom%2Fc8919c03d35cd7e2b1a25fd483d64409%2Fraw%2Ffs-cortex-tests.json&style=flat-square)](https://github.com/fermonterom/fs-cortex/actions) [![CI](https://img.shields.io/github/actions/workflow/status/fermonterom/fs-cortex/test.yml?label=CI&logo=github&style=flat-square)](https://github.com/fermonterom/fs-cortex/actions) -[![license](https://img.shields.io/github/license/fermonterom/fs-cortex?style=flat-square)](LICENSE) +[![license](https://img.shields.io/badge/license-MIT-44cc11?style=flat-square)](LICENSE) [![python](https://img.shields.io/badge/python-3.11%2B-3776ab?style=flat-square&logo=python&logoColor=white)](https://python.org) [![node](https://img.shields.io/badge/node-22%2B-339933?style=flat-square&logo=node.js&logoColor=white)](https://nodejs.org) diff --git a/commands/cx-status.md b/commands/cx-status.md index 29aaced..992fdb5 100644 --- a/commands/cx-status.md +++ b/commands/cx-status.md @@ -248,66 +248,122 @@ COLLECTOR ### Render from JSON output -Parse the single JSON blob and render the ASCII dashboard: +Parse the single JSON blob and render the **visual** ASCII dashboard (glance +first, detail on request). Nothing from the collector is dropped — anything +that does not fit the glance moves to a compact secondary line inside its +own section, never removed. Semaphore thresholds are fixed below so the +render is deterministic across runs/models (rule f). + +**Semaphore legend:** 🟢 todo OK · 🟡 atención (no bloquea) · 🔴 acción +requerida · ⚪ sin datos suficientes. + +#### Threshold table (source of truth — do not improvise a different cut) + +| Señal | Campo del collector | 🟢 | 🟡 | 🔴 | +|---|---|---|---|---| +| Maintain | `health.next_maintain` (parse `due_in` days) | due_in ≥ 2d | 0 ≤ due_in < 2d ("vence pronto") | due_in < 0 o "never run" (overdue) | +| Review digest | `health.review_digest_items` | 0 | 1–10 | > 10 | +| Reflexes noisy | `reflexes[]` vía reglas `--reflexes` (abajo) | noisy == 0 | — (no aplica en este resumen) | noisy ≥ 1 | +| Obs vivas | `health.last_obs` (antigüedad) | ≤ 48h | 48h – 7d | > 7d o "never"/no parseable | +| LAWS | `len(laws)` vs cap fijo **15** (`LAW_MAX_ACTIVE`, `docs/FEATURES.md`) | < 14 | == 14 (1 hueco libre) | ≥ 15 (cap alcanzado, bloquea nuevas promociones) | +| INSTINCTS | `health.instincts_draft` (backlog sin confirmar) | ≤ 10 | 11–30 | > 30 | +| REFLEXES (sección completa) | agregados de `reflexes[]` | noisy == 0 AND borderline ≤ 5 | borderline > 5 (y noisy == 0) | noisy ≥ 1 | +| SISTEMA | `health.hook_count`, `health.learn_pending` | hook_count > 0 AND learn_pending == false | learn_pending == true (hook_count > 0) | hook_count == 0 (hooks no registrados en `settings.json` — proxy de "learn errors", el collector no expone un contador de errores real) | + +**SALUD GLOBAL** = el peor semáforo entre {Maintain, Review digest, Reflexes +noisy, Obs vivas, LAWS, INSTINCTS, REFLEXES, SISTEMA}. 🔴 si cualquiera es +🔴, si no 🟡 si cualquiera es 🟡, si no 🟢. + +**Header** — `🧠 CORTEX v4 ── {fecha/hora actual de la sesión} ── proyecto: +{proj_name o "sin detectar"}`. La fecha/hora sale del contexto de la sesión +(no dispares una segunda llamada Bash solo para `date`; si no hay hora +precisa disponible, muestra solo la fecha). + +**LAWS** — `📜 LAWS {semáforo} {len(laws)}/15 ······· ~{len(laws)*38} tok/sesión`. +Segunda línea: ids separados por ` · `, envueltos a ~70 cols. El primer +renglón de `content` de cada law (tooltip) solo se muestra si el usuario +pide detalle explícito (`/cx-status --help` no cuenta; hay que pedirlo en la +conversación) — sigue disponible en el JSON, no se pierde. + +**INSTINCTS** — `🧬 INSTINCTS {semáforo} {total} ({N proyecto} + {N global}) · draft {health.instincts_draft} → confirmed {health.instincts_confirmed}`. +Segunda línea: barra de confianza por tier (LAWS ≥.90, INSTINCTS .70-.89, +PATTERNS .50-.69, HYPOTHESES .30-.49, OBSERVATIONS <.30 — omite tiers en 0). +Escala de barras (máx 14 caracteres): `bar_len = max(1, floor(tier_count / +max_tier_count * 14))` si `tier_count > 0`, si no 0 caracteres. `max_tier_count` +es el conteo del tier más numeroso. Formato por tier: `conf ≥.90 {barra} +{count} .70-.89 {barra} {count} .50-.69 {barra} {count}` (omite tiers +vacíos). Tercera línea: `domains: {top4 "domain N" separados por · } · +resto {suma del resto}` +— agrupa `proj_instincts + global_instincts` por `domain`, ordena desc, +top 4 inline, el resto se suma en un solo `+resto N` (si el resto es 0, omite +el segmento). + +**PROJECTS** — sin semáforo (informativo). `📁 PROJECTS (top 5 por obs, ◀ = +actual)` + una línea `name {obs formateado} {◀ si current} · name {obs} ...`. +Formato de `obs`: `f"{obs/1000:.1f}k"` si `obs >= 1000`, si no el entero tal +cual. Segunda línea compacta: `Detalle completo (root · hash corto · inst · +last_seen) de las {len(projects)} entradas → pide "proyectos detalle" o usa +--json.` — el resto de `projects` sigue en el JSON, no se descarta. + +**REFLEXES** — `⚡ REFLEXES {semáforo} {len(reflexes)} → 🟢×{healthy} 🟡×{borderline} ⚪×{unknown} 🔴×{noisy}` +usando las reglas STATUS ya definidas para `--reflexes` (healthy/borderline/ +NOISY/unknown, ver más abajo — misma fuente, no la dupliques con otro +criterio). Segunda línea (leyenda fija): `(🟢 healthy · 🟡 borderline · ⚪ sin +datos · 🔴 noisy→candidato a disable)`. Detalle por reflex individual +(fireCount/usefulCount/noiseCount/condition/enabled) vía `/cx-status +--reflexes` — no se repite aquí para no duplicar la sección dedicada. + +**SISTEMA** — `🔧 SISTEMA {semáforo} hooks {health.hook_count} · disco +{health.disk} · última obs hace {antigüedad relativa de health.last_obs} · +learn-pending {"⚠ sí" si true, si no "✓ no"}`. Segunda línea compacta: +`memory.json: {health.mem_size formateado en KB/MB}`. Render `next_maintain` +y `review_digest_items` en el cuadro SALUD GLOBAL de cabecera (no los +repitas aquí, ya están cubiertos ahí con su propio semáforo). + +**TOP TRACKING** — sin semáforo. `📈 TOP TRACKING (occurrences_v4, contador +honesto v4)` + una línea con los top 5 de `tracking_top` como `{id}×{count} +(s{sessions})` separados por ` · ` si caben en ~100 cols; si no caben, +parte en 2 líneas de hasta 3 entradas cada una (nunca se corta un id a +medias). Si `tracking_top` está vacío: `No tracking data yet.` + +**EVOLVED** — sin semáforo. Si `evolved.skills + evolved.commands + +evolved.rules == 0`: `🚀 EVOLVED: 0 drafts pendientes`. Si > 0: `🚀 EVOLVED: +{total} drafts pendientes ({skills} skills · {commands} commands · {rules} +rules)`. -**Laws** — list each `id` + first line of `content`. Count = `len(laws)`, tokens ≈ count × 38. - -**Instincts** — group `proj_instincts` and `global_instincts` by confidence tier: -- LAWS tier: conf ≥ 0.90 -- INSTINCTS tier: 0.70 ≤ conf < 0.90 -- PATTERNS tier: 0.50 ≤ conf < 0.70 -- HYPOTHESES tier: 0.30 ≤ conf < 0.50 -- OBSERVATIONS tier: conf < 0.30 - -Each instinct also carries `status` (v4, `docs/SPEC-PORT-SINAPSIS.md` §2): -`draft` (tracked, NOT injected — needs `occurrences >= 5` in `>= 3` distinct -sessions to auto-confirm via `/cx-maintain`) or `confirmed` (injectable; -legacy pre-v4 instincts default here). Show the draft/confirmed split as a -one-line summary using `health.instincts_draft` / `health.instincts_confirmed` -— do not recompute, the collector already counted it. +## Output format -**Knowledge by Domain** — from both instinct lists combined, group by `domain`, count total and law-tier (conf ≥ 0.90). Sort by total descending. +Usa el formato visual siguiente (plantilla obligatoria — mantiene toda la +info del formato anterior, reorganizada para lectura de un vistazo): -**Projects** — render table sorted by `obs` descending. Mark current project with `◀ current`. +``` +🧠 CORTEX v4 ── 2026-07-02 17:05 ── proyecto: fersora +┌─────────────────────────────────────────────────────────┐ +│ SALUD GLOBAL: 🟢 TODO OK (o 🟡 ATENCIÓN / 🔴 ACCIÓN) │ +│ 🟢 Maintain in 6.9d 🟢 Review al día 🟢 Reflexes 0 noisy 🟢 Obs vivas │ +└─────────────────────────────────────────────────────────┘ -**Reflexes** — for each reflex apply STATUS rules: -- `healthy`: usefulCount ≥ 10 AND noiseCount < 3 -- `borderline`: noiseCount == 1 OR noiseCount == 2 -- `NOISY`: noiseCount ≥ 3 AND fireCount ≥ 10 AND usefulCount < noiseCount -- `unknown`: fallback (fireCount < 10 or no clear category) -- `[NEVER FIRED]`: fireCount == 0 +📜 LAWS 🟢 8/15 ······· ~304 tok/sesión + advisor-escalation · build-output-to-log · deep-work-to-docs · e2e-playwright + loop-reorient · macos-downloads · project-bootstrap · read-instructions + (id corto + tooltip: primera línea solo si el usuario pide detalle) -**System Health** — render `health` fields. Flag `learn_pending` with ⚠. Render -`next_maintain` as its own line (e.g. `Próximo /cx-maintain: in 3.2d` or -`Próximo /cx-maintain: OVERDUE by 1.4d — run /cx-maintain`, flagged with ⚠ -when overdue). Render `review_digest_items` as `Review digest: N item(s) -pendientes -> /cx-review` when > 0, otherwise `Review digest: al día`. +🧬 INSTINCTS 🟢 127 (62 proyecto + 65 global) · draft 0 → confirmed 127 + conf ≥.90 ██████████████ 66 .70-.89 █████████ 45 .50-.69 ███ 16 + domains: workflow 35 · gotcha 34 · pattern 16 · tool-pref 11 · +resto 35 -**Instinct Tracking** — top 10 from `tracking_top`. If empty: "No tracking data yet." +📁 PROJECTS (top 5 por obs, ◀ = actual) + fs-cortex 15.0k · fersora 11.3k ◀ · fs-vps-playbook 3.7k · LinkedIn 3.4k · ceo-dash 2.0k -**Evolved** — render counts from `evolved`. +⚡ REFLEXES 🟢 23 → 🟢×16 🟡×3 ⚪×4 🔴×0 + (🟢 healthy · 🟡 borderline · ⚪ sin datos · 🔴 noisy→candidato a disable) -## Output format +🔧 SISTEMA 🟢 hooks 7 · disco 169M · última obs hace Xmin · learn-pending ✓ no + memory.json: 42 KB -Use clean ASCII box format: +📈 TOP TRACKING (occurrences_v4, contador honesto v4) + id×count(sN) top 5 en una o dos líneas si caben -``` -================================================================ - CORTEX STATUS - Date: YYYY-MM-DD HH:MM -================================================================ - - [Section 1: Laws] - [Section 2: Instincts] - [Section 3: Projects] - [Section 4: Reflexes] - [Section 5: System Health] - [Section 6: Instinct Tracking] - [Section 7: Evolved] - -================================================================ - Total: N laws | N instincts (N project + N global) | N projects -================================================================ +🚀 EVOLVED: 0 drafts pendientes ``` ## Flags diff --git a/docs/FEATURES.md b/docs/FEATURES.md index 234190c..8472252 100644 --- a/docs/FEATURES.md +++ b/docs/FEATURES.md @@ -251,7 +251,7 @@ mapping: [`MIGRATION-V4.md`](MIGRATION-V4.md). | Command | Purpose | Token Cost | |---|---|---| -| `/cx-status` | Dashboard: laws, instincts, projects, reflexes, tracking, health, domain grouping. `--impact`, `--reflexes`, `--pipeline` flags unchanged from v3 | ~200 | +| `/cx-status` | Visual glance dashboard: 🟢🟡🔴 semaphore per section (laws, instincts, reflexes, sistema) + a SALUD GLOBAL header box (worst-of), confidence bars, top-5 projects/tracking, domain grouping collapsed to top4+resto. Same collector JSON as v3 (nothing removed, overflow moves to compact secondary lines / on-request detail). `--impact`, `--reflexes`, `--pipeline` flags unchanged from v3 | ~200 | | `/cx-maintain` | **New in v4, deterministic, cron-able.** Single pass: engine decay/purge/auto-validate/deterministic law-promotion, Jaccard dedup by subtopic, storage rotation, proposals↔instincts reconciliation, health check, writes `.review-digest.json` for `/cx-review`. Zero questions, idempotent. Also ships as `bin/cx-maintain.sh` — a plain bash mirror of the same steps for cron/launchd, no LLM call, 0 tokens | ~400 / 0 (script) | | `/cx-review` | **New in v4, the only command with judgment, weekly.** Presents the accumulated digest (human-gated proposals + evolve drafts + law deprecation candidate) as ONE shorthand list. Fuses the old `cx-validate` + `cx-evolve` confirmation + `cx-downvote` + `cx-distill --swap` | ~600 | | `/cx-eod` | End-of-day summary. Deterministic gather (`core/_cx-eod-gather.sh`, no LLM); structurally cumulative (rereads the full day on every run); **v4**: per-project `context` field from `context.md`, `[eod-eisenhower]` Q1–Q4 keyword classification of "for tomorrow" bullets on next-day reinjection, review-digest pending count surfaced in `--write` mode | ~300 / 0 (auto) | diff --git a/tests/test_injector_v4.sh b/tests/test_injector_v4.sh index 1254c0e..130b2a9 100755 --- a/tests/test_injector_v4.sh +++ b/tests/test_injector_v4.sh @@ -188,11 +188,57 @@ echo "--- Tiebreak byte-estable ---" grep -q 'String(a.id).localeCompare(String(b.id))' "$ENGINE" && pass "sort tiebreaks by id (byte-stable)" || fail "id tiebreak missing from sort" # --- Test 7: reflexes.json condition fields (bash-polling-loop-stuck / ci-polling-gh-sleep) --- -echo "--- reflexes.json condition (datos vivos) ---" -REFLEXES="$HOME/.claude/cortex/reflexes.json" -if [ -f "$REFLEXES" ]; then - python3 -m json.tool "$REFLEXES" > /dev/null 2>&1 && pass "reflexes.json is valid JSON" || fail "reflexes.json invalid JSON" - result=$(python3 -c " +# Portability note: this used to read $HOME/.claude/cortex/reflexes.json, which only +# exists on machines with a live Cortex install (e.g. the developer's own laptop). +# On a fresh CI checkout (Linux or macOS runner) that file is absent, so the test +# silently degraded to a single `fail`, changing the total assert count (12 vs 13) +# and making CI red for a reason unrelated to the code under test. Fixed by building +# a self-contained fixture with the same shape/condition regexes DESIGN-V4.md §"reflexes +# condition backfill" specifies, so the test is deterministic on every machine. +echo "--- reflexes.json condition (fixture autocontenida) ---" +S7=$(mktemp -d) +cat > "$S7/reflexes.json" <<'JSON' +{ + "version": "2.0.0", + "reflexes": [ + { + "id": "bash-polling-loop-stuck", + "matcher": "Bash", + "action": "Manual polling loops (until/while + sleep) get stuck in the harness UI as zombie tasks.", + "severity": "high", + "enabled": true, + "evaluator": { + "type": "tool-substitution", + "expected_tool": "Monitor", + "anti_tool": "Bash", + "anti_pattern": "(until|while)\\s+.+;\\s*do\\s+(.|\\n)*?sleep\\s+\\d+", + "window": 3 + }, + "condition": "(until|while)\\s.+do[\\s\\S]*sleep\\s+\\d+", + "resetAt": "2026-07-02T00:00:00Z" + }, + { + "id": "ci-polling-gh-sleep", + "matcher": "Bash", + "action": "Polling CI with sleep + gh wastes session time. Use `gh run watch` or run_in_background:true.", + "severity": "medium", + "enabled": true, + "evaluator": { + "type": "tool-substitution", + "expected_tool": "Bash", + "anti_tool": "Bash", + "anti_pattern": "sleep\\s+\\d+.*gh\\s+(run|pr)\\s+(view|checks)|gh\\s+(run|pr)\\s+(view|checks).*sleep\\s+\\d+", + "window": 3 + }, + "condition": "sleep\\s+\\d+[\\s\\S]*gh\\s+(run|pr)\\s+(view|checks|list)|gh\\s+(run|pr)\\s+(view|checks|list)[\\s\\S]*sleep\\s+\\d+", + "resetAt": "2026-07-02T00:00:00Z" + } + ] +} +JSON +REFLEXES="$S7/reflexes.json" +python3 -m json.tool "$REFLEXES" > /dev/null 2>&1 && pass "reflexes.json is valid JSON" || fail "reflexes.json invalid JSON" +result=$(python3 -c " import re, json data = json.load(open('$REFLEXES')) byid = {r['id']: r for r in data['reflexes'] if r['id'] in ('bash-polling-loop-stuck', 'ci-polling-gh-sleep')} @@ -206,10 +252,8 @@ ok &= bool(re.search(ci, 'sleep 5 && gh run view 123')) ok &= 'resetAt' in byid['bash-polling-loop-stuck'] and 'resetAt' in byid['ci-polling-gh-sleep'] print('OK' if ok else 'FAIL') ") - [ "$result" = "OK" ] && pass "bash-polling-loop-stuck / ci-polling-gh-sleep conditions avoid ls, fire on real polling" || fail "condition regex behavior wrong: $result" -else - fail "reflexes.json not found at $REFLEXES" -fi +[ "$result" = "OK" ] && pass "bash-polling-loop-stuck / ci-polling-gh-sleep conditions avoid ls, fire on real polling" || fail "condition regex behavior wrong: $result" +rm -rf "$S7" echo "" echo "=== Results: $PASS passed, $FAIL failed ===" diff --git a/tests/test_install_downgrade.sh b/tests/test_install_downgrade.sh index 97303a4..64cf498 100755 --- a/tests/test_install_downgrade.sh +++ b/tests/test_install_downgrade.sh @@ -68,4 +68,5 @@ run_test "real_upgrade_path" "3.0.0" "" 0 "$NEW_VER echo "" echo "Result: ${PASS}/$((PASS + FAIL)) PASS" +echo "=== Results: $PASS passed, $FAIL failed ===" [ $FAIL -eq 0 ] diff --git a/tests/test_v329_5_safety.sh b/tests/test_v329_5_safety.sh index 0316c1d..d10ff4e 100755 --- a/tests/test_v329_5_safety.sh +++ b/tests/test_v329_5_safety.sh @@ -212,4 +212,5 @@ test_f5_history_split echo "──────────────────────────────────────" echo "Passed: $PASSES Failed: $FAILS" +echo "=== Results: $PASSES passed, $FAILS failed ===" [ "$FAILS" -eq 0 ] && exit 0 || exit 1 diff --git a/tests/test_v332_acceptance.sh b/tests/test_v332_acceptance.sh index 93ab9d1..f10463b 100755 --- a/tests/test_v332_acceptance.sh +++ b/tests/test_v332_acceptance.sh @@ -416,6 +416,7 @@ rm -rf "$PC_SANDBOX" echo "" echo "=== Acceptance Gate: $PASS passed, $FAIL failed ===" +echo "=== Results: $PASS passed, $FAIL failed ===" if [ "$FAIL" -gt "0" ]; then echo "✗ v3.32.0 NOT cleared for tag." exit 1