diff --git a/CHANGELOG.md b/CHANGELOG.md index 9fcaeba..ae062ac 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,27 @@ # Changelog +## v4.5.0 (2026-04-18) — unreleased + +### Added — Laws tier (inspired by fs-cortex, credit: Fernando Montero / MIT) +- **`core/_laws-injector.sh`** (new SessionStart hook): emits `SESSION_PREFETCH_LAWS:` lines from `~/.claude/skills/_laws/*.txt`. Max 10 laws per session, cap ~300 tokens (conservative 4 chars/token). Rotates by `last_injected` ASC for variety across sessions. Atomic write (tmp → rename) survives concurrent sessions. +- **`seeds/laws/*.txt`** (5 ported seeds, MIT © Fernando Montero): `read-first`, `grep-read-verify`, `git-triple-check`, `never-hardcode-secrets`, `three-layer-security`. +- **`seeds/laws/README.md`**: schema + attribution. +- **Law model**: `Law = one-line projection of a permanent instinct (or stand-alone seed)`. `source_instinct: null` → stand-alone. `/promote --law` distills a permanent instinct into a Law (see `commands/promote.md` Step 4). +- **10 TDD tests** (`tests/test-laws.sh`): presence, seed count, empty-dir silent exit, emission format, MAX_LAWS cap, `last_injected` atomic update, rotation ordering, malformed-file resilience, whitespace normalization. 10/10 GREEN. + +### Changed +- `install.sh`: copies `_laws-injector.sh` + seed laws into `~/.claude/skills/_laws/` (idempotent; existing files preserve their `last_injected`). +- `core/settings.template.json`: adds `SessionStart` hooks section (`_hooks_total`: 6 → 7, `_hooks_breakdown`: `SessionStart(1) + PreToolUse(4) + PostToolUse(1) + Stop(1)`). +- `commands/promote.md`: adds Step 4 "Offer Law Distillation" — when an instinct is promoted to `permanent`, user can accept a suggested one-liner to write into `_laws/`. + +### Rationale +Sinapsis already projected patterns as instincts (regex-gated, per-tool-use injection). What was missing: a thin tier of **always-on crystallized wisdom** — the 5-10 principles the user never wants to risk forgetting, no matter the context. Matches fs-cortex's `laws` concept without replacing instincts: laws are a *projection* of permanents, not a parallel registry. Existing commands (`/promote`, `/evolve`) keep working unchanged; the hook is purely additive at the end of the SessionStart array. + +### Test badge +116 → 126 passing (suite-wide, 8 test files). + +--- + ## v4.4.1 (2026-04-17) ### Fixed diff --git a/commands/promote.md b/commands/promote.md index 0b98b70..0dec90f 100644 --- a/commands/promote.md +++ b/commands/promote.md @@ -63,14 +63,43 @@ PROMOTE TO PERMANENT 1. Update `level` from `confirmed` to `permanent` in `_instincts-index.json` 2. Preserve all other fields (trigger_pattern, inject, domain, origin, added) -### Step 4: Summary +### Step 4: Offer Law Distillation (v4.5+) + +For each freshly promoted instinct, ask whether to distill a `Law` — a one-liner projection injected at SessionStart via `_laws-injector.sh`. Laws are cheaper than instincts (no regex match, always injected) and best suited for universal principles the user never wants to forget. + +``` + git-commit-conventional is now permanent. + Distill as Law? (always injected at SessionStart, ~300 token budget) + + Suggested: "Use conventional commits (feat/fix/chore/docs/refactor). Subject <=72 chars." + Edit? [Enter to accept, type replacement, or [S] skip] +``` + +On accept, write to `~/.claude/skills/_laws/.txt`: + +``` +--- +id: +source_instinct: +source: /promote +created: +last_injected: 1970-01-01T00:00:00Z +--- + +``` + +Heuristic for suggested text: take `instinct.inject`, trim to one clause, keep <=40 tokens. If user types replacement, use verbatim. + +### Step 5: Summary ``` PROMOTION COMPLETE 2 instincts promoted to permanent. + 1 distilled as Law. Permanent: 1 → 3 Confirmed: 3 → 1 + Laws: 5 → 6 These instincts now have highest priority in domain dedup. ``` diff --git a/core/_laws-injector.sh b/core/_laws-injector.sh new file mode 100644 index 0000000..6ece106 --- /dev/null +++ b/core/_laws-injector.sh @@ -0,0 +1,93 @@ +#!/bin/bash +# Sinapsis v4.5 — Laws Injector (SessionStart hook) +# +# Always-injected one-liner tier: crystallized wisdom projected from permanent +# instincts (or fs-cortex seed stand-alones). Max 10 laws per session, cap ~300 +# tokens, rotation by last_injected ASC for variety across sessions. +# +# Inspired by fs-cortex laws tier — credit: Fernando Montero (MIT, 2026). +# +# Model: +# permanent instinct (full entry in _instincts-index.json) +# | +# v optionally distilled via /promote --law +# Law (one-liner in _laws/.txt, injected at SessionStart) + +LAWS_DIR="$HOME/.claude/skills/_laws" + +[ ! -d "$LAWS_DIR" ] && exit 0 + +if [ "${SINAPSIS_DEBUG:-}" = "1" ]; then + exec 2>>"$HOME/.claude/skills/_sinapsis-debug.log" +fi + +node -e ' +const fs = require("fs"); +const path = require("path"); + +const LAWS_DIR = process.env.HOME + "/.claude/skills/_laws"; +const MAX_LAWS = 10; +const TOKEN_BUDGET = 300; // conservative cap; Claude-style estimate ~4 chars/token +const CHARS_PER_TOKEN = 4; + +function parseLaw(filePath) { + const content = fs.readFileSync(filePath, "utf8"); + const m = content.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n([\s\S]*)$/); + if (!m) return null; + const fm = {}; + for (const line of m[1].split(/\r?\n/)) { + const kv = line.match(/^([\w-]+):\s*(.*)$/); + if (kv) fm[kv[1]] = kv[2].trim(); + } + const text = m[2].trim().replace(/\s+/g, " "); + if (!fm.id || !text) return null; + return Object.assign(fm, { text, filePath }); +} + +let files; +try { + files = fs.readdirSync(LAWS_DIR).filter(f => f.endsWith(".txt")); +} catch(e) { process.exit(0); } +if (!files.length) process.exit(0); + +const laws = []; +for (const f of files) { + try { + const law = parseLaw(path.join(LAWS_DIR, f)); + if (law) laws.push(law); + } catch(e) { /* skip malformed */ } +} +if (!laws.length) process.exit(0); + +// Rotation: oldest last_injected first → round-robin variety over sessions. +laws.sort((a, b) => (a.last_injected || "").localeCompare(b.last_injected || "")); + +const selected = []; +let tokenCount = 0; +for (const law of laws) { + if (selected.length >= MAX_LAWS) break; + const lawTokens = Math.ceil(law.text.length / CHARS_PER_TOKEN); + if (tokenCount + lawTokens > TOKEN_BUDGET) break; + selected.push(law); + tokenCount += lawTokens; +} +if (!selected.length) process.exit(0); + +for (const law of selected) { + process.stdout.write("SESSION_PREFETCH_LAWS: " + law.text + "\n"); +} + +// Atomic last_injected update (write tmp → rename). Survives concurrent sessions. +const now = new Date().toISOString(); +for (const law of selected) { + try { + const content = fs.readFileSync(law.filePath, "utf8"); + const updated = content.replace(/^(last_injected:\s*).*$/m, "$1" + now); + const tmp = law.filePath + ".tmp"; + fs.writeFileSync(tmp, updated); + fs.renameSync(tmp, law.filePath); + } catch(e) { /* best-effort; next session retries */ } +} +' 2>/dev/null + +exit 0 diff --git a/core/settings.template.json b/core/settings.template.json index 883a59e..582efaf 100644 --- a/core/settings.template.json +++ b/core/settings.template.json @@ -1,8 +1,21 @@ { - "_comment": "Sinapsis v4.1 — settings.json template. Copy to ~/.claude/settings.json (merge with existing).", - "_hooks_total": 6, - "_hooks_breakdown": "PreToolUse(4) + PostToolUse(1) + Stop(1)", + "_comment": "Sinapsis v4.5 — settings.json template. Copy to ~/.claude/settings.json (merge with existing).", + "_hooks_total": 7, + "_hooks_breakdown": "SessionStart(1) + PreToolUse(4) + PostToolUse(1) + Stop(1)", "hooks": { + "SessionStart": [ + { + "matcher": "startup", + "hooks": [ + { + "_comment": "laws-injector: emits SESSION_PREFETCH_LAWS lines (max 10, cap ~300 tokens, rotation)", + "type": "command", + "command": "bash ~/.claude/skills/_laws-injector.sh", + "timeout": 3 + } + ] + } + ], "PreToolUse": [ { "matcher": "*", diff --git a/docs/FEATURES.md b/docs/FEATURES.md index e86e7ba..55ca62c 100644 --- a/docs/FEATURES.md +++ b/docs/FEATURES.md @@ -1,8 +1,8 @@ -# Sinapsis v4.3.3 — Feature Reference +# Sinapsis v4.5 — Feature Reference > Complete inventory of all features, commands, hooks, modules, and capabilities. > **Update this file with every feat/fix commit.** -> Last updated: 2026-04-13 +> Last updated: 2026-04-18 --- @@ -31,13 +31,14 @@ Parallel systems (not part of the confidence pipeline): | Injection Point | What | When | Tokens | |---|---|---|---| -| **SessionStart** | Project context.md + EOD resume | Once per session (via _project-context.sh) | ~150 | +| **SessionStart** | Project context.md + EOD resume + **Laws (v4.5)** | Once per session | ~150 + ~300 laws | | **PreToolUse** | Instincts (max 3) + Passive Rules (max 3) | Every tool use (if trigger matches) | ~200 max | -### 6-Hook Pipeline +### 7-Hook Pipeline | Hook | File | Lines | Event | Mode | Timeout | |---|---|---|---|---|---| +| **Laws Injector (v4.5)** | `_laws-injector.sh` | 70 | SessionStart | Sync | 3s | | Observer | `observe_v3.py` | 200 | Pre/PostToolUse | Async (0 tokens) | 10s | | Project Context | `_project-context.sh` | 136 | PreToolUse | Sync (1x/session) | 3s | | Passive Activator | `_passive-activator.sh` | 66 | PreToolUse | Sync | 5s | @@ -45,6 +46,10 @@ Parallel systems (not part of the confidence pipeline): | Observer (post) | `observe_v3.py` | — | PostToolUse | Async | 10s | | Session Learner | `_session-learner.sh` | 329 | Stop | Sync | 15s | +### Laws tier (v4.5) + +Always-injected one-liner projections of permanent instincts (or stand-alone seeds). Rotated by `last_injected` ASC, capped at 10 laws / ~300 tokens per session. 5 seeds ship from fs-cortex (MIT © Fernando Montero): `read-first`, `grep-read-verify`, `git-triple-check`, `never-hardcode-secrets`, `three-layer-security`. New laws distilled via `/promote --law` after an instinct is promoted to `permanent`. + --- ## Hooks (7 files) diff --git a/install.sh b/install.sh index 7c19f3b..1fec01b 100644 --- a/install.sh +++ b/install.sh @@ -166,6 +166,7 @@ cp "$SCRIPT_DIR/core/_eod-gather.sh" "$SKILLS_DIR/_eod-gather.sh" cp "$SCRIPT_DIR/core/_dream.sh" "$SKILLS_DIR/_dream.sh" cp "$SCRIPT_DIR/core/_generate-dashboard.py" "$SKILLS_DIR/_generate-dashboard.py" cp "$SCRIPT_DIR/core/_dashboard-template.html" "$SKILLS_DIR/_dashboard-template.html" +cp "$SCRIPT_DIR/core/_laws-injector.sh" "$SKILLS_DIR/_laws-injector.sh" chmod +x "$SKILLS_DIR/_passive-activator.sh" chmod +x "$SKILLS_DIR/_instinct-activator.sh" @@ -173,9 +174,24 @@ chmod +x "$SKILLS_DIR/_session-learner.sh" chmod +x "$SKILLS_DIR/_project-context.sh" chmod +x "$SKILLS_DIR/_eod-gather.sh" chmod +x "$SKILLS_DIR/_dream.sh" +chmod +x "$SKILLS_DIR/_laws-injector.sh" chmod +x "$SKILLS_DIR/_generate-dashboard.py" 2>/dev/null || true -echo -e "${GREEN} OK${NC} 5 hook scripts + dream cycle + dashboard generator installed" +# ── Step 5c: Ship-with-install seed laws (v4.5) ── +# Copy seed laws to ~/.claude/skills/_laws/. Idempotent: existing files preserved +# (last_injected timestamps survive upgrades). +if [ -d "$SCRIPT_DIR/seeds/laws" ]; then + mkdir -p "$SKILLS_DIR/_laws" + for lawfile in "$SCRIPT_DIR/seeds/laws"/*.txt; do + [ -f "$lawfile" ] || continue + lawname=$(basename "$lawfile") + if [ ! -f "$SKILLS_DIR/_laws/$lawname" ]; then + cp "$lawfile" "$SKILLS_DIR/_laws/$lawname" + fi + done +fi + +echo -e "${GREEN} OK${NC} 6 hook scripts + dream cycle + dashboard generator + laws injector installed" # ── Step 5b: Legacy file cleanup (v4.3.3) ── LEGACY_CLEANED=0 diff --git a/seeds/laws/README.md b/seeds/laws/README.md new file mode 100644 index 0000000..c1b83e8 --- /dev/null +++ b/seeds/laws/README.md @@ -0,0 +1,29 @@ +# Seed laws + +One-liner crystallized wisdom. Shipped with install, copied to `~/.claude/skills/_laws/`, injected at SessionStart by `_laws-injector.sh` (max 10, cap ~300 tokens, rotation by `last_injected`). + +## Schema + +``` +--- +id: +source_instinct: +source: +license: +created: +last_injected: +--- + +``` + +`source_instinct: null` = stand-alone seed with no instinct counterpart. When `/promote --law` distills a permanent instinct, `source_instinct` points to that instinct id. + +## Seeds shipped + +All 5 ported from [fermontero/fs-cortex](https://github.com/fermonterom/fs-cortex) (MIT © 2026 Fernando Montero): + +- read-first +- grep-read-verify +- git-triple-check +- never-hardcode-secrets +- three-layer-security diff --git a/seeds/laws/git-triple-check.txt b/seeds/laws/git-triple-check.txt new file mode 100644 index 0000000..5a40b4e --- /dev/null +++ b/seeds/laws/git-triple-check.txt @@ -0,0 +1,9 @@ +--- +id: git-triple-check +source_instinct: null +source: seed:fermontero-fs-cortex +license: MIT +created: 2026-04-18 +last_injected: 1970-01-01T00:00:00Z +--- +Before any git commit: run git status + git diff + git log to triple-check changes. diff --git a/seeds/laws/grep-read-verify.txt b/seeds/laws/grep-read-verify.txt new file mode 100644 index 0000000..825342c --- /dev/null +++ b/seeds/laws/grep-read-verify.txt @@ -0,0 +1,9 @@ +--- +id: grep-read-verify +source_instinct: null +source: seed:fermontero-fs-cortex +license: MIT +created: 2026-04-18 +last_injected: 1970-01-01T00:00:00Z +--- +Always Grep -> Read -> Verify before editing any file. Never edit blind. diff --git a/seeds/laws/never-hardcode-secrets.txt b/seeds/laws/never-hardcode-secrets.txt new file mode 100644 index 0000000..7ef46d7 --- /dev/null +++ b/seeds/laws/never-hardcode-secrets.txt @@ -0,0 +1,9 @@ +--- +id: never-hardcode-secrets +source_instinct: null +source: seed:fermontero-fs-cortex +license: MIT +created: 2026-04-18 +last_injected: 1970-01-01T00:00:00Z +--- +NEVER hardcode secrets, API keys, or tokens. Always use environment variables. diff --git a/seeds/laws/read-first.txt b/seeds/laws/read-first.txt new file mode 100644 index 0000000..a3808a7 --- /dev/null +++ b/seeds/laws/read-first.txt @@ -0,0 +1,9 @@ +--- +id: read-first +source_instinct: null +source: seed:fermontero-fs-cortex +license: MIT +created: 2026-04-18 +last_injected: 1970-01-01T00:00:00Z +--- +Always read documentation and instructions before executing any skill or command. diff --git a/seeds/laws/three-layer-security.txt b/seeds/laws/three-layer-security.txt new file mode 100644 index 0000000..8971dce --- /dev/null +++ b/seeds/laws/three-layer-security.txt @@ -0,0 +1,9 @@ +--- +id: three-layer-security +source_instinct: null +source: seed:fermontero-fs-cortex +license: MIT +created: 2026-04-18 +last_injected: 1970-01-01T00:00:00Z +--- +Enforce 3-layer security: middleware auth -> API route validation -> Supabase RLS. diff --git a/tests/test-laws.sh b/tests/test-laws.sh new file mode 100644 index 0000000..e5ae374 --- /dev/null +++ b/tests/test-laws.sh @@ -0,0 +1,184 @@ +#!/bin/bash +# test-laws.sh — TDD Unit Tests for Sinapsis laws injector (v4.5) +# Covers: frontmatter parse, SESSION_PREFETCH_LAWS output, rotation, +# last_injected atomic update, MAX_LAWS cap, token budget, malformed handling. +# Run: bash tests/test-laws.sh + +SCRIPT_DIR="$(cd "$(dirname "$0")/.." && pwd)" +INJECTOR="$SCRIPT_DIR/core/_laws-injector.sh" +SEEDS_REPO="$SCRIPT_DIR/seeds/laws" + +PASS=0 +FAIL=0 +TOTAL=10 + +pass() { PASS=$((PASS + 1)); echo " PASS: $1"; } +fail() { FAIL=$((FAIL + 1)); echo " FAIL: $1"; } + +setup_sandbox() { + SANDBOX="$(mktemp -d 2>/dev/null || mktemp -d -t sinapsis-laws)" + mkdir -p "$SANDBOX/.claude/skills/_laws" + export HOME="$SANDBOX" +} + +teardown_sandbox() { + unset HOME + rm -rf "$SANDBOX" 2>/dev/null +} + +seed_laws() { + cp "$SEEDS_REPO"/*.txt "$SANDBOX/.claude/skills/_laws/" 2>/dev/null +} + +run_injector() { + bash "$INJECTOR" 2>/dev/null +} + +# ─ Test 1: injector exists ─ +echo "Test 1: injector file present" +if [ -f "$INJECTOR" ]; then pass "injector file present"; else fail "injector missing: $INJECTOR"; fi + +# ─ Test 2: seeds directory populated ─ +echo "Test 2: seed laws present" +SEED_COUNT=$(find "$SEEDS_REPO" -name "*.txt" -type f 2>/dev/null | wc -l) +SEED_COUNT=$(echo "$SEED_COUNT" | tr -d ' ') +if [ "$SEED_COUNT" -ge 5 ]; then pass "$SEED_COUNT seed laws found"; else fail "expected >=5, got $SEED_COUNT"; fi + +# ─ Test 3: missing dir → silent exit ─ +echo "Test 3: missing _laws dir → no output" +setup_sandbox +rm -rf "$SANDBOX/.claude/skills/_laws" +OUT=$(run_injector) +if [ -z "$OUT" ]; then pass "silent exit when dir absent"; else fail "unexpected output: $OUT"; fi +teardown_sandbox + +# ─ Test 4: empty dir → silent exit ─ +echo "Test 4: empty _laws dir → no output" +setup_sandbox +OUT=$(run_injector) +if [ -z "$OUT" ]; then pass "silent exit when dir empty"; else fail "unexpected output: $OUT"; fi +teardown_sandbox + +# ─ Test 5: valid laws emit SESSION_PREFETCH_LAWS lines ─ +echo "Test 5: emits SESSION_PREFETCH_LAWS lines" +setup_sandbox +seed_laws +OUT=$(run_injector) +LINES=$(echo "$OUT" | grep -c "^SESSION_PREFETCH_LAWS:") +if [ "$LINES" -eq "$SEED_COUNT" ]; then pass "emitted $LINES lines"; else fail "expected $SEED_COUNT lines, got $LINES"; fi +teardown_sandbox + +# ─ Test 6: MAX_LAWS cap (10) ─ +echo "Test 6: MAX_LAWS cap enforced" +setup_sandbox +# Create 15 dummy law files +for i in $(seq 1 15); do + cat > "$SANDBOX/.claude/skills/_laws/law-$i.txt" < 10"; fi +teardown_sandbox + +# ─ Test 7: last_injected updated after run ─ +echo "Test 7: last_injected atomically updated" +setup_sandbox +seed_laws +run_injector > /dev/null +STALE=$(grep -l "1970-01-01T00:00:00Z" "$SANDBOX/.claude/skills/_laws/"*.txt 2>/dev/null | wc -l) +STALE=$(echo "$STALE" | tr -d ' ') +if [ "$STALE" = "0" ]; then pass "all last_injected updated"; else fail "$STALE files still stale"; fi +teardown_sandbox + +# ─ Test 8: rotation — oldest picked first ─ +echo "Test 8: rotation by last_injected ASC" +setup_sandbox +cat > "$SANDBOX/.claude/skills/_laws/law-a.txt" <<'EOF' +--- +id: law-a +source_instinct: null +source: test +created: 2026-04-18 +last_injected: 2026-04-17T10:00:00Z +--- +Law A — middle timestamp. +EOF +cat > "$SANDBOX/.claude/skills/_laws/law-b.txt" <<'EOF' +--- +id: law-b +source_instinct: null +source: test +created: 2026-04-18 +last_injected: 2020-01-01T00:00:00Z +--- +Law B — oldest timestamp. +EOF +cat > "$SANDBOX/.claude/skills/_laws/law-c.txt" <<'EOF' +--- +id: law-c +source_instinct: null +source: test +created: 2026-04-18 +last_injected: 2026-04-18T10:00:00Z +--- +Law C — newest timestamp. +EOF +OUT=$(run_injector) +FIRST=$(echo "$OUT" | head -1) +if echo "$FIRST" | grep -q "Law B"; then pass "oldest emitted first"; else fail "rotation broken, first line was: $FIRST"; fi +teardown_sandbox + +# ─ Test 9: malformed file → skipped, not crash ─ +echo "Test 9: malformed file does not crash" +setup_sandbox +cat > "$SANDBOX/.claude/skills/_laws/broken.txt" <<'EOF' +no frontmatter at all, just text +EOF +cat > "$SANDBOX/.claude/skills/_laws/good.txt" <<'EOF' +--- +id: good +source_instinct: null +source: test +created: 2026-04-18 +last_injected: 1970-01-01T00:00:00Z +--- +Good law. +EOF +OUT=$(run_injector) +if echo "$OUT" | grep -q "Good law"; then pass "good law emitted, broken skipped"; else fail "malformed file broke injector"; fi +teardown_sandbox + +# ─ Test 10: text field collapses multi-line whitespace ─ +echo "Test 10: whitespace normalization in text" +setup_sandbox +cat > "$SANDBOX/.claude/skills/_laws/multiline.txt" <<'EOF' +--- +id: multiline +source_instinct: null +source: test +created: 2026-04-18 +last_injected: 1970-01-01T00:00:00Z +--- +Line one + continued on line two +EOF +OUT=$(run_injector) +if echo "$OUT" | grep -q "Line one continued on line two"; then pass "whitespace normalized"; else fail "normalization failed: $OUT"; fi +teardown_sandbox + +# Summary +echo "" +echo "═══════════════════════════════════════" +echo " Results: $PASS passed, $FAIL failed (of $TOTAL)" +echo "═══════════════════════════════════════" +if [ "$FAIL" -gt 0 ]; then exit 1; fi +exit 0