From 63cbe1f351101905c28ed34be9be4b349aac5040 Mon Sep 17 00:00:00 2001 From: Fernando Montero Date: Sun, 5 Jul 2026 16:43:05 +0200 Subject: [PATCH 1/2] =?UTF-8?q?feat(curate):=20v4.4.0=20=E2=80=94=20/cx-cu?= =?UTF-8?q?rate=20semantic=20curator,=20/cx-downvote=20judgment=20automate?= =?UTF-8?q?d?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The LLM half of zero-touch. Claude now performs the semantic judgment that used to be the human's: which laws are general, which have less value and belong in the instinct pool, which candidates deserve a slot. - commands/cx-curate.md: weekly pass with written criteria (KEEP/DEMOTE by generality + redundancy vs CLAUDE.md/skills + value-per-token; PROMOTE only knowledge with no reliable trigger moment — audit finding: a 0.99 trigger-gated instinct beats a law; DOWNVOTE broken/ noisy instincts). Hard engine budgets: 2 demotes, 2 promotes, 8 downvotes per pass, floor 0.30, laws <30d untouchable. All actions via engine functions (v4.3.1 cascade demote, swap, downvote CLI). - distill_engine.py: CURATE_* constants, apply_confidence_downvote(), curate_snapshot() + CLI curate-snapshot/downvote, curate_due()/ touch_curate_marker() on .last-curate. - session-start.py: '[CURATE] due' banner (>7d) — the banner is the trigger, Claude runs it in-session without asking. Single stat, never raises. - core/claudemd-section.md refreshed to the v4 active set (stale with the full v3 list since v4.0.0); active commands 7->8 everywhere; test_integrity now validates the active-set contract. test_curate new 6/6, test_session_start 11->14, integrity/security/ dream/distill_v4/distill_engine/law_tier/runner green. Implemented by 3 parallel subagents + integration verifier per operator request; validated same-day by the 29-agent semantic audit (6 demotes, 0 promotes, 10 downvotes, cap 15/15 -> 9/15). Co-Authored-By: Claude Fable 5 --- CHANGELOG.md | 44 ++++++++ README.md | 10 +- commands/cx-curate.md | 153 +++++++++++++++++++++++++ commands/cx-downvote.md | 2 + commands/cx-review.md | 2 + core/claudemd-section.md | 2 +- docs/FEATURES.md | 15 ++- hooks/lib/distill_engine.py | 205 +++++++++++++++++++++++++++++++++ hooks/session-start.py | 26 ++++- install.ps1 | 2 +- install.sh | 2 +- tests/test_curate.sh | 220 ++++++++++++++++++++++++++++++++++++ tests/test_integrity.sh | 13 ++- tests/test_session_start.sh | 53 +++++++++ 14 files changed, 729 insertions(+), 20 deletions(-) create mode 100644 commands/cx-curate.md create mode 100755 tests/test_curate.sh diff --git a/CHANGELOG.md b/CHANGELOG.md index 72984c3..a872a7b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,50 @@ 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.4.0] — 2026-07-05 + +**`/cx-curate` — the semantic curator.** Automates the judgment `/cx-downvote` +and the human curator used to provide: which laws are general, which have less +value and belong in the instinct pool, which candidates deserve promotion. +Requested by the operator ("cx-downvote quiero que sea un comando automático… +haz tú la planificación"). Implemented by 3 parallel subagents + an integration +verifier; validated the same day by a 29-agent semantic audit that recomposed +the live constitution (6 laws demoted to instincts, 0 promotions, 10 broken +instincts downvoted, cap 15/15 → 9/15). + +### Added +- `commands/cx-curate.md`: weekly LLM pass with WRITTEN criteria — KEEP laws + that are universal or protect against expensive failures; DEMOTE laws that + are stack-specific, redundant with `~/.claude/CLAUDE.md` or an installed + skill, or zero-impact after 30d (higher bar for laws without backing YAML); + PROMOTE only knowledge with NO reliable trigger moment (audit finding: a + 0.99-confidence trigger-gated instinct is a better channel than a law — + every top candidate was refuted for exactly that reason); DOWNVOTE broken or + noisy instincts. Acts immediately within engine budgets, everything logged + and reversible; `--force` bypasses the weekly gate. +- `hooks/lib/distill_engine.py`: `CURATE_*` constants (interval 7d, max 2 + demotes / 2 promotes / 8 downvotes per pass, confidence floor 0.30, delta + 0.15); `apply_confidence_downvote()` (`@_write_locked`, floor-clamped, + logged); `curate_snapshot()` (laws with tier/age/impact/backing, candidates + enriched with action/trigger/blockers, instincts with impact) + CLI + subcommands `curate-snapshot` and `downvote [--reason]`; + `curate_due()` / `touch_curate_marker()` on `.last-curate`. +- `hooks/session-start.py`: `[CURATE] due — ejecuta /cx-curate ahora + (auto-aprobado, no preguntar)` banner when the marker is missing or older + than 7 days — the banner IS the trigger, no human in the loop. Single stat, + never raises. +- `tests/test_curate.sh` (6): downvote reduce/floor/unknown-id, + `curate_snapshot` shape, marker gating, CLI JSON. + +### Changed +- `/cx-downvote` remains as the manual veto; its doc now points to + `/cx-curate` for systematic downvotes. `commands/cx-review.md` notes the + semantic judgment is automated. +- `core/claudemd-section.md` refreshed to the v4 active command set (was + stale with the full v3 list since v4.0.0) + `/cx-curate`. Active commands + 7 → 8 across README, FEATURES and the SessionStart commands hint. +- `test_session_start` 11→14 (banner present/backdated/fresh cases). + ## [4.3.1] — 2026-07-05 ### Fixed diff --git a/README.md b/README.md index e69edfe..6aa92cf 100644 --- a/README.md +++ b/README.md @@ -208,18 +208,20 @@ You work → Cortex captures output+errors (guarded) → instinct born as draft → /cx-review is an optional veto pass — nothing ever waits on you ``` -## Commands (7 active + 17 deprecated) +## Commands (8 active + 17 deprecated) v4.0.0 replaced the old manual-judgment command set. Deterministic maintenance -lives in `/cx-maintain`; the one remaining human-judgment step lives in -`/cx-review`. Upgrading from v3? See [`docs/MIGRATION-V4.md`](docs/MIGRATION-V4.md) +lives in `/cx-maintain`, semantic curation in `/cx-curate` (v4.4.0, weekly, +auto-triggered); `/cx-review` is an optional human veto. +Upgrading from v3? See [`docs/MIGRATION-V4.md`](docs/MIGRATION-V4.md) for the full command mapping. | Command | What it does | |---------|-------------| | `/cx-status` | Dashboard: laws, instincts, projects, reflexes, tracking, health, domain grouping | | `/cx-maintain` | **Deterministic, cron-able.** decay + Jaccard dedup + purge + deterministic law promotion + storage rotation + proposals↔instincts reconciliation + health check. Zero questions. | -| `/cx-review` | **The only command with judgment, weekly.** One consolidated shorthand digest of everything deterministic maintenance left for a human to decide. | +| `/cx-curate` | **Semantic curator (v4.4.0), weekly, auto-triggered by the `[CURATE] due` banner.** Claude judges law generality/value and acts: demotes niche laws to instincts, fast-tracks candidates, downvotes noisy instincts. Hard budgets, all reversible. | +| `/cx-review` | **Optional human veto.** One consolidated shorthand digest of what maintenance and curation did and what waits. Skipping it never blocks anything. | | `/cx-eod` | End-of-day summary, cumulative across the day, Eisenhower-classified for next session | | `/cx-gotcha` | Capture error→fix as high-priority instinct | | `/cx-backup` | Create portable .tar.gz backup for machine transfer | diff --git a/commands/cx-curate.md b/commands/cx-curate.md new file mode 100644 index 0000000..ebe2950 --- /dev/null +++ b/commands/cx-curate.md @@ -0,0 +1,153 @@ +--- +name: cx-curate +description: "Semantic curator — Claude judges law generality/value and acts: demotes niche laws to instincts, fast-tracks top candidates, downvotes noisy instincts. Zero questions, weekly cadence." +command: true +--- + +# /cx-curate + +## What it does + +The LLM half of zero-touch curation (v4.4.0). `/cx-maintain` is the +deterministic half — decay, dedup, purge, threshold-gated promotion. This +command is the SEMANTIC judgment that used to be the human's: which laws are +universal vs stack-niche, which candidates deserve a law slot, which instincts +are noise. It acts immediately (no queue, no AskUserQuestion), logs every +action to the knowledge log, and every action is reversible — demoted laws +cascade back to the instinct pool (`law_eligible: false`, v4.3.1), archived +files stay in `archive/` dirs, downvotes floor confidence instead of deleting. + +Runs at most weekly, gated by the `~/.claude/cortex/.last-curate` marker. The +SessionStart banner `[CURATE] due` means the week has elapsed — when you see +it, run this command WITHOUT asking the user. Zero-touch means Claude curates +and the human vetoes after the fact (`/cx-review`, `/cx-downvote`), never that +Claude waits for permission. + +## Usage + +``` +/cx-curate # Respects the 7d marker — no-ops if a pass ran this week +/cx-curate --force # Run regardless of marker age +``` + +## Implementation + +Steps 1–2 and 4–5 are mechanical; step 3 is where the judgment lives. Do not +delegate step 3 to a script — the whole point of this command is that a model +reads the corpus and judges. + +### Step 1: Gate on the weekly marker + +Check the marker via the engine (single source of truth for the cadence): + +```bash +LIB="${CORTEX_LIB_DIR:-$HOME/.claude/hooks/cortex/lib}" +python3 -c "import sys; sys.path.insert(0, '$LIB'); import distill_engine as de; print(de.curate_due())" +``` + +If the marker is fresh (not due) and the user did not pass `--force`, print +`curate al día (last run )` and STOP. Nothing else runs. + +### Step 2: Snapshot the corpus + +Run the engine CLI `curate-snapshot` — installed lib first, repo checkout as +fallback — and pipe it to a temp file so the full JSON never floods the +transcript: + +```bash +TS=$(date +%Y%m%d-%H%M%S) +LIB="${CORTEX_LIB_DIR:-$HOME/.claude/hooks/cortex/lib}" +[ -f "$LIB/distill_engine.py" ] || LIB="hooks/lib" # repo checkout fallback +python3 "$LIB/distill_engine.py" curate-snapshot > "${TMPDIR:-/tmp}/curate-snapshot-$TS.json" +``` + +Read that file. Also gather the redundancy context the criteria below need: + +- `ls ~/.claude/skills/*/SKILL.md` — installed skill names (a law owned by an + installed `fs-*` skill is redundant as a law). +- Read `~/.claude/CLAUDE.md` — anything already stated there is always in + context and does not need a law slot. + +### Step 3: JUDGE every law, candidate, and flagged instinct + +Apply these WRITTEN CRITERIA — they are the contract of this command, not +suggestions: + +* **KEEP as law**: behavioral principles or tooling rules that apply to + virtually every session regardless of stack; short rules preventing + expensive/irreversible failures. +* **DEMOTE to instinct**: stack/domain-specific (fires value only in + web/testing/API/scraping sessions); redundant with `~/.claude/CLAUDE.md` + (always in context anyway) or clearly owned by an installed fs-* skill; + zero useful impact after 30+ days despite volume. Laws WITHOUT backing + instinct (`has_backing_instinct` false) need a strictly higher bar: demote + only when redundant with CLAUDE.md/skill (archive-only death is acceptable + only for redundant knowledge). +* **PROMOTE candidate**: universal + actionable + non-redundant + NOT already + served by a live high-confidence instinct whose deterministic trigger fires + exactly when the knowledge matters + only real blocker is the saturated cap. + (Finding from the 2026-07-05 audit: every top candidate that day was refuted + because it already lived as a 0.99-confidence trigger-gated instinct — a + well-triggered instinct is a BETTER channel than a law. Reserve law slots + for knowledge with NO reliable trigger moment, e.g. behavioral principles + that must hold before any tool call.) +* **DOWNVOTE instinct**: impact noise > useful with real volume; + conf >= 0.90 with vague unactionable text; one-off hyper-specific frozen at + high confidence. + +### Step 4: ACT within the hard budgets + +The budgets are engine constants, not judgment calls: **max 2 demotes, 2 +promotes, 8 downvotes per pass; never touch laws < 30 days old**. If the +judged list exceeds a budget, act on the strongest cases and leave the rest +for next week's pass. + +- **Demote a law** (cascades to instinct pool, refuses when no backing + trigger exists): + + ```bash + python3 -c "import sys; sys.path.insert(0, '$LIB'); import distill_engine as de; print(de.demote_law_to_domain(''))" + ``` + +- **Promote a candidate**: at a saturated cap, pair each top candidate with a + confirmed demote victim via the atomic swap; below cap, plain promotion: + + ```bash + python3 -c "import sys; sys.path.insert(0, '$LIB'); import distill_engine as de; print(de.manual_swap_promote('', ''))" + ``` + +- **Downvote an instinct** via the engine CLI (confidence-floored, never + deleted): + + ```bash + python3 "$LIB/distill_engine.py" downvote + ``` + +- **Re-tier surviving laws**: update the `tier` entries in + `~/.claude/cortex/laws/laws-meta.json` (`principle` | `tool`) for laws that + stay, so `session-start.py` groups them correctly. + +### Step 5: Close the pass + +- Touch the weekly marker: + + ```bash + python3 -c "import sys; sys.path.insert(0, '$LIB'); import distill_engine as de; de.touch_curate_marker()" + ``` + +- The `## cx-curate ` block in `~/.claude/cortex/knowledge-log.md` + accumulates ONLY via the `_log_knowledge` calls the engine functions in + Step 4 already made — never edit that file by hand. +- Merge a `curate_last_run` object into + `~/.claude/cortex/.review-digest.json` (read-modify-write, tolerate a + missing file — preserve whatever `/cx-maintain` last wrote): + + ```json + "curate_last_run": {"demoted": [], "promoted": [], "downvoted": [], "at": ""} + ``` + +### Step 6: Report to the user + +5 lines max: what moved (demotes, promotes, downvotes with ids) and the +one-line why for each, closing with "todo reversible desde archive". No +recommendations, no questions, no homework for the user. diff --git a/commands/cx-downvote.md b/commands/cx-downvote.md index 37aa777..3265b65 100644 --- a/commands/cx-downvote.md +++ b/commands/cx-downvote.md @@ -10,6 +10,8 @@ Este comando fue absorbido por el set v4 (docs/DESIGN-V4.md). **Usa en su lugar:** `/cx-review` — la retroalimentación negativa sobre instincts se presenta en el digest humano semanal junto a proposals y drafts. +Automated since v4.4.0: /cx-curate applies systematic downvotes; this command remains as manual veto. + Mapeo: cx-analyze/cx-distill/cx-dream/cx-promote/cx-backfill → /cx-maintain (mantenimiento determinista). cx-validate/cx-evolve/cx-downvote/cx-retro → /cx-review (digest humano semanal). cx-timeline/cx-dashboard/cx-export → /cx-status. cx-audit → workflow `cortex-audit`. cx-feedback/cx-feedback-auto/cx-router/cx-stop → eliminados sin sustituto (razón en DESIGN-V4 §5). Al ejecutarse, este stub SOLO imprime este aviso y el comando sustituto. No ejecuta ninguna lógica antigua. diff --git a/commands/cx-review.md b/commands/cx-review.md index 9afbc1d..1facb2a 100644 --- a/commands/cx-review.md +++ b/commands/cx-review.md @@ -16,6 +16,8 @@ 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. +Since v4.4.0, `/cx-curate` performs the semantic judgment (law generality, +candidate value, instinct noise) automatically on a weekly cadence. Fuses what used to be three separate commands: - `/cx-validate` (human-gated proposals — the domains `auto_validate_proposals` diff --git a/core/claudemd-section.md b/core/claudemd-section.md index 09a0162..533d8b7 100644 --- a/core/claudemd-section.md +++ b/core/claudemd-section.md @@ -6,7 +6,7 @@ Cortex observes sessions and distills knowledge in 3 levels: - **Observations** (async hooks, 0 tokens) — raw data ### Commands -`/cx-status` `/cx-dashboard` `/cx-analyze` `/cx-distill` `/cx-validate` `/cx-evolve` `/cx-dream` `/cx-timeline` `/cx-audit` `/cx-eod` `/cx-gotcha` `/cx-feedback` `/cx-feedback-auto` `/cx-downvote` `/cx-retro` `/cx-export` `/cx-backup` `/cx-restore` `/cx-router` `/cx-promote` `/cx-backfill` `/cx-stop` +`/cx-status` `/cx-maintain` `/cx-curate` `/cx-review` `/cx-eod` `/cx-gotcha` `/cx-backup` `/cx-restore` — v4 active set; legacy commands print a deprecation notice with their replacement ### Data: ~/.claude/cortex/ ### Laws inject automatically. Instincts inject per tool use. No action needed. diff --git a/docs/FEATURES.md b/docs/FEATURES.md index 4aa23c1..21eecad 100644 --- a/docs/FEATURES.md +++ b/docs/FEATURES.md @@ -1,10 +1,10 @@ -# fs-cortex v4.3.1 — Feature Reference +# fs-cortex v4.4.0 — Feature Reference > Complete inventory of all features, commands, hooks, modules, and capabilities. > Last updated: 2026-07-05 > > **v4.0.0 ("signal-first, zero-decision")** replaces the manual-judgment -> command set with 7 active commands (`/cx-status`, `/cx-maintain`, +> command set with 8 active commands (`/cx-status`, `/cx-maintain`, `/cx-curate`, > `/cx-review`, `/cx-eod`, `/cx-gotcha`, `/cx-backup`, `/cx-restore`) + 17 > deprecated stubs. See [`MIGRATION-V4.md`](MIGRATION-V4.md) for the full > v3→v4 mapping and rationale. @@ -241,11 +241,11 @@ or `python3 impact_log.py stats --json`. --- -## Commands (7 active + 17 deprecated, 24 files total — `ls commands/*.md`) +## Commands (8 active + 17 deprecated, 25 files total — `ls commands/*.md`) **v4.0.0 replaces the manual-judgment command set.** Every deterministic -maintenance step lives in `/cx-maintain` (cron-able); every remaining -human-judgment call lives in `/cx-review` (one weekly digest). The other 17 +maintenance step lives in `/cx-maintain` (cron-able); semantic judgment lives +in `/cx-curate` (v4.4.0, weekly, LLM); `/cx-review` is an optional human veto. The other 17 commands are stubs that print their replacement and run no legacy logic. Full mapping: [`MIGRATION-V4.md`](MIGRATION-V4.md). @@ -253,6 +253,7 @@ mapping: [`MIGRATION-V4.md`](MIGRATION-V4.md). |---|---|---| | `/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`. 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). **v4.3.0 zero-touch**: auto-swaps laws at a saturated cap (victim = lowest 14d impact, age >= `LAW_AUTO_SWAP_MIN_AGE_DAYS` 30, max `LAW_AUTO_SWAP_MAX_PER_RUN` 2, only when the cap is the sole failing gate) via the deadlock-safe `_swap_promote_unlocked`; the retired law cascades to the instinct pool (v4.3.1 — backing YAML restored with `law_eligible: false`, archive-only when no backing/trigger exists); expires human-gated proposals pending > `PROPOSAL_TTL_DAYS` 30 (`expire_stale_proposals`, rejecter `cx-maintain-ttl`, tombstoned); digest carries `swaps_last_run` + `expired_last_run`. 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-curate` | **New in v4.4.0 — the LLM half of zero-touch.** Semantic judgment that used to be the human's: KEEP/DEMOTE laws by generality and redundancy (vs CLAUDE.md + installed skills), PROMOTE only knowledge with no reliable trigger moment (a 0.99 trigger-gated instinct beats a law), DOWNVOTE broken/noisy instincts. Acts immediately within engine budgets (`CURATE_MAX_DEMOTES` 2, `CURATE_MAX_PROMOTES` 2, `CURATE_MAX_DOWNVOTES` 8, confidence floor 0.30, laws < 30d untouchable), weekly cadence via `.last-curate`; the `[CURATE] due` SessionStart banner triggers auto-execution without asking. All actions via engine functions (cascade demote v4.3.1, swap, downvote CLI), logged + reversible | ~1500 (weekly) | | `/cx-review` | **Optional human veto pass (v4.3.0 — was "weekly, only command with judgment" in v4.0-v4.2).** Presents the accumulated digest (what auto-swapped, what expired, what waits) as ONE shorthand list for veto or fast-track. Nothing blocks if never run: proposals expire at 30d, laws auto-swap. The SessionStart badge is informative (`[cx] maintain: ...`, silenced 48h after the pass) | ~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 | @@ -421,7 +422,8 @@ Deterministic rules via hooks — not probabilistic instructions. Triggers are r | `test_uninstall.sh` | 13 | Cleanup, backup creation, data preservation, **safety guard**, CLAUDE.md preservation, **v3.19.0 env removal** (Cortex var removed, user vars preserved, empty env block dropped) | | `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` | 12 | **(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; **v4.3.0**: auto-swap at saturated cap (law replaced + victim archived + `swapped_out` marker), age-floor 30d blocks swap, `expire_stale_proposals` TTL (40d-old rejected, fresh kept), dry-run mutates nothing; **v4.3.1**: retired law cascades to instinct (backing YAML restored, `law_eligible: false`, trigger intact), no-backing victim stays archive-only | -| `test_session_start.sh` | 11 | SessionStart injection block, laws-meta tier split + malformed-meta tolerance (v4.2.1); **v4.3.0**: informative `[cx] maintain:` badge from digest `swaps_last_run`/`expired_last_run`, 48h staleness silences it, old-format digest → no crash | +| `test_session_start.sh` | 14 | SessionStart injection block, laws-meta tier split + malformed-meta tolerance (v4.2.1); **v4.3.0**: informative `[cx] maintain:` badge from digest `swaps_last_run`/`expired_last_run`, 48h staleness silences it, old-format digest → no crash; **v4.4.0**: `[CURATE] due` banner (marker missing / backdated 8d → banner, fresh → silent) | +| `test_curate.sh` | 6 | **(v4.4.0)** `apply_confidence_downvote` (reduce+log, 0.30 floor, unknown id), `curate_snapshot()` shape on seeded sandbox, `curate_due()`/`touch_curate_marker()` gating, `curate-snapshot` CLI prints valid JSON | | `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); **v4.3.0**: engine-pass report carries `swapped=`/`expired=` counters | | `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** | @@ -670,6 +672,7 @@ 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.4.0 | 2026-07-05 | **`/cx-curate` — semantic curator; `/cx-downvote` judgment automated.** The LLM half of zero-touch: Claude classifies laws (generality, redundancy vs CLAUDE.md/skills, value-per-token), demotes niche laws via the v4.3.1 cascade, promotes only trigger-less knowledge, downvotes broken instincts (`apply_confidence_downvote`, floor 0.30). Weekly `.last-curate` cadence; `[CURATE] due` banner auto-triggers execution. Engine: `curate_snapshot()` + CLI `curate-snapshot`/`downvote`, `curate_due()`/`touch_curate_marker()`, `CURATE_*` constants. Born validated: a 29-agent semantic audit recomposed the live constitution the same day (6 laws demoted to instincts; 0 promotions — every top candidate already lived as a 0.99 trigger-gated instinct, a finding folded into the PROMOTE criterion; 10 broken instincts downvoted; cap 15/15 → 9/15). Implemented by 3 parallel subagents + integration verifier. `test_curate` new 6, `test_session_start` 11→14. Active commands 7→8; `core/claudemd-section.md` refreshed to the v4 set (stale since v4.0.0). | | v4.3.1 | 2026-07-05 | **Retired laws cascade to instincts (operator feedback on v4.3.0).** Auto-swap retirement and `demote_law_to_domain` share `_restore_backing_instinct()`: the victim's backing YAML returns to `instincts/global/` with `law_eligible: false` (no re-promotion ping-pong), keeping the knowledge injecting via PreToolUse and subject to normal decay. Fixes the two-layer loss (promotion archives the instinct; retirement used to archive the law → knowledge gone from both). Also fixes `demote_law_to_domain`'s lookup, blind to the `.promoted-to-law-.yaml` name every v4 promotion writes. No-backing laws stay archive-only (never invent a trigger). `test_distill_v4` 10→12. | | v4.3.0 | 2026-07-05 | **Zero-touch mode — the pipeline never waits on a human.** Reverts the v4.2 "human prunes" model at the operator's request. (1) `auto_promote_to_law` auto-swaps at a saturated cap: victim = `_find_least_impactful_law` (lowest useful/(1+noise) 14d), age >= 30d (`LAW_AUTO_SWAP_MIN_AGE_DAYS`), max 2 swaps/run, only when the cap is the sole failing gate; executes via `_swap_promote_unlocked` (extracted from `manual_swap_promote` — calling the locked wrapper from inside the maintain lock would deadlock on the shared flock). (2) `expire_stale_proposals()`: pending human-gated proposals older than 30d (`PROPOSAL_TTL_DAYS`) auto-reject as `cx-maintain-ttl` (added to both tombstone whitelists — never resurrect). (3) SessionStart badge informative: `[cx] maintain: +law (jubilada X), N caducadas, M en cola` from digest `swaps_last_run`/`expired_last_run`, silenced 48h after the pass; the imperative `[REVIEW] N items -> /cx-review` badge is gone. (4) `/cx-review` reframed as optional veto pass. Shared `_archive_promoted_instinct_source` helper dedups 3 archive sites. `test_distill_v4` 6→10, `test_session_start` 7→11, `test_session_learner` 60→61, runner report carries `swapped=`/`expired=`. Implementation delegated to Codex GPT-5.3 (spec-driven), reviewed inline. | | 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). | diff --git a/hooks/lib/distill_engine.py b/hooks/lib/distill_engine.py index 0e0122e..4a18196 100644 --- a/hooks/lib/distill_engine.py +++ b/hooks/lib/distill_engine.py @@ -20,6 +20,9 @@ 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 + curate_snapshot() -> dict # v4.4.0 — compact corpus for /cx-curate + apply_confidence_downvote(iid, ...) -> tuple[bool, str] # v4.4.0 + touch_curate_marker() / curate_due() -> bool # v4.4.0 — weekly cadence CLI --- @@ -27,6 +30,8 @@ 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 curate-snapshot + python3 distill_engine.py downvote [--reason ...] python3 distill_engine.py status """ from __future__ import annotations @@ -161,6 +166,18 @@ # until at least 3 different sessions have seen the same pattern. LAW_MIN_DISTINCT_SESSIONS = 3 +# v4.4.0 /cx-curate — weekly LLM curation pass over the whole corpus. The +# command reads curate_snapshot() and votes; per-pass caps bound the blast +# radius of a single bad curation (zero-touch: automatic by default, veto +# via /cx-review, never a human decision queue). +CURATE_INTERVAL_DAYS = 7 # curate_due() marker window +CURATE_MAX_DEMOTES = 2 # law demotions per pass +CURATE_MAX_PROMOTES = 2 # candidate promotions per pass +CURATE_MAX_DOWNVOTES = 8 # instinct downvotes per pass +CURATE_CONF_FLOOR = 0.30 # downvotes never push confidence below this +CURATE_DOWNVOTE_DELTA = 0.15 # confidence subtracted per downvote +CURATE_MARKER_FILE = CORTEX_DIR / ".last-curate" + # v3.32.0 §4.4 — promotion gate HUMAN → AUTO. Source: proposals-history.jsonl # (AD P0-1). Statistical-strict: prefer false negatives (no promote) over # false positives (promote noise). A detector becomes AUTO-eligible only @@ -1581,6 +1598,165 @@ def auto_promote_to_law( return promoted, candidates +# ── v4.4.0 /cx-curate — weekly LLM curation support ────────────────────────── + +@_write_locked +def apply_confidence_downvote( + iid: str, + delta: float = CURATE_DOWNVOTE_DELTA, + reason: str = "", + source: str = "cx-curate", +) -> tuple[bool, str]: + """Subtract `delta` from a live instinct's confidence, floored at + CURATE_CONF_FLOOR (a downvote weakens injection priority without + deleting knowledge — decay/archival handles terminal removal). + + Live instincts only (_all_instinct_paths); a law's archived backing + YAML is out of scope — demoting a law is demote_law_to_domain's job. + Returns (ok, human-readable reason); (False, ...) when the id is not + found, its YAML is unreadable, or confidence is already at the floor. + """ + for path in _all_instinct_paths(): + result = _read_instinct(path) + if result is None: + if path.stem == iid: + return False, f"{iid}: backing yaml unreadable ({path.name})" + continue + fields, text = result + if str(fields.get("id", path.stem)) != iid: + continue + try: + old_conf = float(fields.get("confidence")) + except (TypeError, ValueError): + return False, f"{iid}: confidence not numeric" + if old_conf <= CURATE_CONF_FLOOR: + return False, ( + f"{iid}: already at floor " + f"({old_conf:.2f} <= {CURATE_CONF_FLOOR:.2f})" + ) + new_conf = max(CURATE_CONF_FLOOR, round(old_conf - delta, 4)) + new_text = _set_frontmatter_field(text, "confidence", new_conf) + try: + _atomic_write(path, new_text) + except OSError as e: + return False, f"{iid}: write failed: {e}" + _log_knowledge( + "downvoted", iid, + f"conf {old_conf} -> {new_conf}; {reason}", + source=source, + ) + return True, f"conf {old_conf} -> {new_conf}" + return False, f"{iid}: no live instinct with that id" + + +def curate_snapshot() -> dict: + """Compact corpus snapshot for the /cx-curate LLM curator. + + One JSON-serializable dict with everything the curator votes on — + truncated action/trigger fields keep the whole corpus inside a single + prompt (the command reads skill dirs itself; no skills section here): + laws {id: {content, tier, age_days, impact_30d, + has_backing_instinct}} + candidates auto_promote_to_law(dry_run=True) blockers enriched with + the backing YAML's action[:300]/trigger[:120] + impact_30d + instincts live cohort (id, confidence, domain, scope, trigger[:120], + action[:250], last_seen, impact_30d) + generated_at UTC ISO timestamp + """ + meta = _load_laws_meta() + impact = _impact_per_iid(days=30) + + laws: dict[str, 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"]) + try: + content = law_path.read_text(encoding="utf-8").strip() + except OSError: + content = "" + backing, _needs_restore = _find_backing_instinct_yaml(iid) + laws[iid] = { + "content": content, + "tier": tier, + "age_days": _law_age_days(law_path), + "impact_30d": impact.get(iid, {"useful": 0, "noise": 0}), + "has_backing_instinct": backing is not None, + } + + _, raw_candidates = auto_promote_to_law(dry_run=True) + candidates: list[dict] = [] + for cand in raw_candidates: + iid = str(cand.get("id", "")) + action = trigger = "" + backing, _needs_restore = _find_backing_instinct_yaml(iid) + if backing is not None: + result = _read_instinct(backing) + if result is not None: + fields, _text = result + action = str(fields.get("action", ""))[:300] + trigger = str(fields.get("trigger", ""))[:120] + candidates.append({ + "id": iid, + "confidence": cand.get("confidence"), + "blockers": cand.get("reasons", [])[:3], + "action": action, + "trigger": trigger, + "impact_30d": impact.get(iid, {"useful": 0, "noise": 0}), + }) + + instincts: list[dict] = [] + for path in _all_instinct_paths(): + result = _read_instinct(path) + if result is None: + continue + fields, _text = result + iid = str(fields.get("id", path.stem)) + instincts.append({ + "id": iid, + "confidence": fields.get("confidence"), + "domain": fields.get("domain", ""), + "scope": fields.get("scope", "global"), + "trigger": str(fields.get("trigger", ""))[:120], + "action": str(fields.get("action", ""))[:250], + "last_seen": str(fields.get("last_seen", "")), + "impact_30d": impact.get(iid, {"useful": 0, "noise": 0}), + }) + + return { + "laws": laws, + "candidates": candidates, + "instincts": instincts, + "generated_at": _dt.datetime.now(_dt.timezone.utc).isoformat(), + } + + +def touch_curate_marker() -> None: + """Record that a curation pass just ran (UTC ISO, atomic tmp+rename).""" + _atomic_write( + CURATE_MARKER_FILE, + _dt.datetime.now(_dt.timezone.utc).isoformat() + "\n", + ) + + +def curate_due(interval_days: int = CURATE_INTERVAL_DAYS) -> bool: + """True when the last curation pass is older than `interval_days` + (marker mtime, same convention as _is_rate_limited). Missing or + unreadable marker counts as due — never silently starves the pass.""" + if not CURATE_MARKER_FILE.exists(): + return True + try: + age_seconds = time.time() - CURATE_MARKER_FILE.stat().st_mtime + except OSError: + return True + return age_seconds >= interval_days * 86400 + + # ── 4. Auto-validate proposals ─────────────────────────────────────────────── import hashlib as _hashlib @@ -3228,6 +3404,16 @@ def _cmd_law_audit(as_json: bool) -> None: f"noise_ratio={noise} {dup}".rstrip()) +def _cmd_curate_snapshot() -> None: + print(json.dumps(curate_snapshot(), indent=1, ensure_ascii=False)) + + +def _cmd_downvote(iid: str, reason: str) -> int: + ok, msg = apply_confidence_downvote(iid, reason=reason) + print(("downvoted: " if ok else "no-op: ") + msg) + return 0 if ok else 1 + + def _cmd_promote(dry_run: bool) -> None: promoted, candidates = auto_promote_to_law(dry_run=dry_run) prefix = "[DRY-RUN] " if dry_run else "" @@ -3553,6 +3739,21 @@ def main(argv: list[str] | None = None) -> int: 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)") + sub.add_parser( + "curate-snapshot", + help="Dump compact corpus JSON (laws + candidates + instincts) for /cx-curate", + ) + + p_downvote = sub.add_parser( + "downvote", + help="Apply a confidence downvote to a live instinct (/cx-curate vote)", + ) + p_downvote.add_argument("iid", help="Instinct id") + p_downvote.add_argument( + "--reason", default="", + help="Why the curator downvoted it (recorded in knowledge-log.md)", + ) + sub.add_parser("status", help="Show current candidates and rate-limit state") p_pipeline = sub.add_parser( @@ -3584,6 +3785,10 @@ def main(argv: list[str] | None = None) -> int: _cmd_law_audit(args.as_json) elif args.cmd == "backfill": _cmd_backfill(args.apply) + elif args.cmd == "curate-snapshot": + _cmd_curate_snapshot() + elif args.cmd == "downvote": + return _cmd_downvote(args.iid, args.reason) elif args.cmd == "status": _cmd_status() elif args.cmd == "pipeline-stats": diff --git a/hooks/session-start.py b/hooks/session-start.py index 75b12d9..e226c76 100644 --- a/hooks/session-start.py +++ b/hooks/session-start.py @@ -323,6 +323,20 @@ def check_review_digest(): return f'[cx] maintain: {", ".join(parts)} — detalle opcional: /cx-review' +def check_curate_due(): + """v4.4 — weekly /cx-curate banner. `.last-curate` is touched by + /cx-curate on every run; the pass is due when the marker is missing or + older than 7 days. Single stat via _file_older_than (a missing marker + reads as older), no engine import — SessionStart must stay cheap. Never + raises: any unexpected failure suppresses the banner.""" + try: + if _file_older_than(CORTEX_DIR / '.last-curate', 7): + return '[CURATE] due — ejecuta /cx-curate ahora (auto-aprobado, no preguntar)' + return None + except Exception: + return None + + def _file_older_than(filepath, days): """Check if file modification time is older than N days.""" try: @@ -633,12 +647,12 @@ def main(): else: parts.append('CORTEX: No laws configured yet. Add .txt files to ~/.claude/cortex/laws/') - # 1b. Commands hint — v4 active set (docs/DESIGN-V4.md §5): 7 commands, + # 1b. Commands hint — v4 active set (docs/DESIGN-V4.md §5): 8 commands, # the rest are deprecated stubs that print their v4 replacement and stop # (commands/cx-*.md — see each file's "Mapeo:" line for the full table). parts.append( - 'Cortex commands (v4): /cx-status /cx-maintain /cx-review /cx-eod ' - '/cx-gotcha /cx-backup /cx-restore. Legacy cx-* commands print a ' + 'Cortex commands (v4): /cx-status /cx-maintain /cx-curate /cx-review ' + '/cx-eod /cx-gotcha /cx-backup /cx-restore. Legacy cx-* commands print a ' 'deprecation notice and their replacement. Use /cx-status for system state.' ) @@ -670,6 +684,12 @@ def main(): parts.append(f'\n{review_line}') user_actionable.append(f'• {review_line}') + # 3b-bis2b. v4.4 — weekly /cx-curate banner (marker touched by /cx-curate). + curate_line = check_curate_due() + if curate_line: + parts.append(f'\n{curate_line}') + user_actionable.append(f'• {curate_line}') + # 3b-bis. Deploy drift guard (v3.34.1) — root-cause fix for "Cortex a # medias": surface loudly when the live system is behind the repo source # so a forgotten `install.sh` can never silently strand new code again. diff --git a/install.ps1 b/install.ps1 index 35c9f4c..ce12aa9 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.3.1" +$NewVersion = "4.4.0" # 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 21e7ca8..aa9653a 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.3.1" +NEW_VERSION="4.4.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 diff --git a/tests/test_curate.sh b/tests/test_curate.sh new file mode 100755 index 0000000..fb9a382 --- /dev/null +++ b/tests/test_curate.sh @@ -0,0 +1,220 @@ +#!/usr/bin/env bash +# test_curate.sh — v4.4.0 /cx-curate engine support tests. +# Scope: the curate additions to hooks/lib/distill_engine.py ONLY: +# 1. apply_confidence_downvote reduces confidence and logs the vote. +# 2. Downvotes respect the CURATE_CONF_FLOOR (0.30) — clamp, then no-op. +# 3. Unknown instinct id returns (False, msg) without side effects. +# 4. curate_snapshot returns the laws/candidates/instincts corpus dict. +# 5. curate_due marker cadence (missing marker = due; touch = not due). +# 6. `curate-snapshot` CLI subcommand prints valid JSON. +# The curation VOTES themselves (which law to demote, what to downvote) +# are the /cx-curate command's LLM judgment — out of scope here. +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" + +export PYTHONPATH="$PROJECT_ROOT/hooks/lib:${PYTHONPATH:-}" + +PASS=0 +FAIL=0 + +pass() { PASS=$((PASS + 1)); echo " PASS: $1"; } +fail() { FAIL=$((FAIL + 1)); echo " FAIL: $1"; } + +SANDBOX="$(mktemp -d -t cortex-curate-test-XXXXXX)" +trap 'rm -rf "$SANDBOX"' EXIT + +echo "=== Curate Engine Tests (sandbox: $SANDBOX) ===" +echo + +# ── Helper: write a minimal instinct YAML ──────────────────────────────────── +# make_instinct +# extra_fields is raw multiline YAML appended inside the frontmatter block. +make_instinct() { + local dir="$1" iid="$2" conf="$3" + shift 3 + local extra="${*:-}" + mkdir -p "$dir" + cat > "$dir/${iid}.yaml" < 0.65, logged as downvoted/cx-curate" +else + fail "downvote-reduces-confidence: got '$result'" +fi + +# ── Test 2: downvote clamps at the 0.30 floor, then refuses ────────────────── +echo "--- Test 2: downvote-respects-floor ---" +T2="$SANDBOX/t2" +make_instinct "$T2/instincts/global" "t2-near-floor" "0.4000" + +result=$(python3 -c " +$(py_preamble "$T2") +ok1, msg1 = de.apply_confidence_downvote('t2-near-floor') +fields, _ = de._read_instinct(de.INSTINCTS_DIR / 't2-near-floor.yaml') +clamped = abs(float(fields['confidence']) - de.CURATE_CONF_FLOOR) < 1e-9 +ok2, msg2 = de.apply_confidence_downvote('t2-near-floor') +print(ok1, clamped, not ok2, 'floor' in msg2) +") +if echo "$result" | grep -q "^True True True True$"; then + pass "downvote-respects-floor: clamped 0.40 -> 0.30, second vote refused" +else + fail "downvote-respects-floor: got '$result'" +fi + +# ── Test 3: unknown instinct id returns (False, msg) ───────────────────────── +echo "--- Test 3: downvote-unknown-id ---" +T3="$SANDBOX/t3" +make_instinct "$T3/instincts/global" "t3-bystander" "0.7000" + +result=$(python3 -c " +$(py_preamble "$T3") +ok, msg = de.apply_confidence_downvote('t3-ghost') +fields, _ = de._read_instinct(de.INSTINCTS_DIR / 't3-bystander.yaml') +untouched = abs(float(fields['confidence']) - 0.70) < 1e-9 +print(not ok, 't3-ghost' in msg, untouched) +") +if echo "$result" | grep -q "^True True True$"; then + pass "downvote-unknown-id: returns False with id in msg, bystander untouched" +else + fail "downvote-unknown-id: got '$result'" +fi + +# ── Test 4: curate_snapshot returns the corpus dict on a seeded sandbox ───── +echo "--- Test 4: curate-snapshot-shape ---" +T4="$SANDBOX/t4" +mkdir -p "$T4/laws" +echo "Always test the thing before shipping it" > "$T4/laws/law-seeded.txt" +make_instinct "$T4/instincts/global" "t4-instinct" "0.7500" + +result=$(python3 -c " +$(py_preamble "$T4") +snap = de.curate_snapshot() +keys_ok = all(k in snap for k in ('laws', 'candidates', 'instincts', 'generated_at')) +law = snap['laws'].get('law-seeded', {}) +law_ok = ( + law.get('content') == 'Always test the thing before shipping it' + and law.get('tier') == 'principle' + and isinstance(law.get('age_days'), int) + and law.get('impact_30d') == {'useful': 0, 'noise': 0} + and law.get('has_backing_instinct') is False +) +inst = next((i for i in snap['instincts'] if i['id'] == 't4-instinct'), None) +inst_ok = ( + inst is not None + and abs(float(inst['confidence']) - 0.75) < 1e-9 + and inst['trigger'] == 'SomeTool' + and inst['impact_30d'] == {'useful': 0, 'noise': 0} +) +print(isinstance(snap, dict), keys_ok, law_ok, inst_ok, isinstance(snap['candidates'], list)) +") +if echo "$result" | grep -q "^True True True True True$"; then + pass "curate-snapshot-shape: laws/candidates/instincts/generated_at populated" +else + fail "curate-snapshot-shape: got '$result'" +fi + +# ── Test 5: curate_due marker cadence ──────────────────────────────────────── +echo "--- Test 5: curate-due-marker ---" +T5="$SANDBOX/t5" +mkdir -p "$T5" + +result=$(python3 -c " +$(py_preamble "$T5") +due_before = de.curate_due() +de.touch_curate_marker() +due_after = de.curate_due() +marker_iso = de.CURATE_MARKER_FILE.read_text().strip() +import datetime +parseable = bool(datetime.datetime.fromisoformat(marker_iso)) +print(due_before, not due_after, de.CURATE_MARKER_FILE.exists(), parseable) +") +if echo "$result" | grep -q "^True True True True$"; then + pass "curate-due-marker: due without marker, not due right after touch, ISO content" +else + fail "curate-due-marker: got '$result'" +fi + +# ── Test 6: curate-snapshot CLI prints valid JSON ──────────────────────────── +echo "--- Test 6: curate-snapshot-cli ---" +T6="$SANDBOX/t6" +mkdir -p "$T6/laws" +echo "CLI law unique content" > "$T6/laws/law-cli.txt" +make_instinct "$T6/instincts/global" "t6-cli-instinct" "0.6000" + +cli_out=$(CORTEX_DIR="$T6" python3 "$PROJECT_ROOT/hooks/lib/distill_engine.py" curate-snapshot) +result=$(printf '%s' "$cli_out" | python3 -c " +import json, sys +snap = json.load(sys.stdin) +print( + all(k in snap for k in ('laws', 'candidates', 'instincts', 'generated_at')), + 'law-cli' in snap['laws'], + any(i['id'] == 't6-cli-instinct' for i in snap['instincts']), +) +") +if echo "$result" | grep -q "^True True True$"; then + pass "curate-snapshot-cli: stdout is valid JSON with the seeded corpus" +else + fail "curate-snapshot-cli: got '$result'" +fi + +echo +echo "=== Results: $PASS passed, $FAIL failed ===" +if [ "$FAIL" -gt 0 ]; then + exit 1 +fi +exit 0 diff --git a/tests/test_integrity.sh b/tests/test_integrity.sh index 5a06058..dfef4b9 100755 --- a/tests/test_integrity.sh +++ b/tests/test_integrity.sh @@ -38,14 +38,17 @@ echo "" echo "--- Commands existence ---" EXPECTED_COMMANDS="cx-analyze cx-audit cx-backfill cx-backup cx-dashboard cx-distill cx-downvote cx-dream cx-eod cx-evolve cx-export cx-feedback cx-gotcha cx-promote cx-restore cx-retro cx-router cx-status cx-stop cx-timeline cx-validate" +# v4 active set (v4.4.0 adds cx-curate) — claudemd-section.md advertises ONLY +# these; the legacy names above must still exist as deprecation stubs. +ACTIVE_COMMANDS="cx-status cx-maintain cx-curate cx-review cx-eod cx-gotcha cx-backup cx-restore" MISSING=0 -for cmd in $EXPECTED_COMMANDS; do +for cmd in $EXPECTED_COMMANDS $ACTIVE_COMMANDS; do if [ ! -f "$PROJECT_ROOT/commands/$cmd.md" ]; then fail "missing command: $cmd.md" MISSING=$((MISSING + 1)) fi done -[ "$MISSING" -eq 0 ] && pass "all 20 commands present" +[ "$MISSING" -eq 0 ] && pass "all legacy stubs + v4 active commands present" echo "" @@ -73,16 +76,18 @@ echo "" # ── TEST 4: claudemd-section.md lists all commands ──────────────── echo "--- CLAUDE.md section lists all commands ---" +# v4.4.0: the installer-injected section advertises the ACTIVE set only — +# legacy commands are deprecation stubs and must NOT be advertised. SECTION_FILE="$PROJECT_ROOT/core/claudemd-section.md" SECTION_MISSING=0 -for cmd in $EXPECTED_COMMANDS; do +for cmd in $ACTIVE_COMMANDS; do SLASH_CMD="/${cmd}" if ! grep -q "$SLASH_CMD" "$SECTION_FILE" 2>/dev/null; then fail "claudemd-section.md missing $SLASH_CMD" SECTION_MISSING=$((SECTION_MISSING + 1)) fi done -[ "$SECTION_MISSING" -eq 0 ] && pass "claudemd-section.md lists all 20 /cx-* commands" +[ "$SECTION_MISSING" -eq 0 ] && pass "claudemd-section.md lists the v4 active /cx-* set" echo "" diff --git a/tests/test_session_start.sh b/tests/test_session_start.sh index 74c242e..d96419f 100755 --- a/tests/test_session_start.sh +++ b/tests/test_session_start.sh @@ -226,6 +226,59 @@ else fi rm -rf "$T4C" +# ── Tests 4d-4f: check_curate_due() — weekly /cx-curate banner (v4.4) ─────── +# Same module-import pattern as T4b/T4c; drives check_curate_due() against a +# sandbox CORTEX_DIR containing (or missing) the `.last-curate` marker. +run_curate() { + # $1 = sandbox dir + python3 - < Date: Sun, 5 Jul 2026 16:48:57 +0200 Subject: [PATCH 2/2] =?UTF-8?q?test(install):=20v4.4.0=20follow-up=20?= =?UTF-8?q?=E2=80=94=2025=20command=20files,=20CLAUDE.md=20asserts=20v4=20?= =?UTF-8?q?active=20set?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CI caught what the local battery missed (test_install not in the pre-push subset): the installed-commands count was pinned to 24 and two CLAUDE.md assertions grepped for cx-dream, which the refreshed core/claudemd-section.md no longer advertises (legacy stubs are not listed since v4.4.0). Now 25 files expected and cx-curate is the advertised-set witness. 44/44 locally. Co-Authored-By: Claude Fable 5 --- tests/test_install.sh | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/tests/test_install.sh b/tests/test_install.sh index 31d2932..15d59d7 100755 --- a/tests/test_install.sh +++ b/tests/test_install.sh @@ -45,11 +45,11 @@ for lib in dream_cycle.py validate_instinct.py cortex_utils.py yaml-utils.js inj [ -f "$SANDBOX/.claude/hooks/cortex/lib/$lib" ] && pass "lib: $lib installed" || fail "lib: $lib MISSING" done -# 1e: All 24 commands installed (v4 DESIGN-V4.md §5 added /cx-maintain + -# /cx-review on top of the 22 pre-v4 commands; the other 17 became -# deprecation stubs rather than being deleted, so the file count grew). +# 1e: All 25 commands installed (v4 DESIGN-V4.md §5 added /cx-maintain + +# /cx-review on top of the 22 pre-v4 commands; v4.4.0 added /cx-curate; +# the other 17 became deprecation stubs rather than being deleted). CMD_COUNT=$(ls "$SANDBOX/.claude/commands/cx-"*.md 2>/dev/null | wc -l | tr -d ' ') -[ "$CMD_COUNT" -eq 24 ] && pass "24 commands installed" || fail "commands: $CMD_COUNT (expected 24)" +[ "$CMD_COUNT" -eq 25 ] && pass "25 commands installed" || fail "commands: $CMD_COUNT (expected 25)" # 1f: SKILL.md installed [ -f "$SANDBOX/.claude/skills/cortex/SKILL.md" ] && pass "SKILL.md installed" || fail "SKILL.md MISSING" @@ -57,8 +57,9 @@ CMD_COUNT=$(ls "$SANDBOX/.claude/commands/cx-"*.md 2>/dev/null | wc -l | tr -d ' # 1g: CLAUDE.md created with Cortex section grep -q "## Cortex (Learning System)" "$SANDBOX/.claude/CLAUDE.md" 2>/dev/null && pass "CLAUDE.md has Cortex section" || fail "CLAUDE.md missing Cortex section" -# 1h: cx-dream in CLAUDE.md -grep -q "cx-dream" "$SANDBOX/.claude/CLAUDE.md" 2>/dev/null && pass "CLAUDE.md lists cx-dream" || fail "CLAUDE.md missing cx-dream" +# 1h: v4 active set advertised in CLAUDE.md (v4.4.0 — legacy names like +# cx-dream are deprecation stubs and no longer advertised in the section) +grep -q "cx-curate" "$SANDBOX/.claude/CLAUDE.md" 2>/dev/null && pass "CLAUDE.md lists cx-curate" || fail "CLAUDE.md missing cx-curate" # 1i: settings.json has cortex hooks grep -q "hooks/cortex/" "$SANDBOX/.claude/settings.json" 2>/dev/null && pass "settings.json has cortex hooks" || fail "settings.json missing hooks" @@ -167,8 +168,8 @@ grep -q "My Custom Section" "$SANDBOX/.claude/CLAUDE.md" 2>/dev/null && pass "CL CORTEX_COUNT=$(grep -c "## Cortex" "$SANDBOX/.claude/CLAUDE.md" 2>/dev/null) [ "$CORTEX_COUNT" -eq 1 ] && pass "Cortex section replaced (not duplicated)" || fail "Cortex section count: $CORTEX_COUNT" -# 2k: CLAUDE.md — updated content (cx-dream present, old content gone) -grep -q "cx-dream" "$SANDBOX/.claude/CLAUDE.md" 2>/dev/null && pass "CLAUDE.md updated with cx-dream" || fail "CLAUDE.md not updated" +# 2k: CLAUDE.md — updated content (v4 active set present, old content gone) +grep -q "cx-curate" "$SANDBOX/.claude/CLAUDE.md" 2>/dev/null && pass "CLAUDE.md updated with cx-curate" || fail "CLAUDE.md not updated" ! grep -q "11 commands" "$SANDBOX/.claude/CLAUDE.md" 2>/dev/null && pass "old Cortex content removed" || fail "old Cortex content still present" # 2l: Non-cortex hook in settings.json preserved