diff --git a/CHANGELOG.md b/CHANGELOG.md index 3ea874e..3ace0c9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,21 @@ 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.2] — 2026-07-05 + +### Fixed +- `/cx-maintain` Step 7 (`commands/cx-maintain.md` + `bin/cx-maintain.sh`, + identical embedded script): **learn markers now reset after a maintenance + pass**. `observe.py` touches `.learn-pending` every LEARN_THRESHOLD + observations, but nothing cleared it after `/cx-analyze` retired in v4 — so + SessionStart's "N+ new observations, run /cx-maintain" banner nagged + forever, even immediately after a pass. Step 7 now snapshots the current + observation total into `.last-learn-count` (atomic tmp+rename), zeroes + `.obs-count` and deletes `.learn-pending`, so `check_learn_pending()` + measures "since last maintenance" again. `test_cx_maintain_runner` 9→10 + (seeded markers reset end-to-end); the suite also gets its missing row in + the FEATURES.md tests table (gap since v4.0.0). + ## [4.2.1] — 2026-07-05 **AD follow-up (Codex GPT-5.5).** After merging v4.2.0, a read-only adversarial diff --git a/bin/cx-maintain.sh b/bin/cx-maintain.sh index b26f83e..947cf95 100755 --- a/bin/cx-maintain.sh +++ b/bin/cx-maintain.sh @@ -366,13 +366,35 @@ if not DRY_RUN: else: report["steps"].append({"name": "write-digest", "ok": True, "detail": "(dry-run: not written)"}) -# ── Step 7: compat markers .last-distill / .last-dream ── +# ── Step 7: compat markers .last-distill / .last-dream + learn markers ── +# v4.2.2: also reset the learn markers. observe.py touches .learn-pending +# every LEARN_THRESHOLD observations, but nothing cleared it after +# /cx-analyze retired in v4 — so SessionStart's "N+ new observations, run +# /cx-maintain" banner nagged forever, even right after a pass. Snapshot the +# current observation total into .last-learn-count, zero .obs-count and drop +# the flag, so check_learn_pending() measures "since last maintenance" again. if not DRY_RUN: try: now_iso = datetime.now(timezone.utc).isoformat() (CORTEX_DIR / ".last-distill").write_text(now_iso + "\n", encoding="utf-8") (CORTEX_DIR / ".last-dream").write_text(now_iso + "\n", encoding="utf-8") - report["steps"].append({"name": "compat-markers", "ok": True, "detail": "touched .last-distill, .last-dream"}) + total_obs = 0 + for obs_file in (CORTEX_DIR / "projects").glob("*/observations.jsonl"): + try: + with open(obs_file, encoding="utf-8") as f: + total_obs += sum(1 for _ in f) + except OSError: + pass + for name, value in ((".last-learn-count", str(total_obs)), (".obs-count", "0")): + tmp = CORTEX_DIR / f"{name}.tmp.{os.getpid()}" + tmp.write_text(value, encoding="utf-8") + os.replace(tmp, CORTEX_DIR / name) + try: + (CORTEX_DIR / ".learn-pending").unlink() + except FileNotFoundError: + pass + report["steps"].append({"name": "compat-markers", "ok": True, + "detail": f"touched .last-distill, .last-dream; .last-learn-count={total_obs}; .obs-count=0; cleared .learn-pending"}) except Exception as e: report["steps"].append({"name": "compat-markers", "ok": False, "detail": str(e)}) report["errors"].append(f"compat-markers: {e}") diff --git a/commands/cx-maintain.md b/commands/cx-maintain.md index 30ba79c..5b1717c 100644 --- a/commands/cx-maintain.md +++ b/commands/cx-maintain.md @@ -356,13 +356,35 @@ if not DRY_RUN: else: report["steps"].append({"name": "write-digest", "ok": True, "detail": "(dry-run: not written)"}) -# ── Step 7: compat markers .last-distill / .last-dream ── +# ── Step 7: compat markers .last-distill / .last-dream + learn markers ── +# v4.2.2: also reset the learn markers. observe.py touches .learn-pending +# every LEARN_THRESHOLD observations, but nothing cleared it after +# /cx-analyze retired in v4 — so SessionStart's "N+ new observations, run +# /cx-maintain" banner nagged forever, even right after a pass. Snapshot the +# current observation total into .last-learn-count, zero .obs-count and drop +# the flag, so check_learn_pending() measures "since last maintenance" again. if not DRY_RUN: try: now_iso = datetime.now(timezone.utc).isoformat() (CORTEX_DIR / ".last-distill").write_text(now_iso + "\n", encoding="utf-8") (CORTEX_DIR / ".last-dream").write_text(now_iso + "\n", encoding="utf-8") - report["steps"].append({"name": "compat-markers", "ok": True, "detail": "touched .last-distill, .last-dream"}) + total_obs = 0 + for obs_file in (CORTEX_DIR / "projects").glob("*/observations.jsonl"): + try: + with open(obs_file, encoding="utf-8") as f: + total_obs += sum(1 for _ in f) + except OSError: + pass + for name, value in ((".last-learn-count", str(total_obs)), (".obs-count", "0")): + tmp = CORTEX_DIR / f"{name}.tmp.{os.getpid()}" + tmp.write_text(value, encoding="utf-8") + os.replace(tmp, CORTEX_DIR / name) + try: + (CORTEX_DIR / ".learn-pending").unlink() + except FileNotFoundError: + pass + report["steps"].append({"name": "compat-markers", "ok": True, + "detail": f"touched .last-distill, .last-dream; .last-learn-count={total_obs}; .obs-count=0; cleared .learn-pending"}) except Exception as e: report["steps"].append({"name": "compat-markers", "ok": False, "detail": str(e)}) report["errors"].append(f"compat-markers: {e}") diff --git a/docs/FEATURES.md b/docs/FEATURES.md index 477809f..6a22216 100644 --- a/docs/FEATURES.md +++ b/docs/FEATURES.md @@ -1,4 +1,4 @@ -# fs-cortex v4.2.1 — Feature Reference +# fs-cortex v4.2.2 — Feature Reference > Complete inventory of all features, commands, hooks, modules, and capabilities. > Last updated: 2026-07-05 @@ -252,7 +252,7 @@ mapping: [`MIGRATION-V4.md`](MIGRATION-V4.md). | Command | Purpose | Token Cost | |---|---|---| | `/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-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`. Resets the learn markers (`.last-learn-count`, `.obs-count`, `.learn-pending`) so the SessionStart "N+ new observations" banner measures since-last-maintenance (v4.2.2). 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) | | `/cx-gotcha` | Capture error→fix as high-priority instinct (unchanged) | ~200 | @@ -422,6 +422,7 @@ Deterministic rules via hooks — not probabilistic instructions. Triggers are r | `test_integrity.sh` | 14 | observe.py direct, **21 commands** validated (EXPECTED_COMMANDS subset; 24 command files total as of v4.0.0 — `ls commands/*.md` — incl. new `cx-maintain`/`cx-review` not yet in EXPECTED_COMMANDS), core file schemas, **version consistency** | | `test_distill_v4.sh` | 6 | **(v4.0.0)** `auto_promote_to_law` deterministic gate: conf/projects/occurrences_v4/noise-14d criteria, `law_eligible: false` veto, lazy `occurrences_legacy`/`occurrences_v4` migration | | `test_injector_v4.sh` | 13 | **(v4.0.0)** `status: draft` never injects, `status: confirmed`/legacy-no-field inject normally, subtopic dedup + domain soft-ceiling at conf>=0.85, JSON-fragment + prompt-injection action guards, degenerate-trigger static validation, live `reflexes.json` polling conditions | +| `test_cx_maintain_runner.sh` | 10 | **(v4.0.0, inventoried v4.2.2)** `bin/cx-maintain.sh` hermetic sandbox: exit 0 on repeated runs, decay exactly-once (same-day idempotency via `last_decay_at`), compat markers, valid `.review-digest.json`, no leftover lock dir, `--dry-run` writes nothing; **v4.2.2**: learn markers reset (`.last-learn-count` snapshot, `.obs-count` zeroed, `.learn-pending` cleared) | | `test_install_ps1.ps1` | 10 | PowerShell syntax, version consistency, security features, backup categories, hook config, v3.26-v3.28 source files present, **CI on windows-latest** | | `test_impact.sh` | 32 | **Sprint 0/1 funnel** — schema v1, JS↔Python compat, concurrent writes (10 parallel → 0 loss), rotation, gate GO/NO-GO, formulas, input validation, **v3.17.0 source split** (user/agent ratios, gate input, legacy default), **v3.18.0 auto-eval** (3 evaluator types, no-evaluator default, reflex iid prefix) | | `test_cross_day_tracker.sh` | 10 | Roundtrip, boost tiers (1/2/4/8 days), confidence cap 0.95, Jaccard match, single-token guard, prune >365d, concurrent append | @@ -668,4 +669,5 @@ locally (not installed to `~/.claude/`): | v4.0.0 | 2026-07-02 | **"Signal-first, zero-decision" — full redesign of capture and command set.** `docs/DESIGN-V4.md` + `docs/SPEC-PORT-SINAPSIS.md` (port of Sinapsis v4.6.1, MIT, Luis Salgado). **BREAKING:** 17 of 24 commands deprecated to notice-only stubs (mapping in `docs/MIGRATION-V4.md`); 7 remain active (`cx-status`, `cx-maintain`, `cx-review`, `cx-eod`, `cx-gotcha`, `cx-backup`, `cx-restore`). New `/cx-maintain` (deterministic, cron-able: decay+purge+auto-validate+deterministic law-promotion+Jaccard dedup-by-subtopic+storage rotation+proposals↔instincts reconciliation+health check, writes `.review-digest.json`). New `/cx-review` (the only command with human judgment, weekly, fuses old validate+evolve-confirm+downvote+distill-swap into one shorthand pass). `hooks/observe.py`: per-line capture guards before trusting an error match (`[codex]`/npm-log prefixes, version listings, grep headers, `0 errors`, bare `warning:`), `output` cap 8000→10000, `err_msg` now the first non-guarded matching line. New `status: draft`/`confirmed` instinct lifecycle (`session-learner.js`, `injector-engine.js`) — drafts track silently, auto-promote at `occurrences>=5 AND sessions_seen>=3`; legacy instincts default `confirmed`. Law promotion gate replaced with a fully deterministic one (`conf>=0.95`, `projects_seen>=3`, `occurrences_v4>=10` via lazy `occurrences_legacy` migration, 0 noise 14d) — `auto-distill-candidates.md` buzón removed. Injector: JSON-fragment + prompt-injection guards at load, degenerate-trigger static validation, dedup switched from per-domain to per-subtopic (2 first words of id) with a soft 2-per-domain ceiling at conf>=0.85. `core/_cx-eod-gather.sh`: per-project `context` field, `[eod-eisenhower]` Q1-Q4 keyword classification of "for tomorrow" bullets, review-digest pending count surfaced. New test suites `test_distill_v4.sh` (6/6 PASS), `test_injector_v4.sh` (13/13 PASS). | | v4.1.0 | 2026-07-04 | **Audit hardening (multi-agent audit + adversarial verify).** `session-start.py` injects full law text (not just line 1). `session-learner.js`: tombstone gate matches by trigger vs `proposals-history.jsonl`, dup-trigger dedup, `$HOME`→`~` paths, head+tail sampling. Reflexes: re-enabled `python3-bypass-write-tool`, hardened `bash-cat-use-read`. `storage-rotation.js`: prunes `cross-day-tracker` past 1.5× threshold, stale `.lock`/`.bak` cleanup, `timeline.jsonl` rotation. `distill_engine.py`: `prune_instinct_tracking()` + `reap_stale_nudge_state()` + CLI subcommands. `injector-engine.js`: session token budget 8000→12000 with a visible suppression warning, optional per-reflex `domains` filter. | | v4.2.0 | 2026-07-05 | **Laws layer: de-dup, tier legibility, post-promotion audit** (`docs/DESIGN-laws-v4.2.md`; 3-agent adversarial pass). Fixed double injection (`read-instructions` 41×, `pref-fix` 17× redundant PreToolUse) — `injector-engine.js` skips instinct candidates with a `laws/{id}.txt` twin; `manual_swap_promote` archives the source instinct like `auto_promote_to_law`. `advisor-escalation` trimmed 1505→717 chars. Reverted the v4.1.0 `bash-cat-use-read` lookahead regression (broke `test_reflex_matchers`). Added law presentation split by tier (`[principios]`/`[herramienta]`, `core/laws-meta.default.json`) + `law_audit()` + `law-audit` CLI. | +| v4.2.2 | 2026-07-05 | **Learn markers reset in `/cx-maintain` Step 7.** Nothing cleared `.learn-pending` after `/cx-analyze` retired in v4, so the SessionStart "N+ new observations, run /cx-maintain" banner nagged forever. Step 7 (both `commands/cx-maintain.md` and `bin/cx-maintain.sh`) now snapshots the obs total into `.last-learn-count`, zeroes `.obs-count` and drops the flag. `test_cx_maintain_runner` 9→10 + suite inventoried in the tests table (gap since v4.0.0). | | v4.2.1 | 2026-07-05 | **AD follow-up (Codex GPT-5.5 review of v4.1/v4.2).** `load_laws()` (`session-start.py`) now tolerant of a malformed `laws-meta.json` — a non-dict schema no longer crashes the whole SessionStart injection. `impact.archive` retention fixed from count-based `keep=5` (could delete chunks < 90 days old, contradicting the append-only "never deleted" contract) to 90-day age-based per `DESIGN-V4 §7`; the stale "never deleted" comments in `impact_log.py`/`session-learner.js` reconciled. New laws-meta robustness tests; version history caught up. | diff --git a/install.ps1 b/install.ps1 index c77c75e..e448279 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.2.1" +$NewVersion = "4.2.2" # v3.25.1 — explicit downgrade flag (parity with install.sh). # A behind-remote repo would silently rewind hooks otherwise. diff --git a/install.sh b/install.sh index cf89a9b..8fd5685 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.2.1" +NEW_VERSION="4.2.2" # 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 diff --git a/tests/test_cx_maintain_runner.sh b/tests/test_cx_maintain_runner.sh index 3c77a9f..94053eb 100755 --- a/tests/test_cx_maintain_runner.sh +++ b/tests/test_cx_maintain_runner.sh @@ -65,6 +65,14 @@ status: confirmed --- YAML +# v4.2.2 — seed learn markers + observations so Test 3b can prove the reset: +# observe.py increments .obs-count and touches .learn-pending at threshold; +# the runner must snapshot the total, zero the count and drop the flag. +mkdir -p "$CDIR/projects/testproj" +printf '{"ts":"2026-07-05T10:00:00Z","ev":"tc","tool":"Bash"}\n%.0s' 1 2 3 > "$CDIR/projects/testproj/observations.jsonl" +echo "7" > "$CDIR/.obs-count" +touch "$CDIR/.learn-pending" + # ── Test 1: first run exits 0 ──────────────────────────────────────────────── echo "--- Test 1: first run exits 0 ---" OUT1=$(CORTEX_DIR="$CDIR" "$RUNNER" 2>&1) @@ -85,6 +93,19 @@ else fail ".last-distill or .last-dream missing" fi +# ── Test 3b: learn markers reset (v4.2.2) ──────────────────────────────────── +# Nothing cleared .learn-pending after /cx-analyze retired in v4, so the +# SessionStart "N+ new observations" banner nagged forever. The runner must +# snapshot the obs total, zero .obs-count and drop the flag. +echo "--- Test 3b: learn markers reset after run ---" +LEARN_COUNT=$(cat "$CDIR/.last-learn-count" 2>/dev/null) +OBS_COUNT=$(cat "$CDIR/.obs-count" 2>/dev/null) +if [ "$LEARN_COUNT" = "3" ] && [ "$OBS_COUNT" = "0" ] && [ ! -f "$CDIR/.learn-pending" ]; then + pass "Learn markers reset (.last-learn-count=3, .obs-count=0, .learn-pending gone)" +else + fail "Learn markers not reset: last-learn-count='$LEARN_COUNT' obs-count='$OBS_COUNT' pending=$([ -f "$CDIR/.learn-pending" ] && echo yes || echo no)" +fi + # ── Test 4: digest is valid JSON with expected keys ────────────────────────── echo "--- Test 4: .review-digest.json is valid JSON ---" if [ -f "$CDIR/.review-digest.json" ]; then