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
51 changes: 51 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,57 @@ 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.3.0] — 2026-07-05

**Zero-touch mode.** The learning pipeline no longer waits on a human anywhere:
maintenance decides with deterministic rules and the operator's only role is an
optional veto. Reverts the v4.2 "curated constitution, human prunes" model at
the operator's explicit request. Implementation delegated to Codex GPT-5.3
(spec-driven), reviewed inline.

### Added
- `hooks/lib/distill_engine.py` `auto_promote_to_law()`: **auto-swap at a
saturated law cap**. When an instinct passes every maturity gate and the cap
(15/15) is the only blocker, the engine retires the least-impactful law
(lowest `useful/(1+noise)` over 14d via `_find_least_impactful_law`) and
promotes in one atomic operation. Guards: victim age >=
`LAW_AUTO_SWAP_MIN_AGE_DAYS` (30), max `LAW_AUTO_SWAP_MAX_PER_RUN` (2) per
pass, productive cohorts (ratio > 1.0) never lose a law. Executes via the new
`_swap_promote_unlocked` — extracted from `manual_swap_promote` because
calling the `@_write_locked` wrapper from inside the maintain pass (which
already holds the shared flock) would deadlock. Victim archived to
`laws/archive/` (reversible), swap logged as `auto-swap` in the knowledge log.
- `hooks/lib/distill_engine.py` `expire_stale_proposals()`: pending
human-gated proposals older than `PROPOSAL_TTL_DAYS` (30) auto-reject with
`rejected_by: cx-maintain-ttl` — added to `VALIDATE_AUTHORIZED_REJECTERS`
(Python) and `TOMBSTONE_REJECTERS` (`session-learner.js`) so expired ids
never resurrect. Entries without a parseable `detected` date are never
expired blind.
- `commands/cx-maintain.md` + `bin/cx-maintain.sh` (embedded script kept in
sync): engine pass calls the TTL expiry and reports
`promoted=N (swapped=S) ... expired=E`; digest gains `swaps_last_run`
(in/out pairs) and `expired_last_run`.

### Changed
- `hooks/session-start.py` `check_review_digest()`: the imperative
`[REVIEW] N items pendientes -> /cx-review` badge is replaced by an
informative `[cx] maintain: +law (jubilada X), N propuestas caducadas, M en
cola (caducan a 30d) — detalle opcional: /cx-review`, built from the digest's
new fields and silenced automatically 48h after the pass. Tolerant of old
digests — a malformed or pre-v4.3 digest returns no badge instead of
crashing SessionStart.
- `commands/cx-review.md` + `README.md`: `/cx-review` reframed from "weekly,
the only command with judgment" to an optional veto pass — skipping it
forever is a supported mode.
- `hooks/lib/distill_engine.py`: the three copies of the promote-then-archive
block deduplicated into `_archive_promoted_instinct_source()`.

### Tests
- `test_distill_v4` 6→10 (auto-swap happy path, age-floor block, TTL expiry,
dry-run inertness), `test_session_start` 7→11 (informative badge, 48h
staleness, malformed digest), `test_session_learner` 60→61 (`cx-maintain-ttl`
tombstone), runner report asserts `swapped=`/`expired=`.

## [4.2.2] — 2026-07-05

### Fixed
Expand Down
25 changes: 14 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@
- **Observes** every tool call silently via async hooks (0 tokens overhead), capturing real output + error lines with anti-noise guards
- **Injects** matched instincts and reflexes per tool use via PreToolUse (~120 tokens max)
- **Learns** patterns automatically: instincts start as silent `draft`, earn `confirmed` (injectable) at 5+ occurrences across 3+ sessions — no manual analyze/validate step
- **Maintains** itself: `/cx-maintain` (deterministic, cron-able) promotes proven instincts to Laws, dedups, decays, rotates storage — zero questions
- **Reviews** weekly with you: `/cx-review` is the only command left with judgment — one shorthand digest, two minutes
- **Maintains** itself: `/cx-maintain` (deterministic, cron-able) promotes proven instincts to Laws — auto-swapping the least-impactful law when the cap is full (v4.3.0) — dedups, decays, expires stale proposals at 30 days, rotates storage. Zero questions
- **Reports** instead of assigning work: the SessionStart badge tells you what the last pass did; `/cx-review` is an optional veto digest — nothing ever waits on you
- **Protects** with deterministic reflex hooks (not probabilistic instructions)

v4.0.0 ("signal-first, zero-decision") replaced the old 20+-command,
Expand All @@ -31,7 +31,7 @@ Observe (hooks, output+err_msg captured) → draft (silent tracking)
auto auto

draft → confirmed (occ>=5, sessions>=3) → /cx-maintain → /cx-review
auto cron-able weekly, human
auto cron-able optional veto

OBSERVATIONS → INSTINCTS (draft/confirmed) → LAWS → SKILLS/COMMANDS/RULES
(JSONL, 0 tok) (YAML) (TXT) (evolved/)
Expand Down Expand Up @@ -64,7 +64,7 @@ at 5+ occurrences across 3+ distinct sessions.

**Decay**: -0.05 per 30 days, applied by `/cx-maintain`. What you don't use fades.

**Feedback**: `/cx-review` closes the human loop on incorrect injections. 30%+ rejection rate → confidence reduced.
**Feedback**: `/cx-downvote` (via the optional `/cx-review` digest) vetoes incorrect injections. 30%+ rejection rate → confidence reduced. Human-gated proposals not reviewed within 30 days expire on their own (tombstoned).

**Promotion**: Jaccard similarity ≥ 0.70 + 2 projects + avg confidence ≥ 0.80 → global, computed inside `/cx-maintain`.

Expand Down Expand Up @@ -163,11 +163,12 @@ You don't configure or run anything. Just work — Cortex learns in the backgrou
/cx-maintain ← decay + dedup + promotion to law + storage rotation + health check
```

**Weekly, human** — when you see `[REVIEW] N items pendientes`:
**Optional, human veto** — the `[cx] maintain:` badge reports what the last
pass already did (law swaps, expired proposals, queue):

```
/cx-review ← ONE shorthand digest: pending proposals, evolve drafts, law
deprecation candidates. 2 minutes, not twenty.
/cx-review ← ONE shorthand digest of what auto-swapped, what expired and
what waits. Pure veto — skipping it never blocks anything.
```

**When needed:**
Expand All @@ -182,7 +183,7 @@ You don't configure or run anything. Just work — Cortex learns in the backgrou
### Daily workflow

```
1. Open Claude Code → laws inject automatically, [REVIEW] badge if pending
1. Open Claude Code → laws inject automatically, [cx] badge reports last maintain
2. Work normally → observe.py records real output/errors, injector injects
3. Instincts mature → draft → confirmed automatically, no action from you
4. End of day → /cx-eod (optional but useful, now cumulative)
Expand All @@ -191,8 +192,8 @@ You don't configure or run anything. Just work — Cortex learns in the backgrou
### Weekly maintenance

```
/cx-maintain → /cx-review
deterministic human digest (2 min)
/cx-maintain → /cx-review (optional)
deterministic human veto digest
```

### How knowledge evolves
Expand All @@ -201,7 +202,9 @@ You don't configure or run anything. Just work — Cortex learns in the backgrou
You work → Cortex captures output+errors (guarded) → instinct born as draft
→ 5+ occurrences across 3+ sessions → status: confirmed → starts injecting
→ /cx-maintain promotes proven instincts to law deterministically
→ /cx-review is your only weekly touchpoint for what's left to decide
(auto-swapping the least-impactful law when the cap is full, v4.3.0)
→ stale pending proposals expire on their own after 30 days
→ /cx-review is an optional veto pass — nothing ever waits on you
```

## Commands (7 active + 17 deprecated)
Expand Down
13 changes: 11 additions & 2 deletions bin/cx-maintain.sh
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,7 @@ def step(name):
# public functions run_auto_distill() calls internally, but bypasses its
# 24h rate limiter — a deliberate /cx-maintain run should always execute,
# not silently no-op because SessionStart already ran today.
decayed = archived = promoted = candidates = []
decayed = archived = promoted = candidates = expired = []
validated = skipped_validate = evolve_drafts = 0
engine_ok = False
try:
Expand All @@ -141,13 +141,16 @@ try:
validated = len(validate_result.get("accepted", []))
skipped_validate = len(validate_result.get("skipped", []))
promoted, candidates = de.auto_promote_to_law(dry_run=DRY_RUN)
expired = de.expire_stale_proposals(dry_run=DRY_RUN)
evolve_result = de.auto_evolve_detect(dry_run=DRY_RUN)
evolve_drafts = len(evolve_result.get("drafts_generated", []))
swapped = sum(1 for p in promoted if p.get("swapped_out"))
engine_ok = True
report["steps"].append({"name": "engine-pass", "ok": True, "detail":
f"decayed={len(decayed)} archived={len(archived)} validated={validated} "
f"skipped_validate={skipped_validate} promoted={len(promoted)} "
f"candidates={len(candidates)} evolve_drafts={evolve_drafts}"})
f"(swapped={swapped}) candidates={len(candidates)} expired={len(expired)} "
f"evolve_drafts={evolve_drafts}"})
finally:
de._lock_release(lock_fh)
except Exception as e:
Expand Down Expand Up @@ -348,6 +351,12 @@ if not DRY_RUN:
"law_deprecation_candidate": health["law_deprecation_candidate"],
"laws_active": health["laws_active"],
"laws_cap": health["laws_cap"],
"swaps_last_run": [
{"in": p.get("id"), "out": p.get("swapped_out")}
for p in (promoted or [])
if p.get("swapped_out")
],
"expired_last_run": len(expired or []),
}
digest["total_items"] = (
digest["proposals_human_gated"]
Expand Down
22 changes: 17 additions & 5 deletions commands/cx-maintain.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ runs and prints a report, or a step is skipped with a one-line reason — nothin
ever waits on the user. Idempotent: running it twice in a row is safe (each
sub-step guards its own idempotency: `last_decay_at`, the `.last-auto-distill`
marker, size-gated storage rotation, etc.).
When the law cap is saturated, promotion now auto-swaps under deterministic
guards; stale pending proposals expire after 30 days.

Replaces the manual judgment calls that used to live in `/cx-analyze`,
`/cx-distill`, `/cx-dream`, `/cx-promote`, `/cx-backfill` (deterministic parts
Expand All @@ -24,8 +26,9 @@ criteria, or rotation thresholds. If those engines change their gates, this
command's output changes with them automatically.

Writes `~/.claude/cortex/.review-digest.json` — the accumulated digest that
`/cx-review` presents to a human and that `hooks/session-start.py` uses for the
one-line `[REVIEW] N items pendientes` badge.
`/cx-review` can present on demand and that `hooks/session-start.py` uses for
the informative one-line `[cx] maintain:` badge (swaps + expiries + queue;
silenced automatically 48h after the pass — it reports, it never assigns work).

## Usage

Expand Down Expand Up @@ -84,7 +87,7 @@ LIB_DIR="${CORTEX_LIB_DIR:-$HOME/.claude/hooks/cortex/lib}"
DRY_RUN=0
[ "$1" = "--dry-run" ] && DRY_RUN=1

CORTEX_DIR="$CORTEX_DIR" LIB_DIR="$LIB_DIR" DRY_RUN="$DRY_RUN" python3 << 'PYEOF'
CORTEX_DIR="$CORTEX_DIR" LIB_DIR="$LIB_DIR" DRY_RUN="$DRY_RUN" python3 - <<'PYEOF'
import json, os, re, sys, time
from datetime import date, datetime, timezone
from pathlib import Path
Expand Down Expand Up @@ -114,7 +117,7 @@ def step(name):
# public functions run_auto_distill() calls internally, but bypasses its
# 24h rate limiter — a deliberate /cx-maintain run should always execute,
# not silently no-op because SessionStart already ran today.
decayed = archived = promoted = candidates = []
decayed = archived = promoted = candidates = expired = []
validated = skipped_validate = evolve_drafts = 0
engine_ok = False
try:
Expand All @@ -131,13 +134,16 @@ try:
validated = len(validate_result.get("accepted", []))
skipped_validate = len(validate_result.get("skipped", []))
promoted, candidates = de.auto_promote_to_law(dry_run=DRY_RUN)
expired = de.expire_stale_proposals(dry_run=DRY_RUN)
evolve_result = de.auto_evolve_detect(dry_run=DRY_RUN)
evolve_drafts = len(evolve_result.get("drafts_generated", []))
swapped = sum(1 for p in promoted if p.get("swapped_out"))
engine_ok = True
report["steps"].append({"name": "engine-pass", "ok": True, "detail":
f"decayed={len(decayed)} archived={len(archived)} validated={validated} "
f"skipped_validate={skipped_validate} promoted={len(promoted)} "
f"candidates={len(candidates)} evolve_drafts={evolve_drafts}"})
f"(swapped={swapped}) candidates={len(candidates)} expired={len(expired)} "
f"evolve_drafts={evolve_drafts}"})
finally:
de._lock_release(lock_fh)
except Exception as e:
Expand Down Expand Up @@ -338,6 +344,12 @@ if not DRY_RUN:
"law_deprecation_candidate": health["law_deprecation_candidate"],
"laws_active": health["laws_active"],
"laws_cap": health["laws_cap"],
"swaps_last_run": [
{"in": p.get("id"), "out": p.get("swapped_out")}
for p in (promoted or [])
if p.get("swapped_out")
],
"expired_last_run": len(expired or []),
}
digest["total_items"] = (
digest["proposals_human_gated"]
Expand Down
15 changes: 9 additions & 6 deletions commands/cx-review.md
Original file line number Diff line number Diff line change
@@ -1,18 +1,21 @@
---
name: cx-review
description: Weekly human review — the ONLY command with judgment. Digest of pending proposals, near-threshold drafts and law candidates in one shorthand pass.
description: Optional human veto pass — digest of what /cx-maintain decided (law swaps, expired proposals) and what waits. Zero-touch since v4.3.0, skipping it never blocks the pipeline.
command: true
---

# /cx-review

## What it does

The **only** command in Cortex v4 that requires human judgment
(`docs/DESIGN-V4.md` §5, principle P2). Everything deterministic already ran
via `/cx-maintain` (decay, dedup, purge, auto-promotion, storage rotation) —
this command presents what is LEFT over that a human must decide, as ONE
consolidated shorthand list. Two minutes, not twenty.
The optional human veto surface of Cortex. Since v4.3.0 (zero-touch) nothing
waits on this command: `/cx-maintain` auto-swaps laws at a saturated cap under
deterministic guards (victim = lowest 14d impact, age >= 30d, max 2 per run)
and expires human-gated proposals pending > 30 days (tombstoned, never
resurrect). This command presents what happened and what is still in queue as
ONE consolidated shorthand list, so the operator can veto (`/cx-downvote`,
restore from archive) or fast-track an item. Skipping it forever is a valid
mode — the pipeline never blocks on a human.

Fuses what used to be three separate commands:
- `/cx-validate` (human-gated proposals — the domains `auto_validate_proposals`
Expand Down
Loading
Loading