Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
156 changes: 106 additions & 50 deletions commands/cx-status.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion docs/FEATURES.md
Original file line number Diff line number Diff line change
Expand Up @@ -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) |
Expand Down
62 changes: 53 additions & 9 deletions tests/test_injector_v4.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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')}
Expand All @@ -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 ==="
Expand Down
1 change: 1 addition & 0 deletions tests/test_install_downgrade.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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 ]
1 change: 1 addition & 0 deletions tests/test_v329_5_safety.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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
1 change: 1 addition & 0 deletions tests/test_v332_acceptance.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading