diff --git a/CHANGELOG.md b/CHANGELOG.md index 6822b5a..5bb2141 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,51 @@ All notable changes to fs-cortex will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). +## [4.2.0] — 2026-07-05 + +**Laws layer: de-dup, legibility, post-promotion audit.** Follow-up to the +question "are laws the right mechanism, or noise?" — investigated with measured +data + a 3-agent adversarial counter-analysis that refuted the invasive ideas +(shrinking, demoting gotchas, stopping auto-promotion) and confirmed the +surgical ones. Full rationale in `docs/DESIGN-laws-v4.2.md`. + +### Fixed +- **Double injection (measured bug)**: `read-instructions-before-executing` + (41 redundant PreToolUse injections) and `pref-fix-all-lint-test-issues` (17) + existed as both a law AND an active global instinct. `injector-engine.js` now + skips any instinct candidate whose id has a `laws/{id}.txt` twin, and the two + duplicate instincts were archived. Root cause fixed: `manual_swap_promote` + now archives the source instinct on promotion (like `auto_promote_to_law` + already did) — seed/manual paths previously left the duplicate behind. +- **`advisor-escalation` bloat**: trimmed 1505 → 717 chars (kept the 4 triggers + and exclusions, dropped the verbose mechanics). It was 26% of the whole law + budget; v4.1.0's full-text injection had surfaced it. +- **`bash-cat-use-read` regression (v4.1.0)**: the noise-reduction negative + lookahead `(?!\s*(\|<<))` also excluded legitimate targets — `cat file.py | + head`, `tail file.json | grep` — where Read/Grep IS the better tool, breaking + the `test_reflex_matchers` contract (piped source-file cat must still fire). + Reverted the lookahead in seed + live; the reflex fires on any recognised + source extension regardless of pipe, as the contract specifies. + +### Added +- **Law presentation split by tier** (`session-start.py` + `core/laws-meta.default.json`): + laws render in two labelled blocks — `[principios]` (behavioural) vs + `[herramienta]` (tool gotchas) — so principles don't get diluted by mechanical + gotchas. Driven by an optional `laws/laws-meta.json` `{id: {tier}}` map; + absent map or entry → single block / `principle` default (backward compatible). +- **`law_audit()` + `law-audit` CLI** (`distill_engine.py`): per-law + `{id, tier, age_days, dup_active_instinct, backing_instinct_noise}`, written to + `.law-audit.json`. Gives periodic legibility to prune laws by data — the honest + model for a curated constitution (measuring "law followed" is unsolvable without + a trigger, per the adversarial pass). + +### Notes +- Deliberately NOT done (adversarial refutation): domain-scoping laws (only 2-3 + of the "mechanical" laws are project-domain-specific; the rest are universal + tooling — new machinery unjustified for the payoff); demoting the 7 tool + gotchas to instincts (re-exposes them to trigger-miss); hand-curating laws down + to 5-7 (no evidence current content is bad; token cost is <0.5%). + ## [4.1.0] — 2026-07-04 **Audit hardening.** Fixes and hygiene from a multi-agent read-only audit of the diff --git a/core/laws-meta.default.json b/core/laws-meta.default.json new file mode 100644 index 0000000..a735a56 --- /dev/null +++ b/core/laws-meta.default.json @@ -0,0 +1,21 @@ +{ + "version": 1, + "_comment": "tier drives the SessionStart presentation split (principle vs tool). Absent id -> defaults to 'principle' (never hidden).", + "laws": { + "advisor-escalation": {"tier": "principle"}, + "deep-work-to-docs": {"tier": "principle"}, + "read-instructions-before-executing": {"tier": "principle"}, + "loop-reorient": {"tier": "principle"}, + "pref-fix-all-lint-test-issues": {"tier": "principle"}, + "gotcha-ad-por-fase-no-sustituye-e2e": {"tier": "principle"}, + "build-output-to-log": {"tier": "tool"}, + "cat-pipe-head-claudemd-anti": {"tier": "tool"}, + "gotcha-inline-python-readability": {"tier": "tool"}, + "macos-downloads-read-tool": {"tier": "tool"}, + "gotcha-mcp-server-wrong-config-file": {"tier": "tool"}, + "gotcha-webfetch-fallback-to-websearch": {"tier": "tool"}, + "gotcha-firecrawl-over-tavily-html": {"tier": "tool"}, + "e2e-playwright-selectors": {"tier": "tool"}, + "gotcha-api-route-error-message-leak": {"tier": "tool"} + } +} diff --git a/core/reflexes.default.json b/core/reflexes.default.json index a60e6d7..8fd4edd 100644 --- a/core/reflexes.default.json +++ b/core/reflexes.default.json @@ -140,7 +140,7 @@ { "id": "bash-cat-use-read", "matcher": "Bash", - "condition": "(?:^|[;&|]\\s*)(cat|head|tail)\\s+(-[0-9]+\\s+)?[\\.\\/~]?\\S*\\.(py|js|jsx|ts|tsx|md|json|jsonl|ya?ml|sh|html|css|toml|cfg|ini|conf|sql|env)\\b(?!\\s*(\\||<<))", + "condition": "(?:^|[;&|]\\s*)(cat|head|tail)\\s+(-[0-9]+\\s+)?[\\.\\/~]?\\S*\\.(py|js|jsx|ts|tsx|md|json|jsonl|ya?ml|sh|html|css|toml|cfg|ini|conf|sql|env)\\b", "action": "Use Read tool (with limit/offset) instead of cat/head/tail for source files.", "severity": "medium", "enabled": true, @@ -152,7 +152,7 @@ "type": "tool-substitution", "expected_tool": "Read", "anti_tool": "Bash", - "anti_pattern": "(?:^|[;&|]\\s*)(cat|head|tail)\\s+(-[0-9]+\\s+)?[\\.\\/~]?\\S*\\.(py|js|jsx|ts|tsx|md|json|jsonl|ya?ml|sh|html|css|toml|cfg|ini|conf|sql|env)\\b(?!\\s*(\\||<<))", + "anti_pattern": "(?:^|[;&|]\\s*)(cat|head|tail)\\s+(-[0-9]+\\s+)?[\\.\\/~]?\\S*\\.(py|js|jsx|ts|tsx|md|json|jsonl|ya?ml|sh|html|css|toml|cfg|ini|conf|sql|env)\\b", "window": 3 } }, @@ -195,4 +195,4 @@ } } ] -} +} \ No newline at end of file diff --git a/docs/DESIGN-laws-v4.2.md b/docs/DESIGN-laws-v4.2.md new file mode 100644 index 0000000..ce2d698 --- /dev/null +++ b/docs/DESIGN-laws-v4.2.md @@ -0,0 +1,56 @@ +# DESIGN — v4.2.0 · Laws layer: de-dup, legibility, post-promotion audit + +Fecha: 2026-07-05 · Estado: aprobado (Fernando delegó el criterio) · Origen: pregunta de Fer "¿son las laws la mejor forma de hacer esto, o son ruido?" + +## Contexto y evidencia (medida, no supuesta) + +La capa de leyes son `laws/*.txt` inyectadas SIEMPRE al SessionStart (`session-start.py:load_laws` → `565-568`), sin filtro, ~975 tokens. Investigación de esta sesión + contra-análisis adversarial (3 agentes): + +- **Coste real:** 15 leyes, ~975 tokens/sesión = <0.5% de un contexto de 200k. **El coste NO es el problema.** +- **Sin feedback de salida:** la promoción a ley SÍ exige feedback de entrada (DESIGN-V4 §3: conf≥0.95, ≥3 proyectos, ≥10 ocurrencias, 14 días sin ruido). Pero una vez escrita la ley, nada vuelve a medir su valor salvo `_find_least_impactful_law` cuando el cap 15 satura. Feedback congelado post-promoción. +- **Doble inyección (bug confirmado):** `read-instructions-before-executing` (41 inyecciones PreToolUse redundantes medidas en impact.jsonl) y `pref-fix-all-lint-test-issues` (17) existen como ley Y como instinct global activo. `auto_promote_to_law` archiva el instinct fuente, pero las rutas seed + swap manual NO → duplicados. +- **Mezcla de granularidad:** las 15 mezclan principios de conducta (advisor-escalation, deep-work, loop-reorient) con gotchas de herramienta (cat→Read, inline-python) en un solo bloque "follow always", lo que diluye la atención sobre los principios. + +## Qué NO se hace (y por qué) — evitar sobre-corrección + +El contra-análisis refutó las ideas invasivas: +- **NO encoger a 5-7 curadas a mano:** el contenido actual no es malo; el coste es <0.5%. Sin evidencia que lo justifique. +- **NO demotar los gotchas a instinct:** se promovieron porque son universales Y ya pasaron trigger-gating. Como instinct, un log con formato inesperado no dispara el regex; como ley siempre está. Demotar = perder disparo garantizado. +- **NO parar `auto_promote_to_law`:** ya tiene gates de calidad (no es ciega). +- **NO scoping por dominio:** solo 2-3 leyes son de dominio de proyecto (playwright→testing, api-route→web); el resto de "mecánicas" son universales de tooling. Montar meta-file + detección de dominio en Python para 2 casos que ahorran ~100 tok es abstracción nueva injustificada (viola minimalismo). + +## Cambios v4.2.0 + +### C1 — De-dup + fix de raíz (bug) +- **Data:** archivar `instincts/global/read-instructions-before-executing.yaml` y `pref-fix-all-lint-test-issues.yaml` → `instincts/global/archive/*.dup-of-law-20260705.yaml`. +- **`injector-engine.js`:** al construir candidatos, excluir cualquier instinct cuyo `id` tenga un fichero `laws/{id}.txt` (una ley ya se inyecta al SessionStart; el instinct gemelo es redundante). Leer LAWS_DIR una vez → Set de law-ids → skip. Retrocompatible. +- **`distill_engine.py`:** que `manual_swap_promote` (y cualquier ruta de creación de ley) archive SIEMPRE el instinct fuente si existe, igual que `auto_promote_to_law`. + +### C2 — Adelgazar advisor-escalation +- **Data:** `laws/advisor-escalation.txt` de ~1500 chars a ~500 (resumen + los 4 triggers esenciales, sin el detalle expandido). Es un principio válido, solo desproporcionado (26% del presupuesto en una ley; el fix de inyección-completa de v4.1.0 lo destapó). + +### C3 — Legibilidad: split de presentación por tier +- **Data:** nuevo `laws/laws-meta.json` (+ seed `core/laws-meta.default.json`) con `{version, laws: {: {tier: "principle"|"tool"}}}`. Clasificación inicial abajo. +- **`session-start.py:load_laws`:** devolver las leyes agrupadas por tier. `565-568` renderiza dos sub-bloques bajo el header: + ``` + CORTEX LAWS (follow always): + [principios] + - ... + [herramienta] + - ... + ``` + Retrocompat: ley sin entrada en meta → tier "principle" (nunca se oculta nada). Sin meta-file → comportamiento actual (un bloque). + +### C4 — Auditoría post-promoción (legibilidad periódica) +- **`distill_engine.py`:** nueva función `law_audit()` que devuelve por cada ley activa `{id, tier, age_days, dup_active_instinct: bool, backing_instinct_noise: ratio|null}`, más subcomando CLI `law-audit [--json]` (mismo patrón que los `prune-tracking`/`reap-nudges` de v4.1.0). Escribe también `~/.claude/cortex/.law-audit.json` al correr. NO se toca el heredoc de cx-maintain ni `bin/cx-maintain.sh` (tienen WIP ajeno sin commitear); la integración en el digest se hará cuando ese WIP asiente. Da a Fernando legibilidad periódica para podar a mano — el modelo correcto ahora que las leyes son una constitución curada (humano poda con datos, no auto-feedback falso; medir "ley seguida" es irresoluble sin trigger, confirmado por el adversarial). + +## Clasificación tier inicial (laws-meta.json) + +**principle** (conducta / cómo trabajar): advisor-escalation · deep-work-to-docs · read-instructions-before-executing · loop-reorient · pref-fix-all-lint-test-issues · gotcha-ad-por-fase-no-sustituye-e2e + +**tool** (gotcha de herramienta): build-output-to-log · cat-pipe-head-claudemd-anti · gotcha-inline-python-readability · macos-downloads-read-tool · gotcha-mcp-server-wrong-config-file · gotcha-webfetch-fallback-to-websearch · gotcha-firecrawl-over-tavily-html · e2e-playwright-selectors · gotcha-api-route-error-message-leak + +## Verificación +- Test pair + `run_all.sh` verde. +- Smoke `echo '{}' | python3 hooks/session-start.py` con los dos bloques. +- Deploy `install.sh` + verificar dedup (0 instincts gemelos), advisor adelgazada, digest con law_audit. diff --git a/docs/FEATURES.md b/docs/FEATURES.md index aa3afd2..9d5dfa2 100644 --- a/docs/FEATURES.md +++ b/docs/FEATURES.md @@ -1,4 +1,4 @@ -# fs-cortex v4.1.0 — Feature Reference +# fs-cortex v4.2.0 — Feature Reference > Complete inventory of all features, commands, hooks, modules, and capabilities. > Last updated: 2026-07-02 diff --git a/hooks/lib/distill_engine.py b/hooks/lib/distill_engine.py index 38b4150..d308ddf 100644 --- a/hooks/lib/distill_engine.py +++ b/hooks/lib/distill_engine.py @@ -19,12 +19,14 @@ auto_validate_proposals(dry_run=False) -> dict auto_promote_to_law(dry_run=False) -> tuple[list[dict], list[dict]] auto_evolve_detect(dry_run=False) -> dict + law_audit() -> dict # v4.2.0 §C4 — post-promotion legibility audit CLI --- python3 distill_engine.py auto [--dry-run] python3 distill_engine.py decay [--dry-run] python3 distill_engine.py promote [--dry-run] + python3 distill_engine.py law-audit [--json] python3 distill_engine.py status """ from __future__ import annotations @@ -85,6 +87,15 @@ # nudge_state) via its own path constant rather than importing impact_log, # to keep this module's write surface self-contained. NUDGE_STATE_FILE = CORTEX_DIR / "nudge-state.json" +# v4.2.0 §C4 (DESIGN-laws-v4.2.md): tier classification for laws, consumed +# by session-start.py's principle/tool split and read here for law_audit(). +# Absent id -> defaults to "principle" (never hidden), same retrocompat rule +# session-start.py uses. +LAWS_META_FILE = LAWS_DIR / "laws-meta.json" +# v4.2.0 §C4: law_audit() writes its per-run snapshot here so Fernando gets +# periodic legibility to prune the laws layer by hand with data instead of +# an unmeasurable auto-feedback signal (see DESIGN-laws-v4.2.md §C4). +LAW_AUDIT_FILE = CORTEX_DIR / ".law-audit.json" RATE_LIMIT_HOURS = 24 DECAY_PER_30_DAYS = 0.05 @@ -896,6 +907,32 @@ def manual_swap_promote( pass return False, f"write new law failed (rolled back): {e}" + # v4.2.0 §C1 (DESIGN-laws-v4.2.md): archive the source instinct on every + # law-creation path, not just auto_promote_to_law. Pre-fix, manual swap + # promotion left the source instinct YAML live in instincts/global/ after + # writing the law file, so it kept firing as a PreToolUse instinct AND + # injecting at every SessionStart as a law — the exact double-injection + # bug DESIGN-laws-v4.2.md §"Contexto y evidencia" measured (41 + 17 + # redundant injections). Same archive convention auto_promote_to_law uses. + if new_instinct_path.exists(): + archive_dir = new_instinct_path.parent / "archive" + ts_date = today.strftime("%Y%m%d") + archive_dest = archive_dir / f"{new_iid}.promoted-to-law-{ts_date}.yaml" + try: + archive_dir.mkdir(parents=True, exist_ok=True) + new_instinct_path.rename(archive_dest) + _log_knowledge( + "archived", new_iid, + f"promoted to law via swap; source archived as {archive_dest.name}", + source="cx-distill-swap", + ) + except OSError as e: + _log_knowledge( + "archive-failed", new_iid, + f"law promoted via swap but source archive failed: {e}", + source="cx-distill-swap", + ) + _log_knowledge( "swap-promoted", new_iid, f"archived={deprecate_iid} archive_file={archive_path.name}", @@ -1006,6 +1043,98 @@ def _law_content_for_jaccard(law_path: Path) -> str: return "" +def _load_laws_meta() -> dict: + """Load laws/laws-meta.json. Returns {} on missing/invalid (retrocompat: + caller defaults every id's tier to 'principle', same rule session-start.py + applies when the meta-file is absent or an id has no entry).""" + if not LAWS_META_FILE.exists(): + return {} + try: + data = json.loads(LAWS_META_FILE.read_text(encoding="utf-8")) + except (OSError, json.JSONDecodeError): + return {} + if not isinstance(data, dict): + return {} + laws = data.get("laws") + return laws if isinstance(laws, dict) else {} + + +def law_audit() -> dict: + """Post-promotion audit of the active laws layer (v4.2.0 §C4). + + DESIGN-laws-v4.2.md §"Contexto y evidencia": promotion TO a law is + statistically gated (auto_promote_to_law's 4 criteria), but once a law + is written nothing measures its ongoing value — "feedback congelado + post-promoción". This gives a human (Fernando) periodic legibility to + prune the laws layer with data, rather than an auto-feedback signal that + the DESIGN doc's adversarial review confirmed is unmeasurable without a + trigger (a law always injects, so "was it followed" can't be attributed). + + For every active (non-archived) law in laws/*.txt, returns: + - id: law filename stem + - tier: from laws/laws-meta.json, default "principle" + (retrocompat — never hides an unclassified law) + - age_days: days since the .txt file's mtime + - dup_active_instinct: True if an instinct YAML with the same id is + still live in instincts/global/ or any + projects/*/instincts/ (the exact C1 bug this + module's fix targets — a law with a live + twin instinct double-injects) + - backing_instinct_noise: useful/(1+noise) ratio over the last 14 days + of impact.jsonl for this id, or None if the + id has no impact data at all (distinct from + a real 0.0 ratio) + + Returns {"as_of": , "laws": [ ...per-law dicts... ]}. Also + writes the same payload to LAW_AUDIT_FILE (~/.claude/cortex/.law-audit.json) + as a side effect, mirroring compute_pipeline_stats's read-only shape but + persisted so a periodic runner (cx-maintain) can diff runs without + recomputing. + """ + today = _dt.date.today() + meta = _load_laws_meta() + impact = _impact_per_iid(days=14) + + # Same "alive" definition _all_instinct_paths()/prune_instinct_tracking() + # use elsewhere in this module: a non-archived instinct YAML with this id + # in instincts/global/ or any projects/*/instincts/. + alive_instinct_ids = {p.stem for p in _all_instinct_paths()} + + entries: list[dict] = [] + if LAWS_DIR.is_dir(): + for law_path in sorted(LAWS_DIR.glob("*.txt")): + if "archive" in str(law_path): + continue + iid = law_path.stem + tier = "principle" + meta_entry = meta.get(iid) + if isinstance(meta_entry, dict) and meta_entry.get("tier"): + tier = str(meta_entry["tier"]) + + iid_impact = impact.get(iid) + if iid_impact is None: + noise_ratio = None + else: + useful = int(iid_impact.get("useful", 0) or 0) + noise = int(iid_impact.get("noise", 0) or 0) + noise_ratio = round(useful / (1 + noise), 4) + + entries.append({ + "id": iid, + "tier": tier, + "age_days": _law_age_days(law_path, today), + "dup_active_instinct": iid in alive_instinct_ids, + "backing_instinct_noise": noise_ratio, + }) + + result = {"as_of": today.isoformat(), "laws": entries} + try: + _atomic_write(LAW_AUDIT_FILE, json.dumps(result, indent=2, ensure_ascii=False) + "\n") + except OSError: + pass + return result + + # v4 (2026-07-02, DESIGN-V4.md §3, audit-cortex-2026-07-02.md follow-up #1): # a trigger is frequently a raw regex ("Bash|Edit|Write", "Read.*\\(file_path") # rather than prose. The old `trigger[:40]` blind char-slice truncated those @@ -2952,6 +3081,21 @@ def _cmd_reap_nudges(dry_run: bool) -> None: f"(-{NUDGE_REAP_DECAY:.2f} conf, direction reset)") +def _cmd_law_audit(as_json: bool) -> None: + result = law_audit() + if as_json: + print(json.dumps(result, indent=2, ensure_ascii=False)) + return + laws = result["laws"] + print(f"Law audit ({result['as_of']}) — {len(laws)} active law(s), " + f"written to {LAW_AUDIT_FILE}:") + for entry in laws: + dup = "DUP-INSTINCT" if entry["dup_active_instinct"] else "" + noise = "n/a" if entry["backing_instinct_noise"] is None else f"{entry['backing_instinct_noise']:.2f}" + print(f" {entry['id']} [{entry['tier']}] age={entry['age_days']}d " + f"noise_ratio={noise} {dup}".rstrip()) + + def _cmd_promote(dry_run: bool) -> None: promoted, candidates = auto_promote_to_law(dry_run=dry_run) prefix = "[DRY-RUN] " if dry_run else "" @@ -3265,6 +3409,15 @@ def main(argv: list[str] | None = None) -> int: ) p_reap_nudges.add_argument("--dry-run", action="store_true") + p_law_audit = sub.add_parser( + "law-audit", + help="Post-promotion audit of active laws (tier, age, dup instinct, noise ratio)", + ) + p_law_audit.add_argument( + "--json", dest="as_json", action="store_true", + help="Machine-readable JSON output", + ) + p_backfill = sub.add_parser("backfill", help="Recover legacy session_id/session fields and rebuild tracking sessions (preview by default; --apply writes)") p_backfill.add_argument("--apply", action="store_true", help="Write changes (atomically + cross-runtime locked since v3.35.0 / issue #49)") @@ -3295,6 +3448,8 @@ def main(argv: list[str] | None = None) -> int: _cmd_prune_tracking(args.dry_run) elif args.cmd == "reap-nudges": _cmd_reap_nudges(args.dry_run) + elif args.cmd == "law-audit": + _cmd_law_audit(args.as_json) elif args.cmd == "backfill": _cmd_backfill(args.apply) elif args.cmd == "status": diff --git a/hooks/lib/injector-engine.js b/hooks/lib/injector-engine.js index cd5fb02..29ca92d 100644 --- a/hooks/lib/injector-engine.js +++ b/hooks/lib/injector-engine.js @@ -434,8 +434,27 @@ function main() { // the one already accepted from that domain have confidence >= 0.85. const seenSubtopics = new Set(); const domainConfidences = new Map(); // domain -> confidences already accepted + // v4.2.0 (DESIGN-laws-v4.2.md §C1): skip instincts already inyected as a law + // at SessionStart — an instinct whose id has a laws/{id}.txt twin is a + // confirmed duplicate (measured: read-instructions-before-executing 41x, + // pref-fix-all-lint-test-issues 17x redundant PreToolUse injections). + let lawIds = new Set(); + try { + const LAWS_DIR = path.join(CORTEX_DIR, 'laws'); + lawIds = new Set( + fs.readdirSync(LAWS_DIR) + .filter((f) => f.endsWith('.txt')) + .map((f) => f.slice(0, -4)) + ); + } catch {} for (const inst of candidates) { if (matchedInstincts.length >= MAX_INSTINCTS) break; + if (lawIds.has(inst.id)) { + if (process.env.CORTEX_DEBUG) { + try { process.stderr.write(`[cortex:injector] skip ${inst.id} — already inyected as law\n`); } catch {} + } + continue; + } if ((injectedCounts[inst.id] || 0) >= MAX_REPEAT_INJECTIONS) { suppressed.push({ id: inst.id }); continue; diff --git a/hooks/session-start.py b/hooks/session-start.py index e791848..bb28878 100644 --- a/hooks/session-start.py +++ b/hooks/session-start.py @@ -49,9 +49,24 @@ def detect_project(cwd): def load_laws(): - """Read all active law files. Engine caps total at LAW_MAX_ACTIVE=15 (see hooks/lib/distill_engine.py:LAW_MAX_ACTIVE).""" + """Read all active law files, tagged with their presentation tier. + + Engine caps total at LAW_MAX_ACTIVE=15 (see hooks/lib/distill_engine.py:LAW_MAX_ACTIVE). + Tier comes from laws-meta.json ({laws: {: {tier: "principle"|"tool"}}}); + an id absent from meta (or a missing meta file) defaults to "principle" — + a law is never hidden for lack of metadata. Returns a list of + {"text": str, "tier": str} dicts, sorted by filename like before. + """ if not LAWS_DIR.is_dir(): return [] + + meta_tiers = {} + try: + meta = json.loads((LAWS_DIR / 'laws-meta.json').read_text()) + meta_tiers = {k: v.get('tier', 'principle') for k, v in meta.get('laws', {}).items()} + except (FileNotFoundError, json.JSONDecodeError, OSError): + pass + law_files = sorted(LAWS_DIR.glob('*.txt')) laws = [] for f in law_files: @@ -62,7 +77,8 @@ def load_laws(): # readable list item under the '- {law}' join downstream, # instead of breaking the bullet list structure. indented = full_text.replace('\n', '\n ') - laws.append(indented) + tier = meta_tiers.get(f.stem, 'principle') + laws.append({'text': indented, 'tier': tier}) except Exception: pass return laws @@ -564,7 +580,20 @@ def main(): # 1. Laws laws = load_laws() if laws: - law_lines = '\n'.join(f'- {law}' for law in laws) + has_meta = (LAWS_DIR / 'laws-meta.json').is_file() + if has_meta: + # C3: split presentation by tier for legibility. A missing tier + # (retrocompat) already defaulted to "principle" in load_laws(). + principles = [law['text'] for law in laws if law['tier'] != 'tool'] + tools = [law['text'] for law in laws if law['tier'] == 'tool'] + sections = [] + if principles: + sections.append('[principios]\n' + '\n'.join(f'- {t}' for t in principles)) + if tools: + sections.append('[herramienta]\n' + '\n'.join(f'- {t}' for t in tools)) + law_lines = '\n'.join(sections) + else: + law_lines = '\n'.join(f'- {law["text"]}' for law in laws) parts.append(f'CORTEX LAWS (follow always):\n{law_lines}') else: parts.append('CORTEX: No laws configured yet. Add .txt files to ~/.claude/cortex/laws/') diff --git a/install.ps1 b/install.ps1 index 53b546c..7b98271 100644 --- a/install.ps1 +++ b/install.ps1 @@ -20,7 +20,7 @@ $CommandsDir = Join-Path $ClaudeDir "commands" $HooksDir = Join-Path (Join-Path $ClaudeDir "hooks") "cortex" $SettingsFile = Join-Path $ClaudeDir "settings.json" $ClaudeMd = Join-Path $ClaudeDir "CLAUDE.md" -$NewVersion = "4.1.0" +$NewVersion = "4.2.0" # v3.25.1 — explicit downgrade flag (parity with install.sh). # A behind-remote repo would silently rewind hooks otherwise. @@ -212,6 +212,15 @@ else { } } +# v4.2.0: laws-meta.json drives the SessionStart law tier split (principle vs tool). +# Absent -> laws render as a single block (backward compatible), so this only seeds it. +$lawsMetaSrc = [IO.Path]::Combine($ScriptDir, "core", "laws-meta.default.json") +$lawsMetaDest = Join-Path (Join-Path $CortexDir "laws") "laws-meta.json" +if ((-not (Test-Path $lawsMetaDest)) -and (Test-Path $lawsMetaSrc)) { + Copy-Item $lawsMetaSrc $lawsMetaDest + Print-Ok "Created laws-meta.json" +} + $reflexesDest = Join-Path $CortexDir "reflexes.json" if (-not (Test-Path $reflexesDest)) { Copy-Item ([IO.Path]::Combine($ScriptDir, "core", "reflexes.default.json")) $reflexesDest diff --git a/install.sh b/install.sh index 3d40674..5ce690d 100755 --- a/install.sh +++ b/install.sh @@ -21,7 +21,7 @@ COMMANDS_DIR="$CLAUDE_DIR/commands" HOOKS_DIR="$CLAUDE_DIR/hooks/cortex" SETTINGS_FILE="$CLAUDE_DIR/settings.json" CLAUDE_MD="$CLAUDE_DIR/CLAUDE.md" -NEW_VERSION="4.1.0" +NEW_VERSION="4.2.0" # v3.25.1 — explicit downgrade flag. The installer is a copy-not-merge of # hooks/commands, so running an older `install.sh` over a newer install @@ -234,6 +234,12 @@ except Exception as e: " 2>/dev/null fi fi +# v4.2.0: laws-meta.json drives the SessionStart law tier split (principle vs tool). +# Absent -> laws render as a single block (backward compatible), so this only seeds it. +if [ ! -f "$CORTEX_DIR/laws/laws-meta.json" ] && [ -f "$SCRIPT_DIR/core/laws-meta.default.json" ]; then + cp "$SCRIPT_DIR/core/laws-meta.default.json" "$CORTEX_DIR/laws/laws-meta.json" + print_ok "Created laws-meta.json" +fi if [ ! -f "$CORTEX_DIR/reflexes.json" ]; then cp "$SCRIPT_DIR/core/reflexes.default.json" "$CORTEX_DIR/reflexes.json" print_ok "Created reflexes.json"