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.
- hippocampus/__init__.py +7 -0
- hippocampus/_core/.claude/agents/auditor.md +53 -0
- hippocampus/_core/.claude/hooks/capture_discovered_work.sh +4 -0
- hippocampus/_core/.claude/hooks/guard_auditor_override.sh +15 -0
- hippocampus/_core/.claude/hooks/guard_ci_static.sh +22 -0
- hippocampus/_core/.claude/hooks/guard_landing.sh +20 -0
- hippocampus/_core/.claude/hooks/guard_push_secret.sh +38 -0
- hippocampus/_core/.claude/hooks/lib/configlib.sh +39 -0
- hippocampus/_core/.claude/hooks/lib/hooklib.sh +36 -0
- hippocampus/_core/.claude/hooks/nudge_declarative.sh +48 -0
- hippocampus/_core/.claude/hooks/session_start_handoff.sh +26 -0
- hippocampus/_core/.claude/hooks/warn_config_edit.sh +13 -0
- hippocampus/_core/.claude/hooks/warn_silent_failure.sh +6 -0
- hippocampus/_core/.claude/skills/landing-the-plane/SKILL.md +42 -0
- hippocampus/_core/.claude/skills/memory-consolidation/SKILL.md +67 -0
- hippocampus/_core/.github/workflows/secret-history-sweep.yml +15 -0
- hippocampus/_core/THREAT_MODEL.md +43 -0
- hippocampus/_core/ci/gitlab-secret-history-sweep.yml +16 -0
- hippocampus/_core/install.sh +533 -0
- hippocampus/_core/scripts/check_no_pii.py +307 -0
- hippocampus/_core/scripts/consolidate_ops.py +120 -0
- hippocampus/_core/scripts/hippocampus_config.py +282 -0
- hippocampus/_core/scripts/hippocampus_smoke.sh +155 -0
- hippocampus/_core/scripts/hippocampus_sync.py +152 -0
- hippocampus/_core/scripts/migrate_memory.py +44 -0
- hippocampus/_core/scripts/scan_secrets_range.py +117 -0
- hippocampus/cli.py +228 -0
- hippocampus_layer-1.0.0.dist-info/METADATA +140 -0
- hippocampus_layer-1.0.0.dist-info/RECORD +32 -0
- hippocampus_layer-1.0.0.dist-info/WHEEL +4 -0
- hippocampus_layer-1.0.0.dist-info/entry_points.txt +2 -0
- hippocampus_layer-1.0.0.dist-info/licenses/LICENSE +21 -0
hippocampus/__init__.py
ADDED
|
@@ -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,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
|