hippocampus-layer 1.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (32) hide show
  1. hippocampus/__init__.py +7 -0
  2. hippocampus/_core/.claude/agents/auditor.md +53 -0
  3. hippocampus/_core/.claude/hooks/capture_discovered_work.sh +4 -0
  4. hippocampus/_core/.claude/hooks/guard_auditor_override.sh +15 -0
  5. hippocampus/_core/.claude/hooks/guard_ci_static.sh +22 -0
  6. hippocampus/_core/.claude/hooks/guard_landing.sh +20 -0
  7. hippocampus/_core/.claude/hooks/guard_push_secret.sh +38 -0
  8. hippocampus/_core/.claude/hooks/lib/configlib.sh +39 -0
  9. hippocampus/_core/.claude/hooks/lib/hooklib.sh +36 -0
  10. hippocampus/_core/.claude/hooks/nudge_declarative.sh +48 -0
  11. hippocampus/_core/.claude/hooks/session_start_handoff.sh +26 -0
  12. hippocampus/_core/.claude/hooks/warn_config_edit.sh +13 -0
  13. hippocampus/_core/.claude/hooks/warn_silent_failure.sh +6 -0
  14. hippocampus/_core/.claude/skills/landing-the-plane/SKILL.md +42 -0
  15. hippocampus/_core/.claude/skills/memory-consolidation/SKILL.md +67 -0
  16. hippocampus/_core/.github/workflows/secret-history-sweep.yml +15 -0
  17. hippocampus/_core/THREAT_MODEL.md +43 -0
  18. hippocampus/_core/ci/gitlab-secret-history-sweep.yml +16 -0
  19. hippocampus/_core/install.sh +533 -0
  20. hippocampus/_core/scripts/check_no_pii.py +307 -0
  21. hippocampus/_core/scripts/consolidate_ops.py +120 -0
  22. hippocampus/_core/scripts/hippocampus_config.py +282 -0
  23. hippocampus/_core/scripts/hippocampus_smoke.sh +155 -0
  24. hippocampus/_core/scripts/hippocampus_sync.py +152 -0
  25. hippocampus/_core/scripts/migrate_memory.py +44 -0
  26. hippocampus/_core/scripts/scan_secrets_range.py +117 -0
  27. hippocampus/cli.py +228 -0
  28. hippocampus_layer-1.0.0.dist-info/METADATA +140 -0
  29. hippocampus_layer-1.0.0.dist-info/RECORD +32 -0
  30. hippocampus_layer-1.0.0.dist-info/WHEEL +4 -0
  31. hippocampus_layer-1.0.0.dist-info/entry_points.txt +2 -0
  32. hippocampus_layer-1.0.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,7 @@
1
+ """Hippocampus — persistent memory + enforcement layer for agent-driven git repos.
2
+
3
+ Distribution name on PyPI: ``hippocampus-layer`` (``hippocampus`` is taken);
4
+ import name and console command are both ``hippocampus``.
5
+ """
6
+
7
+ __version__ = "1.0.0"
@@ -0,0 +1,53 @@
1
+ ---
2
+ name: auditor
3
+ description: Blocks any push/PR containing secrets or policy-sensitive data per THREAT_MODEL.md.
4
+ tools: Read, Grep, Glob, Bash
5
+ model: opus
6
+ ---
7
+ You are the pre-push semantic auditor — the SEMANTIC layer that sits ATOP the
8
+ deterministic floor. The DETERMINISTIC scanners (`check_no_pii` + the CI secret floor) are the
9
+ ground truth; you only ADD findings — you cannot remove or override a deterministic block. If
10
+ the deterministic gate already blocked this push, it stays blocked no matter what you decide.
11
+
12
+ Your policy is the repo's `THREAT_MODEL.md` — read it FIRST, every run. It defines who
13
+ receives this repo, what they already know, and what is actually gate-worthy. The arming
14
+ signal is deterministic: while `THREAT_MODEL.md` is missing OR still contains the literal
15
+ line `<!-- HIPPOCAMPUS-TEMPLATE: delete this line to arm the auditor -->`, ALLOW with the
16
+ reason "no threat model — semantic layer inactive (deterministic floor still enforced)": a
17
+ security layer without a policy must not improvise one. Only once that exact line is gone
18
+ do you perform semantic review.
19
+
20
+ You are wired as a PreToolUse agent hook, so you return the standard **PreToolUse decision
21
+ format** (never a bespoke `{"ok": ...}` shape). The hook input JSON arrives in `$ARGUMENTS` and
22
+ carries `session_id` and `tool_input.command`. Proceed in order:
23
+
24
+ 1. **Honor the semantic-layer override.** Read `session_id` from the input. If the skip marker
25
+ `.workstate/.local/auditor-skip.<session_id>` exists (the operator set `AUDITOR_OVERRIDE=1`;
26
+ when `HOOK_LOCAL_DIR` is set in the environment, look under that directory instead of
27
+ `.workstate/.local/`), the operator has consciously waived the SEMANTIC review ONLY.
28
+ Immediately ALLOW — the deterministic floor still enforces independently — and stop:
29
+ ```json
30
+ {"hookSpecificOutput": {"hookEventName": "PreToolUse", "permissionDecision": "allow",
31
+ "permissionDecisionReason": "semantic layer overridden (deterministic floor still enforced)"}}
32
+ ```
33
+
34
+ 2. **Review the pushed diff.** Resolve the range being pushed and read it (e.g.
35
+ `git log -p @{u}..HEAD`, or `git diff` of the range). Flag the leaks `THREAT_MODEL.md`
36
+ declares gate-worthy that a regex scanner misses — typical classes: internal hostnames,
37
+ customer/partner names, private endpoints, PII in fixtures, and stray `.env` / key files.
38
+
39
+ 3. **Return the decision.** To BLOCK on any finding, emit `permissionDecision: "deny"` with a
40
+ reason listing `path:line + rule` for each finding (values redacted):
41
+ ```json
42
+ {"hookSpecificOutput": {"hookEventName": "PreToolUse", "permissionDecision": "deny",
43
+ "permissionDecisionReason": "<path:line + rule for each finding, values redacted>"}}
44
+ ```
45
+ Otherwise ALLOW with `permissionDecision: "allow"`.
46
+
47
+ **Redaction (mandatory).** NEVER print the raw secret or PII value — and never reproduce ANY
48
+ SUBSTRING of it — ANYWHERE in your output. This covers not just the decision JSON but every
49
+ word of free-text reasoning, narration, or prose you emit alongside it: do not echo, quote,
50
+ paraphrase, or partially spell out the value in any of them. The ONLY permitted appearance of a
51
+ flagged value is its redaction `<first4>…redacted` (the first four characters, then the literal
52
+ `…redacted`). The reason string names the location and the rule (`path:line + rule`), never the
53
+ sensitive value itself.
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env bash
2
+ source "$(dirname "$0")/lib/hooklib.sh"
3
+ once_per_session capture || exit 0
4
+ emit_nudge "capture: jot any discovered follow-ups in .workstate/open-work.md as '… (from #X)', and keep .workstate/HANDOFF.md fresh."
@@ -0,0 +1,15 @@
1
+ #!/usr/bin/env bash
2
+ # Semantic-auditor companion — SEMANTIC-LAYER override marker. On `git push` / `gh pr create`
3
+ # carrying AUDITOR_OVERRIDE=1 (inline VAR= or inherited env), drop a per-session skip marker
4
+ # that the `auditor` agent hook honors to waive ONLY the semantic review. The deterministic G1
5
+ # push gate + the CI secret floor are untouched and still block. This hook never itself blocks.
6
+ set -euo pipefail
7
+ source "$(dirname "$0")/lib/hooklib.sh"
8
+ CMD="$(hook_field tool_input.command)"
9
+ case "$CMD" in *"git push"*|*"gh pr create"*) : ;; *) exit 0 ;; esac
10
+ if printf '%s' "$CMD" | grep -q 'AUDITOR_OVERRIDE=1' || [ "${AUDITOR_OVERRIDE:-}" = "1" ]; then
11
+ sid="$(hook_field session_id)"; sid="${sid:-nosid}"
12
+ : > "$LOCAL_DIR/auditor-skip.$sid"
13
+ log_override auditor-semantic "$CMD"
14
+ fi
15
+ exit 0
@@ -0,0 +1,22 @@
1
+ #!/usr/bin/env bash
2
+ # G4 — ci-static push gate. Runs YOUR project's fast static checks (lint, typecheck) before a
3
+ # push leaves the machine. CONFIG-ACTIVATED: set STATIC_CMDS below (or export
4
+ # HIPPOCAMPUS_CI_STATIC) to a shell command line, e.g. "just lint && just types" or
5
+ # "npm run lint && npx tsc --noEmit". While unset the gate is neutral (every push passes).
6
+ set -euo pipefail
7
+ source "$(dirname "$0")/lib/hooklib.sh"
8
+ hippo_load_config # config-activated: [gates.ci_static].commands
9
+ STATIC_CMDS="${HIPPOCAMPUS_CI_STATIC:-}" # real env still wins (configlib skips set vars)
10
+ [ -z "$STATIC_CMDS" ] && exit 0
11
+ CMD="$(hook_field tool_input.command)"
12
+ case "$CMD" in *"git push"*|*"git "*"push"*) : ;; *) exit 0 ;; esac
13
+ if printf '%s' "$CMD" | grep -q 'CI_WIP=1' || [ "${CI_WIP:-}" = "1" ]; then
14
+ log_override ci-static "$CMD"; exit 0
15
+ fi
16
+ cd "$REPO_ROOT"
17
+ # run the project's exact static recipes inline; the full test suite is the CI backstop.
18
+ # Pushes are infrequent, so the inline cost is acceptable.
19
+ if ! out="$(bash -c "$STATIC_CMDS" 2>&1)"; then
20
+ emit_block "BLOCKED: static checks failed at HEAD ($STATIC_CMDS). Fix, or push with CI_WIP=1.\n$out"
21
+ fi
22
+ emit_nudge "ci-static ok ($STATIC_CMDS). Full test suite runs in CI as the backstop."
@@ -0,0 +1,20 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ source "$(dirname "$0")/lib/hooklib.sh"
4
+ [ -n "$(hook_field agent_id)" ] && exit 0 # never gate a subagent's stop
5
+ [ "$(hook_field stop_hook_active)" = "true" ] && exit 0 # loop-breaker (8-block cap backs this)
6
+ crons="$(hook_field session_crons)" # /loop or cron -> relax...
7
+ [ -n "$crons" ] && [ "$crons" != "[]" ] && exit 0 # ...but NOT an empty [] (audit R2-#5)
8
+ [ "${LANDING_OVERRIDE:-}" = "1" ] && { log_override landing override; exit 0; }
9
+ cd "$REPO_ROOT"
10
+ H=".workstate/HANDOFF.md"
11
+ [ -f "$H" ] || emit_block "Land the plane: create .workstate/HANDOFF.md (did / next / repo-state) before stopping."
12
+ # 'current' = HANDOFF newer than the last NON-HANDOFF commit (committing HANDOFF can't
13
+ # invalidate its own freshness). mtime via python3 — portable (GNU `stat -c` vs BSD `stat -f`).
14
+ last_code="$(git log -1 --format=%ct -- . ':(exclude).workstate/HANDOFF.md' 2>/dev/null)"; last_code="${last_code:-0}"
15
+ h_commit="$(git log -1 --format=%ct -- "$H" 2>/dev/null)"; h_commit="${h_commit:-0}"
16
+ h_mtime="$(python3 -c 'import os,sys;print(int(os.path.getmtime(sys.argv[1])))' "$H" 2>/dev/null || echo 0)"
17
+ if [ "$h_mtime" -lt "$last_code" ] && [ "$h_commit" -lt "$last_code" ]; then
18
+ emit_block "Land the plane: .workstate/HANDOFF.md is stale (older than your last code change). Update it, then stop."
19
+ fi
20
+ exit 0
@@ -0,0 +1,38 @@
1
+ #!/usr/bin/env bash
2
+ # G1 — deterministic secret/PII PUSH gate. On `git push` / `gh pr|release create`, scan the
3
+ # pushed commit RANGE for secrets and BLOCK (exit 2) on any hit. NOT overridable: this is the
4
+ # deterministic floor (distinct from the semantic auditor's AUDITOR_OVERRIDE). A leak that
5
+ # slips past pre-commit still gets caught here before it leaves the machine.
6
+ set -euo pipefail
7
+ # Invariant 3: a security gate's internal error must BLOCK (exit 2), never fall open —
8
+ # under Claude Code only exit 2 blocks; a bare `set -e` death is exit 1 = tool proceeds.
9
+ trap 'printf "guard_push_secret: internal error — failing CLOSED\n" >&2; exit 2' ERR
10
+ source "$(dirname "$0")/lib/hooklib.sh"
11
+ CMD="$(hook_field tool_input.command)"
12
+
13
+ # Collapse newlines/tabs AND shell operators to spaces BEFORE matching: a publish verb that is
14
+ # preceded/followed by a newline, a tab, or an un-spaced shell operator (`false||git push`,
15
+ # `echo x|git push`, `(git push …)`, `git push;echo`) is not space-surrounded, so the space-wrapped
16
+ # case below would miss it and exit-0 WITHOUT scanning — defeating the deterministic floor. Mapping
17
+ # the operators ; | & ( ) < > (as well as \n \t) to spaces makes the operator-delimited verb space-
18
+ # delimited, so the existing arms below match it. Normalizing only ADDS matches (a clean range still
19
+ # passes; a security gate that over-matches merely scans a command that did not need it) — a missed
20
+ # range is the danger being closed. Mirrors guard_ci_static.sh's multiline-tolerant matcher.
21
+ CMD_FLAT="$(printf '%s' "$CMD" | tr '\n\t;|&()<>' ' ')"
22
+
23
+ # Match `git push` / `git -C.. push` / `gh pr|release create` as COMMANDS, not the word
24
+ # "push" inside a commit message or unrelated string. Non-push commands pass untouched.
25
+ case " $CMD_FLAT " in
26
+ *" git push "*|*" git push"|*"&& git push"*|*"; git push"*|*" git -C "*" push"*) : ;;
27
+ *" gh pr create"*|*" gh release create"*) : ;;
28
+ *) exit 0 ;;
29
+ esac
30
+
31
+ # Resolve the REAL scanner by this hook's own location (NOT the cwd-derived REPO_ROOT): the
32
+ # scanner self-bootstraps its sys.path from its own path, so it imports correctly while its
33
+ # `git log` runs against the repo being pushed (cwd). No PYTHONPATH needed (Task 1.3).
34
+ SCANNER="$(cd "$(dirname "$0")/../.." && pwd)/scripts/scan_secrets_range.py"
35
+ if ! python3 "$SCANNER"; then
36
+ emit_block "BLOCKED (not overridable): secret/PII detected in the pushed commit range. Scrub it from history (git rebase/filter-repo) before pushing."
37
+ fi
38
+ exit 0
@@ -0,0 +1,39 @@
1
+ #!/usr/bin/env bash
2
+ # Config for bash hooks. PARSES config.env — NEVER sources it (spec Invariant 2):
3
+ # only `HIPPOCAMPUS_<NAME>='…'` lines whose right side is entirely single-quoted
4
+ # segments joined by \' (exactly what hippocampus_sync.py emits) are exported.
5
+ # Anything else is ignored with a warning: a hand-edited `exit 0` or `$(…)` can
6
+ # never execute here. Already-set environment variables always win.
7
+ # G1 (guard_push_secret.sh) never calls hippo_load_config (spec Invariant 1).
8
+
9
+ _hippo_valid_rhs() { # rhs must be '…' segments joined by \'
10
+ local rhs="$1"
11
+ while :; do
12
+ case "$rhs" in "'"*) rhs="${rhs#?}" ;; *) return 1 ;; esac
13
+ case "$rhs" in *"'"*) ;; *) return 1 ;; esac
14
+ rhs="${rhs#*"'"}"
15
+ [ -z "$rhs" ] && return 0
16
+ case "$rhs" in "\\'"*) rhs="${rhs#??}" ;; *) return 1 ;; esac
17
+ done
18
+ }
19
+
20
+ hippo_load_config() { # opt-in per hook; silent no-op when config.env is absent
21
+ local root f line name lineno
22
+ local name_re='^HIPPOCAMPUS_[A-Z0-9_]+$'
23
+ root="${REPO_ROOT:-$(git rev-parse --show-toplevel 2>/dev/null || pwd)}"
24
+ f="${HIPPOCAMPUS_CONFIG_ENV:-$root/.claude/hooks/lib/config.env}"
25
+ [ -f "$f" ] || return 0
26
+ lineno=0
27
+ while IFS= read -r line || [ -n "$line" ]; do
28
+ lineno=$((lineno + 1))
29
+ case "$line" in ''|'#'*) continue ;; esac
30
+ name="${line%%=*}"
31
+ if ! [[ $name =~ $name_re ]] || ! _hippo_valid_rhs "${line#*=}"; then
32
+ printf 'configlib: ignoring malformed config.env line %d (not sync output)\n' "$lineno" >&2
33
+ continue
34
+ fi
35
+ if eval "[ -n \"\${${name}+x}\" ]"; then continue; fi # real env wins
36
+ eval "export $line" # safe: grammar-validated above — value is inert quoted text
37
+ done < "$f"
38
+ return 0
39
+ }
@@ -0,0 +1,36 @@
1
+ #!/usr/bin/env bash
2
+ # Shared hook helpers. Sourced by every hook. Reads the JSON payload from stdin once.
3
+ HOOK_INPUT="${HOOK_INPUT:-$(cat)}"
4
+ REPO_ROOT="$(git rev-parse --show-toplevel 2>/dev/null || pwd)"
5
+ LOCAL_DIR="${HOOK_LOCAL_DIR:-$REPO_ROOT/.workstate/.local}" # tests set HOOK_LOCAL_DIR to a tmp dir
6
+ mkdir -p "$LOCAL_DIR" 2>/dev/null || true
7
+ source "$(dirname "${BASH_SOURCE[0]}")/configlib.sh" # defines hippo_load_config; NEVER calls it
8
+
9
+ hook_field() { # hook_field <dotted.key> -> prints string value ('' if absent)
10
+ printf '%s' "$HOOK_INPUT" | python3 -c '
11
+ import sys, json
12
+ try: d = json.load(sys.stdin)
13
+ except Exception: sys.exit(0)
14
+ cur = d
15
+ for part in sys.argv[1].split("."):
16
+ cur = cur.get(part) if isinstance(cur, dict) else None
17
+ if cur is None: break
18
+ print(cur if isinstance(cur, str) else ("" if cur is None else json.dumps(cur)))
19
+ ' "$1"
20
+ }
21
+
22
+ once_per_session() { # exit 0 the FIRST time this (session,key) is seen
23
+ local sid marker
24
+ sid="$(hook_field session_id)"; sid="${sid:-nosid}"
25
+ marker="$LOCAL_DIR/nudge.$sid.$1"
26
+ [ -e "$marker" ] && return 1
27
+ : > "$marker"; return 0
28
+ }
29
+
30
+ log_override() { # log_override <gate> <reason>
31
+ printf '%s\t%s\t%s\n' "$(git rev-parse --short HEAD 2>/dev/null || echo -)" "$1" "$2" \
32
+ >> "$LOCAL_DIR/overrides.log"
33
+ }
34
+
35
+ emit_block() { printf '%b\n' "$1" >&2; exit 2; } # block tool/stop (%b renders \n)
36
+ emit_nudge() { printf '%b\n' "$1" >&2; exit 0; } # advisory, non-blocking
@@ -0,0 +1,48 @@
1
+ #!/usr/bin/env bash
2
+ # Declarative [[nudges]] from hippocampus.toml (compiled into config.env by sync).
3
+ # Python `re` match against the Bash command — 4KB truncation, forked child killed
4
+ # after 2s (spec: a pathological regex fails to "no nudge", never hangs, NEVER blocks).
5
+ source "$(dirname "$0")/lib/hooklib.sh"
6
+ hippo_load_config
7
+ count="${HIPPOCAMPUS_NUDGE_COUNT:-0}"
8
+ case "$count" in ''|*[!0-9]*) exit 0 ;; esac
9
+ [ "$count" -eq 0 ] && exit 0
10
+ CMD="$(hook_field tool_input.command)"
11
+ [ -z "$CMD" ] && exit 0
12
+ msg="$(printf '%s' "$CMD" | python3 -c '
13
+ import os, re, signal, sys
14
+ count = int(sys.argv[1])
15
+ cmd = sys.stdin.read()[:4096]
16
+ r, w = os.pipe()
17
+ pid = os.fork()
18
+ if pid == 0: # child: match and flush incrementally
19
+ os.close(r) # so a later rule timeout cannot erase
20
+ for i in range(1, count + 1): # earlier rules already-found messages
21
+ pat = os.environ.get(f"HIPPOCAMPUS_NUDGE_{i}_MATCH", "")
22
+ m = os.environ.get(f"HIPPOCAMPUS_NUDGE_{i}_MSG", "")
23
+ src = os.environ.get(f"HIPPOCAMPUS_NUDGE_{i}_SRC", "")
24
+ if not (pat and m):
25
+ continue
26
+ try:
27
+ hit = re.search(pat, cmd)
28
+ except re.error:
29
+ hit = None
30
+ if hit:
31
+ line = m + (f" [{src}]" if src else "")
32
+ os.write(w, (line + "\n").encode())
33
+ os._exit(0)
34
+ os.close(w) # parent: 2s or the child dies
35
+ signal.signal(signal.SIGALRM, lambda *_: os.kill(pid, signal.SIGKILL))
36
+ signal.alarm(2)
37
+ data = b""
38
+ while True:
39
+ chunk = os.read(r, 65536)
40
+ if not chunk:
41
+ break
42
+ data += chunk
43
+ os.waitpid(pid, 0)
44
+ signal.alarm(0)
45
+ sys.stdout.write(data.decode(errors="replace"))
46
+ ' "$count" 2>/dev/null)" || exit 0
47
+ [ -n "$msg" ] && emit_nudge "$msg"
48
+ exit 0
@@ -0,0 +1,26 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ REPO_ROOT="$(git rev-parse --show-toplevel 2>/dev/null || pwd)"
4
+ CONFIGLIB="$REPO_ROOT/.claude/hooks/lib/configlib.sh"
5
+ [ -f "$CONFIGLIB" ] && source "$CONFIGLIB" 2>/dev/null || true
6
+ type hippo_load_config >/dev/null 2>&1 && { hippo_load_config || true; }
7
+ ONBOARDING="${HIPPOCAMPUS_ONBOARDING_DOC:-docs/onboarding.md}"
8
+ # startup check: warn if auto-memory isn't pointed in-repo, else writes go OFF-repo silently
9
+ CFG="$REPO_ROOT/.claude/settings.local.json"; want="$REPO_ROOT/.memory"
10
+ got="$(python3 -c 'import json,sys;print(json.load(open(sys.argv[1])).get("autoMemoryDirectory",""))' "$CFG" 2>/dev/null || echo "")"
11
+ [ "$got" = "$want" ] || echo "⚠️ autoMemoryDirectory='$got' (expected '$want') — memory may be writing OFF-repo. See $ONBOARDING"
12
+ # the CI secret floor ships for GitHub + GitLab only; other hosts get hooks-only — say so
13
+ remotes="$(git -C "$REPO_ROOT" remote -v 2>/dev/null || true)"
14
+ if [ -n "$remotes" ]; then
15
+ case "$remotes" in
16
+ *github.com*|*gitlab*) : ;;
17
+ *) echo "⚠️ no known CI floor host in git remotes — the non-bypassable CI secret floor ships for GitHub/GitLab; on this host only the local hooks protect you. See $ONBOARDING" ;;
18
+ esac
19
+ fi
20
+ [ -n "${HIPPOCAMPUS_DECISION_LOG:-}" ] && echo "Durable record: ${HIPPOCAMPUS_DECISION_LOG} (write history there, not in the baton)"
21
+ H="$REPO_ROOT/.workstate/HANDOFF.md"
22
+ if [ -f "$H" ]; then
23
+ echo "=== .workstate/HANDOFF.md (session baton) ==="
24
+ cat "$H"
25
+ fi
26
+ exit 0
@@ -0,0 +1,13 @@
1
+ #!/usr/bin/env bash
2
+ # Invariant 6: the enforcement layer's control files are never edited silently.
3
+ # 🟡 nudge on Write/Edit touching hippocampus.toml, config.env, or settings.json.
4
+ source "$(dirname "$0")/lib/hooklib.sh"
5
+ FP="$(hook_field tool_input.file_path)"
6
+ [ -z "$FP" ] && exit 0
7
+ rel="${FP#"$REPO_ROOT"/}"
8
+ case "$rel" in
9
+ hippocampus.toml|.claude/hooks/lib/config.env|.claude/settings.json) : ;;
10
+ *) exit 0 ;;
11
+ esac
12
+ changed="$(cd "$REPO_ROOT" && git diff -U0 -- "$rel" 2>&1 | grep '^[+-][^+-]' | head -12 || true)"
13
+ emit_nudge "config-edit: $rel changed — this file configures the enforcement layer. If you edited hippocampus.toml, run: python3 scripts/hippocampus_sync.py sync. If you edited a GENERATED file (config.env, settings.json) by hand, sync will overwrite it and CI's drift check will fail.\nChanged lines:\n$changed"
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env bash
2
+ source "$(dirname "$0")/lib/hooklib.sh"
3
+ CMD="$(hook_field tool_input.command)"
4
+ printf '%s' "$CMD" | grep -q '2>/dev/null' || exit 0
5
+ printf '%s' "$CMD" | grep -Eq 'grep|test |\[ |if ' || exit 0
6
+ emit_nudge "silent-command-failure: suppressed stderr + state inference — check the raw exit code FIRST; empty output != a state."
@@ -0,0 +1,42 @@
1
+ ---
2
+ name: landing-the-plane
3
+ description: "End-of-session handoff runbook — run this when you are wrapping up a working session, before you stop after making commits. Rewrites the `.workstate/HANDOFF.md` baton (what-I-did / what's-next / repo-state + a HEAD stamp) and reconciles `.workstate/open-work.md` so the next session resumes cleanly. Satisfies the G3 Stop gate (`.claude/hooks/guard_landing.sh`), which blocks a stop when the baton is stale."
4
+ ---
5
+
6
+ # Landing the plane — end-of-session handoff
7
+
8
+ Walk this **before you stop** at the end of a working session in which you committed
9
+ anything. It hands the baton to the next session and clears the G3 Stop gate. The baton
10
+ is `.workstate/HANDOFF.md`; the flat work list is `.workstate/open-work.md`. Both are
11
+ tracked (only `.workstate/.local/` is git-ignored).
12
+
13
+ ## The workflow
14
+
15
+ 1. **Rewrite the three baton sections** in `.workstate/HANDOFF.md` — overwrite in place,
16
+ never duplicate-append:
17
+ - **What I did** — what changed this session (tasks, commits, decisions).
18
+ - **What's next** — the immediate next action, so a fresh controller can resume.
19
+ - **Repo state** — branch, HEAD, test status, and where the authoritative resume map lives.
20
+
21
+ 2. **Stamp the HEAD** as the last line: `<!-- HEAD: $(git rev-parse HEAD) -->`. This records
22
+ the commit the baton describes, so the gate can tell current from stale.
23
+
24
+ 3. **Reconcile `.workstate/open-work.md`** — close finished items (delete their lines), add
25
+ the follow-ups you discovered. Keep it a flat, one-item-per-line list.
26
+
27
+ 4. **Now it is safe to stop.** Land the plane *last*, after your final code commit: editing
28
+ the baton refreshes its mtime, so G3 passes.
29
+
30
+ ## What the gate checks (`.claude/hooks/guard_landing.sh`)
31
+
32
+ - **Stop-only.** It fires when the main agent tries to stop; it never gates a subagent.
33
+ - **Freshness by mtime.** It blocks only when the baton is **missing**, or **stale** — its
34
+ file mtime *and* its last commit are both older than the last non-`HANDOFF.md` commit. A
35
+ HANDOFF-only commit cannot refresh itself (the "last code change" excludes it), so a bare
36
+ edit — which bumps the mtime — is what clears the gate.
37
+ - **Autonomous-aware.** It relaxes for a `/loop` or scheduled-cron session (a non-empty
38
+ `session_crons` list, not an empty `[]`) and for a re-entrant stop; `LANDING_OVERRIDE=1`
39
+ is the manual escape hatch.
40
+
41
+ > The baton is ephemeral — a one-session pointer, not a log. The durable record is your
42
+ > project's decision log and `.memory/` pages; write history there, not here.
@@ -0,0 +1,67 @@
1
+ ---
2
+ name: memory-consolidation
3
+ description: "Periodic manual memory garbage-collection runbook — invoke as /consolidate to tidy the `.memory/` store and the `.workstate/open-work.md` work list. Drives the deterministic ops in `scripts/consolidate_ops.py`: auto-applies the safe, git-tracked rewrites (dedupe, frontmatter repair, closed-work compaction, index reconcile) and flags the judgment calls (orphans + linker suggestions, negative-value pages, contradictions, coverage gaps) for a human to resolve. The sole writer of `CLAUDE.md`, under its ~100-line cap."
4
+ model: opus
5
+ ---
6
+
7
+ # Memory consolidation — the /consolidate manual GC
8
+
9
+ Run this **when invoked as `/consolidate`** — a periodic, human-in-the-loop tidy of durable
10
+ memory. It orchestrates the nine pure functions in `scripts/consolidate_ops.py` (import them;
11
+ they only read the dir you hand them or transform strings you pass — **none delete, none write
12
+ to disk**). Every write is yours, git-tracked and recoverable. Nothing here runs automatically:
13
+ the ops classify, you decide, and the auto-apply class is the *only* class that mutates.
14
+
15
+ The memory-store ops act on `.memory/` (topic pages + the `MEMORY.md` router index — the
16
+ harness-owned store). The one exception is closed-work compaction, which acts on **our**
17
+ `.workstate/open-work.md` flat list, not a `.memory/` page. Name the right target per op.
18
+
19
+ ## 1. Auto-apply — deterministic, safe, git-tracked
20
+
21
+ Apply these directly; each is reversible via git.
22
+
23
+ - **Dedupe.** `find_exact_duplicates(dir)` returns variadic tuples — the **N-way** groups of
24
+ byte-identical `*.md` pages. Collapse each group to one canonical page and re-point inbound
25
+ links to it. No information is lost (the members are byte-identical), so this is not a hard
26
+ delete of content — it removes redundant copies only.
27
+ - **Frontmatter repair.** `repair_frontmatter(text)` guarantees an `updated:` key; the body is
28
+ preserved verbatim. Self-edit the page in place — **never** duplicate-append.
29
+ - **Closed-work compaction.** `compact_closed_work(text)` on `.workstate/open-work.md` drops
30
+ completed `- [x]` and struck `~~…~~` lines while keeping open items and `(from #X)` lineage.
31
+ - **Index reconcile.** `reconcile_index(dir)` returns `{"missing": [...], "dangling": [...]}`,
32
+ counting **only real markdown links** (a prose parenthetical is not a link). Add the missing
33
+ pages to `MEMORY.md`; remove the dangling links. Idempotent — a second run is a no-op.
34
+
35
+ ## 2. Flag-only — surface for a human, never auto-apply, never delete
36
+
37
+ These are judgment calls. Report them in the run summary with a recommendation; **never delete**
38
+ a page and never edit on your own initiative.
39
+
40
+ - **Orphans + linker suggestions** (audit R2-#19). `find_orphans(dir)` lists pages with zero
41
+ inbound links. For each, suggest **where to link it from** — name the candidate sibling pages
42
+ whose content relates — so a human can wire it in. The linker suggestion is advice, not an edit.
43
+ - **Negative-value pages.** `compression_flag(text)` is the operator-variant **compression
44
+ test**: `True` when a page draws on `<2` distinct sources — a single-source mirror of one
45
+ greppable file, which earns no keep. Flag it as negative-value; do not delete it.
46
+ - **Contradiction / staleness / coverage-gap.** Where two pages disagree, a page has gone stale,
47
+ or a live topic has no page, flag it and defer to a human. When ambiguous, defer — do not guess.
48
+
49
+ ## 3. Guardrails (binding)
50
+
51
+ - **Never delete.** No op deletes; you never hard-delete a page carrying unique content. The only
52
+ removal is collapsing byte-identical duplicates (§1).
53
+ - **`CLAUDE.md` is written by /consolidate alone**, under its ~100-line cap. A promotion must
54
+ **displace**: `promote_with_displacement(claude_md, new_law, displaced_line)` appends the new
55
+ law only after removing `displaced_line` by **exact-line match**. Name a real, present line
56
+ verbatim — a wrong or absent line **fails loud** (`ValueError`). It post-verifies the cap and
57
+ refuses (`ValueError`) any result over 100 non-blank lines (`claude_md_lines`). At the cap with
58
+ no displacement it refuses. This task never actually writes `CLAUDE.md`; the skill only owns it.
59
+ - **New-page vs edit-in-place.** A *new page* is a distinct entity you would `[[link]]`; an
60
+ *edit* is an attribute of one that exists. **Prefer editing.** A page is authoritative or it
61
+ should not exist — no per-read hedge.
62
+ - **Idempotent.** Re-running /consolidate on a tidy store changes nothing.
63
+ - **Emit `run_summary(actions)`** at the end — the human-readable log of what was applied and
64
+ what was flagged. It is never empty (`"consolidate run: no-op"` when nothing changed).
65
+
66
+ > Where this file and `CLAUDE.md` differ, `CLAUDE.md` is canonical. The compression test,
67
+ > new-page rule, and cap live there under "Memory conventions"; this skill enforces them.
@@ -0,0 +1,15 @@
1
+ name: secret-history-sweep
2
+ on:
3
+ schedule:
4
+ - cron: "0 6 * * 1"
5
+ workflow_dispatch: {}
6
+ jobs:
7
+ trufflehog:
8
+ runs-on: ubuntu-latest
9
+ steps:
10
+ - uses: actions/checkout@v4
11
+ with:
12
+ fetch-depth: 0
13
+ - uses: trufflesecurity/trufflehog@27b0417c16317ca9a472a9a8092acce143b49c55 # v3.95.9
14
+ with:
15
+ extra_args: --only-verified
@@ -0,0 +1,43 @@
1
+ # THREAT_MODEL.md — the semantic auditor's policy
2
+ <!-- HIPPOCAMPUS-TEMPLATE: delete this line to arm the auditor -->
3
+
4
+ The semantic auditor (`.claude/agents/auditor.md`) reviews every outbound push against THIS
5
+ file. While the HIPPOCAMPUS-TEMPLATE sentinel line above exists, the auditor stays inactive —
6
+ deleting that line (after writing real answers below) is the explicit, greppable arming
7
+ step; a reformat or annotation of the template can never arm it by accident. The deterministic
8
+ secret gate and the CI floor protect the repo regardless of what's written here.
9
+
10
+ Answer the three questions that decide everything:
11
+
12
+ ## 1. Who receives this repo?
13
+
14
+ <!-- e.g. "public — anyone on the internet", "a contractor under NDA", "internal team only".
15
+ TEMPLATE — replace me. -->
16
+
17
+ ## 2. What do they already know?
18
+
19
+ <!-- Sensitivity is relative: a partner who already knows your org's name doesn't need the
20
+ name gated; a public release does. List what the recipient legitimately already knows,
21
+ so the auditor doesn't false-positive on it. TEMPLATE — replace me. -->
22
+
23
+ ## 3. What is actually gate-worthy?
24
+
25
+ <!-- The classes of content that must NEVER leave in a push. Be concrete: e.g. customer
26
+ names, internal hostnames, unreleased product names, personal data of class X. The
27
+ deterministic scanners already handle credential shapes — list what only a READER
28
+ would catch. TEMPLATE — replace me. -->
29
+
30
+ ---
31
+
32
+ **To activate the auditor** once this file is real, add the agent hook to
33
+ `.claude/settings.json` under the `PreToolUse` → `"matcher": "Bash"` hook list:
34
+
35
+ ```json
36
+ { "type": "agent", "if": "Bash(git push *)", "model": "opus", "timeout": 120,
37
+ "prompt": "Run the `auditor` agent (.claude/agents/auditor.md) over the pushed diff for $ARGUMENTS. Block on any finding per THREAT_MODEL.md; redact values." }
38
+ ```
39
+
40
+ Note the honest cost: the agent hook runs an LLM review on every push (latency + spend), and
41
+ agent hooks fail open — the deterministic floor, not this layer, is the guarantee.
42
+ `AUDITOR_OVERRIDE=1 git push …` waives one push's semantic review only (logged); nothing
43
+ waives the deterministic floor.
@@ -0,0 +1,16 @@
1
+ # Scheduled deep-history TruffleHog sweep — GitLab parity with
2
+ # .github/workflows/secret-history-sweep.yml. Runs ONLY on scheduled pipelines
3
+ # (never on push/MR); create the schedule once at Build → Pipeline schedules
4
+ # (weekly, e.g. cron "0 6 * * 1", matching the GitHub sweep).
5
+ secret-history-sweep:
6
+ stage: check
7
+ rules:
8
+ - if: '$CI_PIPELINE_SOURCE == "schedule"'
9
+ image:
10
+ name: trufflesecurity/trufflehog:latest
11
+ entrypoint: [""]
12
+ variables:
13
+ GIT_STRATEGY: clone
14
+ GIT_DEPTH: "0" # full clone — the sweep's whole point is deep history
15
+ script:
16
+ - trufflehog git file://. --only-verified --fail