leopold-driver 0.1.1 → 0.1.3
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.
- package/README.md +19 -5
- package/assets/VERSION +1 -0
- package/assets/extensions/README.md +52 -0
- package/assets/extensions/gstack/extension.json +8 -0
- package/assets/extensions/gstack/manage.sh +68 -0
- package/assets/extensions/leopold/extension.json +8 -0
- package/assets/extensions/leopold/manage.sh +59 -0
- package/assets/extensions/ovmem/README.md +101 -0
- package/assets/extensions/ovmem/extension.json +8 -0
- package/assets/extensions/ovmem/install.sh +330 -0
- package/assets/extensions/ovmem/manage.sh +87 -0
- package/assets/extensions/ovmem/models.json +24 -0
- package/assets/extensions/ovmem/payload/RUNTIME.md +121 -0
- package/assets/extensions/ovmem/payload/ovmem-cleanup.py +148 -0
- package/assets/extensions/ovmem/payload/ovmem.py +421 -0
- package/assets/extensions/serena/README.md +50 -0
- package/assets/extensions/serena/extension.json +8 -0
- package/assets/extensions/serena/manage.sh +119 -0
- package/assets/hooks/guard-irreversible.sh +185 -0
- package/assets/hooks/hooks.json +20 -0
- package/assets/hooks/stop-continuity.sh +132 -0
- package/assets/install.sh +184 -0
- package/assets/scripts/__pycache__/leopold-watch.cpython-312.pyc +0 -0
- package/assets/scripts/leopold-doctor.sh +53 -0
- package/assets/scripts/leopold-menu.sh +132 -0
- package/assets/scripts/leopold-update-check.sh +23 -0
- package/assets/scripts/leopold-update.sh +13 -0
- package/assets/scripts/leopold-watch.py +585 -0
- package/assets/scripts/record-demo.sh +61 -0
- package/assets/scripts/test-guard.sh +76 -0
- package/assets/scripts/test-hooks.sh +121 -0
- package/assets/settings.template.json +23 -0
- package/assets/skills/leopold-brief/SKILL.md +121 -0
- package/assets/skills/leopold-doctor/SKILL.md +23 -0
- package/assets/skills/leopold-run/SKILL.md +171 -0
- package/assets/skills/leopold-status/SKILL.md +34 -0
- package/assets/skills/leopold-stop/SKILL.md +36 -0
- package/assets/skills/leopold-update/SKILL.md +27 -0
- package/assets/skills/leopold-watch/SKILL.md +48 -0
- package/assets/templates/CHARTER.md +32 -0
- package/assets/templates/DECISIONS.md +15 -0
- package/assets/templates/GUARDRAILS.md +38 -0
- package/assets/templates/MISSION.md +22 -0
- package/assets/templates/PLAN.md +9 -0
- package/dist/guard.js +82 -23
- package/dist/harness.js +71 -0
- package/dist/index.js +53 -23
- package/package.json +6 -3
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# serena extension — LSP-backed code intelligence (MCP) for Claude Code.
|
|
3
|
+
#
|
|
4
|
+
# Serena gives the agent IDE-grade, symbol-level tools (find_symbol,
|
|
5
|
+
# find_referencing_symbols, replace_symbol_body, ...) instead of grep + whole-file
|
|
6
|
+
# reads. That means sharper edits AND far fewer tokens — which is exactly the lever
|
|
7
|
+
# Leopold needs to keep an autonomous run's context lean. Mandatory for quality.
|
|
8
|
+
#
|
|
9
|
+
# Setup is the OFFICIAL path (uv tool + `claude mcp add`), NOT the MCP marketplace —
|
|
10
|
+
# the marketplace ships outdated commands (per the Serena maintainers).
|
|
11
|
+
set -euo pipefail
|
|
12
|
+
|
|
13
|
+
CLAUDE="${CLAUDE_HOME:-$HOME/.claude}"
|
|
14
|
+
SETTINGS="$CLAUDE/settings.json"
|
|
15
|
+
BIN="$HOME/.local/bin"
|
|
16
|
+
PKG="serena-agent" # PyPI package -> `serena` + `serena-hooks`
|
|
17
|
+
MCP_ARGS="start-mcp-server --context=claude-code --project-from-cwd"
|
|
18
|
+
|
|
19
|
+
say() { printf '\033[36m->\033[0m %s\n' "$*"; }
|
|
20
|
+
ok() { printf ' \033[32mok\033[0m %s\n' "$*"; }
|
|
21
|
+
warn() { printf ' \033[33mwarn\033[0m %s\n' "$*"; }
|
|
22
|
+
have() { command -v "$1" >/dev/null 2>&1; }
|
|
23
|
+
sver() { serena --version 2>/dev/null | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -1; }
|
|
24
|
+
mcp_present() { have claude && claude mcp get serena >/dev/null 2>&1; }
|
|
25
|
+
hooks_wired() { [ -f "$SETTINGS" ] && grep -q 'serena-hooks ' "$SETTINGS" 2>/dev/null; }
|
|
26
|
+
|
|
27
|
+
wire_hooks() {
|
|
28
|
+
have jq || { warn "jq missing; skipping Serena hook wiring (recommended for tool adherence)"; return 0; }
|
|
29
|
+
[ -f "$SETTINGS" ] || echo '{}' > "$SETTINGS"
|
|
30
|
+
cp "$SETTINGS" "$SETTINGS.serena.bak"
|
|
31
|
+
local rm="serena-hooks remind --client=claude-code" ap="serena-hooks auto-approve --client=claude-code"
|
|
32
|
+
local ac="serena-hooks activate --client=claude-code" cl="serena-hooks cleanup --client=claude-code"
|
|
33
|
+
local tmp; tmp="$(mktemp)"
|
|
34
|
+
jq --arg rm "$rm" --arg ap "$ap" --arg ac "$ac" --arg cl "$cl" '
|
|
35
|
+
.hooks //= {} | .hooks.PreToolUse //= [] | .hooks.SessionStart //= [] | .hooks.SessionEnd //= []
|
|
36
|
+
| (if any(.hooks.PreToolUse[]?.hooks[]?; .command==$rm) then . else .hooks.PreToolUse += [{matcher:"",hooks:[{type:"command",command:$rm}]}] end)
|
|
37
|
+
| (if any(.hooks.PreToolUse[]?.hooks[]?; .command==$ap) then . else .hooks.PreToolUse += [{matcher:"mcp__serena__*",hooks:[{type:"command",command:$ap}]}] end)
|
|
38
|
+
| (if any(.hooks.SessionStart[]?.hooks[]?; .command==$ac) then . else .hooks.SessionStart += [{hooks:[{type:"command",command:$ac}]}] end)
|
|
39
|
+
| (if any(.hooks.SessionEnd[]?.hooks[]?; .command==$cl) then . else .hooks.SessionEnd += [{hooks:[{type:"command",command:$cl}]}] end)
|
|
40
|
+
' "$SETTINGS" > "$tmp" && mv "$tmp" "$SETTINGS"
|
|
41
|
+
ok "Serena hooks wired: remind + auto-approve + activate + cleanup (backup at $SETTINGS.serena.bak)"
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
do_install() {
|
|
45
|
+
if ! have uv; then
|
|
46
|
+
say "installing uv (Serena's only prerequisite)"
|
|
47
|
+
curl -LsSf https://astral.sh/uv/install.sh | sh || { warn "uv install failed"; return 1; }
|
|
48
|
+
export PATH="$BIN:$PATH"
|
|
49
|
+
fi
|
|
50
|
+
if have serena && [ "${1:-install}" != update ]; then
|
|
51
|
+
ok "serena already installed ($(sver))"
|
|
52
|
+
else
|
|
53
|
+
say "installing Serena (LSP code intelligence)"
|
|
54
|
+
have serena || printf ' \033[2m(downloading serena-agent + deps — up to a minute on first install)\033[0m\n'
|
|
55
|
+
uv tool install -p 3.13 "$PKG" || uv tool install "$PKG" \
|
|
56
|
+
|| { warn "uv tool install $PKG failed"; return 1; }
|
|
57
|
+
export PATH="$BIN:$PATH"
|
|
58
|
+
fi
|
|
59
|
+
have serena || { warn "serena not on PATH after install (add $BIN to PATH)"; return 1; }
|
|
60
|
+
ok "serena $(sver) ready"
|
|
61
|
+
|
|
62
|
+
# Global config is created automatically on first run; only init if it is missing.
|
|
63
|
+
if [ ! -f "$HOME/.serena/serena_config.yml" ]; then
|
|
64
|
+
serena init </dev/null >/dev/null 2>&1 || warn "serena init skipped (the MCP server creates the global config on first start)"
|
|
65
|
+
fi
|
|
66
|
+
|
|
67
|
+
# Register the MCP server (user scope = all projects). claude mcp add is NOT idempotent,
|
|
68
|
+
# so only add when it is not already present.
|
|
69
|
+
if have claude; then
|
|
70
|
+
if mcp_present; then ok "Serena MCP already registered (user scope)"
|
|
71
|
+
else
|
|
72
|
+
say "registering Serena MCP for Claude Code (user scope, all projects)"
|
|
73
|
+
if claude mcp add --scope user serena -- serena $MCP_ARGS >/dev/null 2>&1; then ok "MCP registered"
|
|
74
|
+
else warn "couldn't auto-register; run: claude mcp add --scope user serena -- serena $MCP_ARGS"; fi
|
|
75
|
+
fi
|
|
76
|
+
else
|
|
77
|
+
warn "the 'claude' CLI is not on PATH — register manually once it is:"
|
|
78
|
+
warn " claude mcp add --scope user serena -- serena $MCP_ARGS"
|
|
79
|
+
fi
|
|
80
|
+
|
|
81
|
+
# Recommended hooks (counter Claude Code's bias toward its built-in tools).
|
|
82
|
+
hooks_wired && ok "Serena hooks already wired" || wire_hooks
|
|
83
|
+
echo
|
|
84
|
+
ok "Serena ready. Reconnect with /mcp (or restart Claude Code) to load its tools."
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
case "${1:-}" in
|
|
88
|
+
detect) have serena ;;
|
|
89
|
+
status)
|
|
90
|
+
if have serena; then
|
|
91
|
+
printf 'serena %s' "$(sver)"
|
|
92
|
+
mcp_present && printf ' · MCP connected' || printf ' · MCP not registered'
|
|
93
|
+
hooks_wired && printf ' · hooks on'; echo
|
|
94
|
+
else echo "not installed"; fi
|
|
95
|
+
;;
|
|
96
|
+
install) do_install install ;;
|
|
97
|
+
update) do_install update ;;
|
|
98
|
+
remove)
|
|
99
|
+
if mcp_present; then claude mcp remove serena -s user >/dev/null 2>&1 && echo "unregistered Serena MCP"; fi
|
|
100
|
+
if [ -f "$SETTINGS" ] && have jq; then
|
|
101
|
+
cp "$SETTINGS" "$SETTINGS.serena.bak"
|
|
102
|
+
tmp="$(mktemp)"
|
|
103
|
+
jq 'if .hooks then .hooks |= ( to_entries
|
|
104
|
+
| map(.value |= ( map(.hooks |= map(select((.command // "") | test("serena-hooks ") | not)))
|
|
105
|
+
| map(select((.hooks | length) > 0)) ))
|
|
106
|
+
| from_entries ) else . end' "$SETTINGS" > "$tmp" && mv "$tmp" "$SETTINGS"
|
|
107
|
+
echo "unwired Serena hooks (backup at $SETTINGS.serena.bak)"
|
|
108
|
+
fi
|
|
109
|
+
echo "left installed: the serena CLI. To remove it too: uv tool uninstall $PKG"
|
|
110
|
+
;;
|
|
111
|
+
doctor)
|
|
112
|
+
echo "serena: $(have serena && serena --version 2>/dev/null | head -1 || echo missing)"
|
|
113
|
+
echo "MCP: $(mcp_present && echo "registered (user scope)" || echo "not registered")"
|
|
114
|
+
_hc="$(grep -c 'serena-hooks ' "$SETTINGS" 2>/dev/null || true)"; echo "hooks: ${_hc:-0}/4 Serena hooks in settings.json"
|
|
115
|
+
echo "uv: $(have uv && echo present || echo missing)"
|
|
116
|
+
have claude || echo "note: 'claude' CLI not found — MCP registration must be done manually"
|
|
117
|
+
;;
|
|
118
|
+
*) echo "usage: manage.sh {detect|status|install|update|remove|doctor}" >&2; exit 2 ;;
|
|
119
|
+
esac
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Leopold PreToolUse guard: keeps irreversible / outbound actions locked while a
|
|
3
|
+
# Leopold autonomous run is active. This is defense-in-depth on top of Claude
|
|
4
|
+
# Code's own permission system. It never loosens anything; it only adds denials.
|
|
5
|
+
#
|
|
6
|
+
# Hook contract (Claude Code PreToolUse): read JSON on stdin. To block, print a
|
|
7
|
+
# hookSpecificOutput object with permissionDecision "deny" and exit 0. To allow,
|
|
8
|
+
# exit 0 with no output. Detection is intentionally over-broad: when in doubt it
|
|
9
|
+
# blocks, because a blocked-but-safe op just means the human runs it.
|
|
10
|
+
#
|
|
11
|
+
# Each denial below has a matching case in scripts/test-guard.sh (red-team suite).
|
|
12
|
+
|
|
13
|
+
input="$(cat 2>/dev/null || true)"
|
|
14
|
+
command -v jq >/dev/null 2>&1 || exit 0 # cannot parse safely -> defer to CC perms
|
|
15
|
+
|
|
16
|
+
cwd="$(printf '%s' "$input" | jq -r '.cwd // empty' 2>/dev/null || true)"
|
|
17
|
+
[ -z "${cwd:-}" ] && cwd="$PWD"
|
|
18
|
+
LEO="$cwd/.leopold"
|
|
19
|
+
STATE="$LEO/state.json"
|
|
20
|
+
|
|
21
|
+
# Only guard during an active autonomous run.
|
|
22
|
+
[ -f "$STATE" ] || exit 0
|
|
23
|
+
# Fail CLOSED on a malformed state.json: if the file exists but does not parse,
|
|
24
|
+
# we cannot prove the run is inactive, so we keep guarding (and say so).
|
|
25
|
+
if ! jq -e . "$STATE" >/dev/null 2>&1; then
|
|
26
|
+
active="true"
|
|
27
|
+
else
|
|
28
|
+
active="$(jq -r '.active // false' "$STATE" 2>/dev/null || echo true)"
|
|
29
|
+
fi
|
|
30
|
+
[ "$active" = "true" ] || exit 0
|
|
31
|
+
|
|
32
|
+
tool="$(printf '%s' "$input" | jq -r '.tool_name // empty' 2>/dev/null || true)"
|
|
33
|
+
|
|
34
|
+
deny() {
|
|
35
|
+
local r="$1" ts
|
|
36
|
+
jq -cn --arg r "$r" '{hookSpecificOutput:{hookEventName:"PreToolUse",permissionDecision:"deny",permissionDecisionReason:$r}}'
|
|
37
|
+
ts="$(date -u +%Y-%m-%dT%H:%M:%SZ 2>/dev/null || echo '')"
|
|
38
|
+
printf '{"ts":"%s","event":"guard_block","tool":"%s"}\n' "$ts" "$tool" >> "$LEO/events.jsonl" 2>/dev/null || true
|
|
39
|
+
exit 0
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
has_token() { [ -f "$LEO/$1" ]; }
|
|
43
|
+
matches() { printf '%s' "$1" | grep -qiE -- "$2"; } # $1 text, $2 ERE (case-insensitive)
|
|
44
|
+
|
|
45
|
+
# Resolve the git subcommand, skipping leading global options AND their values
|
|
46
|
+
# (-c k=v, -C path, --git-dir, --work-tree, --namespace, --exec-path, --config-env).
|
|
47
|
+
# This is what closes the `git -c user.name=x commit` / `git -C /r commit` bypass.
|
|
48
|
+
git_subcmd() { # $1 = normalized command -> echoes the subcommand (or empty)
|
|
49
|
+
local toks=() i n t want=0
|
|
50
|
+
read -ra toks <<< "$1"
|
|
51
|
+
n=${#toks[@]}; i=0
|
|
52
|
+
# match 'git', '/usr/bin/git', './git', 'env git' ... by basename
|
|
53
|
+
while [ "$i" -lt "$n" ] && [ "${toks[$i]##*/}" != "git" ]; do i=$((i+1)); done
|
|
54
|
+
i=$((i+1))
|
|
55
|
+
while [ "$i" -lt "$n" ]; do
|
|
56
|
+
t="${toks[$i]}"
|
|
57
|
+
if [ "$want" -eq 1 ]; then want=0; i=$((i+1)); continue; fi
|
|
58
|
+
case "$t" in
|
|
59
|
+
-c|-C|--git-dir|--work-tree|--namespace|--exec-path|--config-env) want=1 ;;
|
|
60
|
+
--*=*|-*) : ;; # self-contained option, skip
|
|
61
|
+
*) printf '%s' "$t"; return ;;
|
|
62
|
+
esac
|
|
63
|
+
i=$((i+1))
|
|
64
|
+
done
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
case "$tool" in
|
|
68
|
+
Task|Agent)
|
|
69
|
+
# Subagent fan-out is the #1 autonomous-run cost blowup, on TWO axes — how MANY are
|
|
70
|
+
# spawned, and how much CONTEXT each one carries. Guard both.
|
|
71
|
+
stype="$(printf '%s' "$input" | jq -r '.tool_input.subagent_type // empty' 2>/dev/null || true)"
|
|
72
|
+
|
|
73
|
+
# (a) Context per subagent: an oversized prompt means context is being dumped into the
|
|
74
|
+
# subagent (each is billed in full). A normal scoped prompt is a few KB; deny the absurd.
|
|
75
|
+
plen="$(printf '%s' "$input" | jq -r '.tool_input.prompt // .tool_input.description // empty' 2>/dev/null | wc -c | tr -d ' ')"
|
|
76
|
+
if [ "${plen:-0}" -gt 262144 ] 2>/dev/null; then
|
|
77
|
+
deny "Leopold guard: this subagent prompt is ~$(( ${plen:-0} / 1024 ))KB — you're handing the subagent a huge context (each spawn is billed in full). Pass a minimal, scoped prompt: point it at files to read, don't paste them in."
|
|
78
|
+
fi
|
|
79
|
+
|
|
80
|
+
# (b) Forks clone the ENTIRE parent session (multi-MB) — they ARE the per-subagent
|
|
81
|
+
# context leak. Forbidden by default (max_forks 0); a fresh scoped subagent does the
|
|
82
|
+
# same work with a clean slate. Raise max_forks in GUARDRAILS.md only for a sub-task
|
|
83
|
+
# that genuinely needs the full conversation.
|
|
84
|
+
if [ "$stype" = "fork" ]; then
|
|
85
|
+
max_fk="$(jq -r '.max_forks // 0' "$STATE" 2>/dev/null || echo 0)"; case "$max_fk" in (*[!0-9]*|"") max_fk=0 ;; esac
|
|
86
|
+
forked="$(jq -r '.forks_spawned // 0' "$STATE" 2>/dev/null || echo 0)"; case "$forked" in (*[!0-9]*|"") forked=0 ;; esac
|
|
87
|
+
if [ "$forked" -ge "$max_fk" ] 2>/dev/null; then
|
|
88
|
+
deny "Leopold guard: forks are forbidden in autonomous mode ($forked/$max_fk). A fork clones the WHOLE session context (multi-MB) into the spawn — this is the leak that runs up the bill. Use a regular subagent with a minimal prompt instead (it starts clean). To allow a fork, raise max_forks in GUARDRAILS.md."
|
|
89
|
+
fi
|
|
90
|
+
gtmp="$(mktemp 2>/dev/null || echo "$STATE.gtmp")"
|
|
91
|
+
jq --argjson f "$((forked+1))" '.forks_spawned=$f' "$STATE" > "$gtmp" 2>/dev/null && mv "$gtmp" "$STATE" || rm -f "$gtmp" 2>/dev/null
|
|
92
|
+
fi
|
|
93
|
+
|
|
94
|
+
# (c) Total subagent budget (forks count here too). Past it, deny so the run continues
|
|
95
|
+
# serially instead of exploding. Counters live in state.json (best-effort).
|
|
96
|
+
max_sub="$(jq -r '.max_subagents // 8' "$STATE" 2>/dev/null || echo 8)"; case "$max_sub" in (*[!0-9]*|"") max_sub=8 ;; esac
|
|
97
|
+
spawned="$(jq -r '.subagents_spawned // 0' "$STATE" 2>/dev/null || echo 0)"; case "$spawned" in (*[!0-9]*|"") spawned=0 ;; esac
|
|
98
|
+
if [ "$spawned" -ge "$max_sub" ] 2>/dev/null; then
|
|
99
|
+
deny "Leopold guard: subagent budget exhausted ($spawned/$max_sub this run). Spawning many subagents multiplies cost — each re-loads context. Do this task yourself in-turn (work serially). Raise max_subagents in GUARDRAILS.md and restart if you truly must."
|
|
100
|
+
fi
|
|
101
|
+
gtmp="$(mktemp 2>/dev/null || echo "$STATE.gtmp")"
|
|
102
|
+
jq --argjson s "$((spawned+1))" '.subagents_spawned=$s' "$STATE" > "$gtmp" 2>/dev/null && mv "$gtmp" "$STATE" || rm -f "$gtmp" 2>/dev/null
|
|
103
|
+
# Audit trail: one line per spawn (size + fork flag) so a run's cost is inspectable.
|
|
104
|
+
sts="$(date -u +%Y-%m-%dT%H:%M:%SZ 2>/dev/null || echo '')"
|
|
105
|
+
printf '{"ts":"%s","event":"subagent_spawn","fork":%s,"prompt_kb":%s,"total":%s}\n' \
|
|
106
|
+
"$sts" "$([ "$stype" = fork ] && echo true || echo false)" "$(( ${plen:-0} / 1024 ))" "$((spawned+1))" \
|
|
107
|
+
>> "$LEO/events.jsonl" 2>/dev/null || true
|
|
108
|
+
;;
|
|
109
|
+
|
|
110
|
+
Bash)
|
|
111
|
+
cmd="$(printf '%s' "$input" | jq -r '.tool_input.command // empty' 2>/dev/null || true)"
|
|
112
|
+
# normalize: newlines/tabs -> space, collapse runs (defeats whitespace/tab evasion).
|
|
113
|
+
norm="$(printf '%s' "$cmd" | tr '\n\t' ' ' | tr -s ' ')"
|
|
114
|
+
|
|
115
|
+
# Opt-in deny-by-default (LEOPOLD_PARANOID=1): only a small allowlist of
|
|
116
|
+
# read/build/test/lint commands passes; everything else is denied. Best-effort
|
|
117
|
+
# (it keys off the first command word), kept off by default in favor of the
|
|
118
|
+
# hardened denylist below. Documented in docs/guardrails.md.
|
|
119
|
+
if [ "${LEOPOLD_PARANOID:-0}" = "1" ]; then
|
|
120
|
+
first="$(printf '%s' "$norm" | awk '{print $1}')"; base="${first##*/}"; ok=0
|
|
121
|
+
case "$base" in
|
|
122
|
+
ls|cat|head|tail|wc|grep|rg|awk|sed|find|file|stat|tree|pwd|cd|echo|printf|true|jq|make|node|npx|tsc|mkdir|cp|touch|test|diff|cmp) ok=1 ;;
|
|
123
|
+
git) case "$(git_subcmd "$norm")" in add|status|diff|log|show|rev-parse|branch|fetch|remote|config) ok=1 ;; esac ;;
|
|
124
|
+
npm|pnpm|yarn) matches "$norm" '[[:space:]](run|test|ci|pack|install|build|typecheck|lint)([[:space:]]|$)' && ok=1 ;;
|
|
125
|
+
esac
|
|
126
|
+
[ "$ok" = 1 ] || deny "Leopold PARANOID: '$base' is not on the allowlist (read/build/test/lint/git add). Unset LEOPOLD_PARANOID to use the default denylist."
|
|
127
|
+
fi
|
|
128
|
+
|
|
129
|
+
# --- destructive deletes (rm recursive+force, in any spelling) ----------
|
|
130
|
+
if matches "$norm" '(^|[^[:alnum:]_-])rm([[:space:]]|$)'; then
|
|
131
|
+
rec=0; force=0
|
|
132
|
+
matches "$norm" '(--recursive|(^|[[:space:]])-[a-z]*r)' && rec=1
|
|
133
|
+
matches "$norm" '(--force|(^|[[:space:]])-[a-z]*f)' && force=1
|
|
134
|
+
[ "$rec" = 1 ] && [ "$force" = 1 ] && \
|
|
135
|
+
deny "Leopold guard: recursive+forced rm is forbidden in autonomous mode (any spelling). Stop and let the human run it."
|
|
136
|
+
fi
|
|
137
|
+
# find-based deletion
|
|
138
|
+
if matches "$norm" '(^|[^[:alnum:]_-])find([[:space:]]|$)'; then
|
|
139
|
+
matches "$norm" '[[:space:]]-delete([[:space:]]|$)' && \
|
|
140
|
+
deny "Leopold guard: 'find ... -delete' is forbidden in autonomous mode."
|
|
141
|
+
matches "$norm" '-exec[[:space:]]+rm([[:space:]]|$)' && \
|
|
142
|
+
deny "Leopold guard: 'find ... -exec rm' is forbidden in autonomous mode."
|
|
143
|
+
fi
|
|
144
|
+
|
|
145
|
+
# --- git, by resolved subcommand (bypass-resistant) ---------------------
|
|
146
|
+
gsub="$(git_subcmd "$norm")"
|
|
147
|
+
case "$gsub" in
|
|
148
|
+
reset) matches "$norm" '(^|[[:space:]])--hard([[:space:]]|$)' && \
|
|
149
|
+
deny "Leopold guard: 'git reset --hard' is forbidden in autonomous mode." ;;
|
|
150
|
+
clean) matches "$norm" '(--force|(^|[[:space:]])-[a-z]*f)' && \
|
|
151
|
+
deny "Leopold guard: 'git clean -f' is forbidden in autonomous mode." ;;
|
|
152
|
+
branch) matches "$norm" '(^|[[:space:]])-D([[:space:]]|$)' && \
|
|
153
|
+
deny "Leopold guard: 'git branch -D' is forbidden in autonomous mode." ;;
|
|
154
|
+
push)
|
|
155
|
+
matches "$norm" '(--force|--force-with-lease|(^|[[:space:]])-f([[:space:]]|$))' && \
|
|
156
|
+
deny "Leopold guard: force-push is forbidden in autonomous mode."
|
|
157
|
+
has_token ALLOW_PUSH || \
|
|
158
|
+
deny "Leopold guard: git push is locked. Pushing is the user's call; report readiness instead." ;;
|
|
159
|
+
commit)
|
|
160
|
+
has_token ALLOW_GIT || \
|
|
161
|
+
deny "Leopold guard: git commit is locked. Stage the work (git add) and report instead. To allow this session: touch .leopold/ALLOW_GIT" ;;
|
|
162
|
+
esac
|
|
163
|
+
|
|
164
|
+
# --- outbound: PRs / releases / publishing ------------------------------
|
|
165
|
+
if matches "$norm" '(^|[^[:alnum:]_-])gh([[:space:]].*)?(pr[[:space:]]+(create|merge)|release[[:space:]]+create)'; then
|
|
166
|
+
has_token ALLOW_PUSH || \
|
|
167
|
+
deny "Leopold guard: opening/merging PRs and creating releases is locked. Report readiness instead."
|
|
168
|
+
fi
|
|
169
|
+
if matches "$norm" '(npm|pnpm|yarn)[[:space:]]+publish|cargo[[:space:]]+publish|twine[[:space:]]+upload|pip[[:space:]].*upload'; then
|
|
170
|
+
has_token ALLOW_PUBLISH || \
|
|
171
|
+
deny "Leopold guard: publishing packages is locked in autonomous mode."
|
|
172
|
+
fi
|
|
173
|
+
;;
|
|
174
|
+
|
|
175
|
+
Edit|Write|MultiEdit|NotebookEdit)
|
|
176
|
+
path="$(printf '%s' "$input" | jq -r '.tool_input.file_path // .tool_input.path // empty' 2>/dev/null || true)"
|
|
177
|
+
case "$path" in
|
|
178
|
+
*/GUARDRAILS.md) deny "Leopold guard: GUARDRAILS.md is immutable during an autonomous run." ;;
|
|
179
|
+
*/settings.json|*/settings.local.json) deny "Leopold guard: editing Claude Code settings is forbidden in autonomous mode." ;;
|
|
180
|
+
*/leopold/hooks/*|*/.leopold/state.json) deny "Leopold guard: the guardrail hooks and run state are immutable during an autonomous run." ;;
|
|
181
|
+
esac
|
|
182
|
+
;;
|
|
183
|
+
esac
|
|
184
|
+
|
|
185
|
+
exit 0
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"description": "Leopold hooks: Stop-hook continuity and the PreToolUse git-lock guard. Both are no-ops unless a Leopold run is active in the project.",
|
|
3
|
+
"hooks": {
|
|
4
|
+
"Stop": [
|
|
5
|
+
{
|
|
6
|
+
"hooks": [
|
|
7
|
+
{ "type": "command", "command": "bash \"${CLAUDE_PLUGIN_ROOT}/hooks/stop-continuity.sh\"" }
|
|
8
|
+
]
|
|
9
|
+
}
|
|
10
|
+
],
|
|
11
|
+
"PreToolUse": [
|
|
12
|
+
{
|
|
13
|
+
"matcher": "Bash|Edit|Write|MultiEdit|NotebookEdit",
|
|
14
|
+
"hooks": [
|
|
15
|
+
{ "type": "command", "command": "bash \"${CLAUDE_PLUGIN_ROOT}/hooks/guard-irreversible.sh\"" }
|
|
16
|
+
]
|
|
17
|
+
}
|
|
18
|
+
]
|
|
19
|
+
}
|
|
20
|
+
}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Leopold Stop hook: state-coupled autonomous continuity.
|
|
3
|
+
#
|
|
4
|
+
# When the main agent finishes a turn, this hook reads .leopold/state.json and
|
|
5
|
+
# PLAN.md in the project. If a Leopold run is active and work remains with no
|
|
6
|
+
# stop condition met, it blocks the stop and re-injects a compact "continue"
|
|
7
|
+
# instruction. Otherwise it allows the session to halt.
|
|
8
|
+
#
|
|
9
|
+
# Hook contract (Claude Code Stop): read JSON on stdin. To keep going, print
|
|
10
|
+
# {"decision":"block","reason":"..."} and exit 0. To allow stopping, exit 0 with
|
|
11
|
+
# no output. Failure mode is intentionally fail-open (allow stop): a broken hook
|
|
12
|
+
# must never trap a session in a loop.
|
|
13
|
+
|
|
14
|
+
input="$(cat 2>/dev/null || true)"
|
|
15
|
+
|
|
16
|
+
# jq is required to parse state safely; without it, allow the stop.
|
|
17
|
+
command -v jq >/dev/null 2>&1 || exit 0
|
|
18
|
+
|
|
19
|
+
cwd="$(printf '%s' "$input" | jq -r '.cwd // empty' 2>/dev/null || true)"
|
|
20
|
+
[ -z "${cwd:-}" ] && cwd="$PWD"
|
|
21
|
+
LEO="$cwd/.leopold"
|
|
22
|
+
STATE="$LEO/state.json"
|
|
23
|
+
|
|
24
|
+
# Not a Leopold run -> normal stop.
|
|
25
|
+
[ -f "$STATE" ] || exit 0
|
|
26
|
+
|
|
27
|
+
now="$(date -u +%Y-%m-%dT%H:%M:%SZ 2>/dev/null || echo '')"
|
|
28
|
+
|
|
29
|
+
# Fail SAFE and LOUD on a broken state file. Malformed JSON, or a missing/non-numeric
|
|
30
|
+
# budget field, means the iteration budget cannot be trusted -- the run could loop
|
|
31
|
+
# forever on a silently-skipped budget. Stop the run and say why, rather than continue
|
|
32
|
+
# blindly or die in silence.
|
|
33
|
+
state_invalid() {
|
|
34
|
+
printf '{"ts":"%s","event":"state_invalid","reason":"%s"}\n' "$now" "$1" >> "$LEO/events.jsonl" 2>/dev/null || true
|
|
35
|
+
echo "Leopold: .leopold/state.json is invalid ($1) -- stopping the run (fail-safe). Fix the file or re-run /leopold-brief." >&2
|
|
36
|
+
printf '{"active":false,"stopped_reason":"state_invalid"}\n' > "$STATE" 2>/dev/null || true
|
|
37
|
+
exit 0
|
|
38
|
+
}
|
|
39
|
+
jq -e . "$STATE" >/dev/null 2>&1 || state_invalid "malformed JSON"
|
|
40
|
+
|
|
41
|
+
active="$(jq -r '.active // false' "$STATE" 2>/dev/null || echo false)"
|
|
42
|
+
[ "$active" = "true" ] || exit 0
|
|
43
|
+
|
|
44
|
+
# A present-but-non-numeric budget field is the real hole: it would make the
|
|
45
|
+
# `iter >= max` test below error out (and get swallowed), silently skipping the
|
|
46
|
+
# budget -> unbounded loop. So any present budget field must be an integer. Missing
|
|
47
|
+
# fields fall back to the safe defaults below (50 / 0 / 3), so the budget still holds.
|
|
48
|
+
for f in iteration max_iterations consecutive_failures max_failures; do
|
|
49
|
+
v="$(jq -r --arg k "$f" '.[$k] // empty' "$STATE" 2>/dev/null)"
|
|
50
|
+
if [ -n "$v" ] && ! printf '%s' "$v" | grep -qE '^[0-9]+$'; then
|
|
51
|
+
state_invalid "non-numeric $f ($v)"
|
|
52
|
+
fi
|
|
53
|
+
done
|
|
54
|
+
|
|
55
|
+
log_event() { printf '%s\n' "$1" >> "$LEO/events.jsonl" 2>/dev/null || true; }
|
|
56
|
+
|
|
57
|
+
allow_stop() {
|
|
58
|
+
local r="$1" tmp
|
|
59
|
+
tmp="$(mktemp 2>/dev/null || echo "$STATE.tmp")"
|
|
60
|
+
jq --arg r "$r" '.active=false | .stopped_reason=$r' "$STATE" > "$tmp" 2>/dev/null && mv "$tmp" "$STATE" || true
|
|
61
|
+
log_event "{\"ts\":\"$now\",\"event\":\"stop\",\"reason\":\"$r\"}"
|
|
62
|
+
# Safety hygiene: always clear the kill switch and per-session git opt-in tokens
|
|
63
|
+
# so the next run re-locks git and does not halt immediately on a stale STOP.
|
|
64
|
+
rm -f "$LEO/STOP" "$LEO/ALLOW_GIT" "$LEO/ALLOW_PUSH" "$LEO/ALLOW_PUBLISH" 2>/dev/null || true
|
|
65
|
+
# on_finish policy (GUARDRAILS.md): archive the run logs on a clean finish.
|
|
66
|
+
if [ "$r" = "plan_complete" ] && grep -qiE '^[[:space:]]*-?[[:space:]]*on_finish:[[:space:]]*archive' "$LEO/GUARDRAILS.md" 2>/dev/null; then
|
|
67
|
+
arch="$LEO/runs/$(date -u +%Y%m%dT%H%M%SZ)"
|
|
68
|
+
mkdir -p "$arch" 2>/dev/null || true
|
|
69
|
+
[ -f "$LEO/DECISIONS.md" ] && mv "$LEO/DECISIONS.md" "$arch/" 2>/dev/null || true
|
|
70
|
+
[ -f "$LEO/events.jsonl" ] && mv "$LEO/events.jsonl" "$arch/" 2>/dev/null || true
|
|
71
|
+
cp "$LEO/PLAN.md" "$arch/" 2>/dev/null || true
|
|
72
|
+
fi
|
|
73
|
+
exit 0
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
# Kill switch.
|
|
77
|
+
if [ -f "$LEO/STOP" ]; then allow_stop "kill_switch"; fi
|
|
78
|
+
|
|
79
|
+
iter="$(jq -r '.iteration // 0' "$STATE" 2>/dev/null || echo 0)"
|
|
80
|
+
max_iter="$(jq -r '.max_iterations // 50' "$STATE" 2>/dev/null || echo 50)"
|
|
81
|
+
fails="$(jq -r '.consecutive_failures // 0' "$STATE" 2>/dev/null || echo 0)"
|
|
82
|
+
max_fails="$(jq -r '.max_failures // 3' "$STATE" 2>/dev/null || echo 3)"
|
|
83
|
+
|
|
84
|
+
# Budgets and repeated failure.
|
|
85
|
+
if [ "$iter" -ge "$max_iter" ] 2>/dev/null; then allow_stop "iteration_budget"; fi
|
|
86
|
+
if [ "$fails" -ge "$max_fails" ] 2>/dev/null; then allow_stop "repeated_failure"; fi
|
|
87
|
+
|
|
88
|
+
# Context budget — the real money pit. A long run accumulates context every turn; on a
|
|
89
|
+
# big-context model it never auto-compacts, so each turn re-bills the whole (growing)
|
|
90
|
+
# transcript and any fork clones it (one report: a session ballooned to ~6MB over 681
|
|
91
|
+
# turns). Stop when the transcript passes max_context_mb (default 5) so it cannot silently
|
|
92
|
+
# balloon. The brief persists -> a fresh /leopold-run resumes from PLAN.md with clean context.
|
|
93
|
+
max_ctx_mb="$(jq -r '.max_context_mb // 5' "$STATE" 2>/dev/null || echo 5)"
|
|
94
|
+
case "$max_ctx_mb" in (*[!0-9]*|"") max_ctx_mb=5 ;; esac
|
|
95
|
+
ctx_mb=0
|
|
96
|
+
tpath="$(printf '%s' "$input" | jq -r '.transcript_path // empty' 2>/dev/null || true)"
|
|
97
|
+
if [ -n "$tpath" ] && [ -f "$tpath" ]; then
|
|
98
|
+
ctx_bytes="$(wc -c < "$tpath" 2>/dev/null || echo 0)"
|
|
99
|
+
ctx_mb="$(awk "BEGIN{printf \"%.1f\", ${ctx_bytes:-0}/1048576}" 2>/dev/null || echo 0)"
|
|
100
|
+
[ "$(( ${ctx_bytes:-0} / 1048576 ))" -ge "$max_ctx_mb" ] 2>/dev/null && allow_stop "context_budget"
|
|
101
|
+
fi
|
|
102
|
+
|
|
103
|
+
# Plan complete? (no unchecked checkboxes remain)
|
|
104
|
+
PLAN="$LEO/PLAN.md"
|
|
105
|
+
open_items="$(grep -cE '^[[:space:]]*- \[ \]' "$PLAN" 2>/dev/null || true)"
|
|
106
|
+
open_items="${open_items:-0}"
|
|
107
|
+
if [ "$open_items" -eq 0 ] 2>/dev/null; then allow_stop "plan_complete"; fi
|
|
108
|
+
|
|
109
|
+
# Loop detection: if the SET of open plan items is byte-identical for N consecutive
|
|
110
|
+
# turns, the run is thrashing without progress (no item checked off, no item added) ->
|
|
111
|
+
# stop and report rather than burn the whole iteration budget hammering one spot.
|
|
112
|
+
max_np="$(jq -r '.max_no_progress // 6' "$STATE" 2>/dev/null || echo 6)"
|
|
113
|
+
case "$max_np" in (*[!0-9]*|"") max_np=6 ;; esac
|
|
114
|
+
sig="$(grep -E '^[[:space:]]*- \[ \]' "$PLAN" 2>/dev/null | cksum | awk '{print $1}')"
|
|
115
|
+
last_sig="$(jq -r '.progress_sig // empty' "$STATE" 2>/dev/null || true)"
|
|
116
|
+
np="$(jq -r '.no_progress // 0' "$STATE" 2>/dev/null || echo 0)"
|
|
117
|
+
case "$np" in (*[!0-9]*|"") np=0 ;; esac
|
|
118
|
+
if [ "$sig" = "$last_sig" ] && [ -n "$last_sig" ]; then np=$((np + 1)); else np=0; fi
|
|
119
|
+
if [ "$np" -ge "$max_np" ] 2>/dev/null; then allow_stop "no_progress"; fi
|
|
120
|
+
|
|
121
|
+
# Otherwise: continue. Increment the iteration counter; persist the progress signature.
|
|
122
|
+
next=$((iter + 1))
|
|
123
|
+
tmp="$(mktemp 2>/dev/null || echo "$STATE.tmp")"
|
|
124
|
+
jq --argjson n "$next" --arg t "$now" --argjson np "$np" --arg sig "$sig" --argjson cm "${ctx_mb:-0}" --arg tp "${tpath:-}" \
|
|
125
|
+
'.iteration=$n | .last_turn=$t | .no_progress=$np | .progress_sig=$sig | .context_mb=$cm
|
|
126
|
+
| (if $tp != "" then .transcript_path=$tp else . end)' "$STATE" > "$tmp" 2>/dev/null && mv "$tmp" "$STATE" || true
|
|
127
|
+
log_event "{\"ts\":\"$now\",\"event\":\"turn_start\",\"iteration\":$next,\"open_items\":$open_items,\"no_progress\":$np}"
|
|
128
|
+
|
|
129
|
+
reason="Leopold autonomous mode is ACTIVE (turn $next/$max_iter, $open_items open plan items). Do not stop. Steps: (1) Read .leopold/PLAN.md and pick the next unchecked item. (2) Complete it; reach for the gstack playbook skill that fits the situation. (3) On any fork, apply .leopold/CHARTER.md and the decision protocol: if the call is reversible OR the charter is clear, decide it yourself, append the decision to .leopold/DECISIONS.md, and keep going; stop only for an irreversible AND ambiguous fork, a charter contradiction, or a mission-premise change. (4) Mark the finished item as done ([x]) in PLAN.md. Hard rules: git commit/push/publish stay locked; never edit files outside this project; never touch .leopold/GUARDRAILS.md or the hooks. When the plan is complete or a stop condition is met, write a short final summary and then stop."
|
|
130
|
+
|
|
131
|
+
jq -cn --arg r "$reason" '{decision:"block", reason:$r}'
|
|
132
|
+
exit 0
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Leopold installer.
|
|
3
|
+
# Copies the skills into ~/.claude/skills/, the hooks/templates/docs into
|
|
4
|
+
# ~/.claude/leopold/, and wires the Stop + PreToolUse hooks into
|
|
5
|
+
# ~/.claude/settings.json (idempotent, with a backup). The hooks are no-ops
|
|
6
|
+
# unless a Leopold run is active, so they are safe to leave installed.
|
|
7
|
+
set -euo pipefail
|
|
8
|
+
|
|
9
|
+
# Resolve the source tree. When run from a clone, that is the script's dir.
|
|
10
|
+
# When piped (curl ... | bash), there is no local tree, so we fetch one first.
|
|
11
|
+
_self="${BASH_SOURCE[0]:-}"
|
|
12
|
+
if [ -n "$_self" ] && [ -d "$(dirname "$_self")/skills" ]; then
|
|
13
|
+
SRC="$(cd "$(dirname "$_self")" && pwd)"
|
|
14
|
+
else
|
|
15
|
+
SRC="${LEOPOLD_SRC:-$HOME/.local/share/leopold}"
|
|
16
|
+
echo "-> fetching Leopold into $SRC"
|
|
17
|
+
if [ -d "$SRC/.git" ]; then
|
|
18
|
+
( cd "$SRC" && git pull --ff-only -q ) || true
|
|
19
|
+
else
|
|
20
|
+
mkdir -p "$(dirname "$SRC")"
|
|
21
|
+
git clone --progress --depth 1 https://github.com/Jonhvmp/leopold.git "$SRC"
|
|
22
|
+
fi
|
|
23
|
+
fi
|
|
24
|
+
CLAUDE="${CLAUDE_HOME:-$HOME/.claude}"
|
|
25
|
+
SKILLS="$CLAUDE/skills"
|
|
26
|
+
LEO_HOME="$CLAUDE/leopold"
|
|
27
|
+
SETTINGS="$CLAUDE/settings.json"
|
|
28
|
+
|
|
29
|
+
# Optional gstack integration: pass --with-gstack to install it non-interactively.
|
|
30
|
+
WITH_GSTACK=0
|
|
31
|
+
for _a in "$@"; do [ "$_a" = "--with-gstack" ] && WITH_GSTACK=1; done
|
|
32
|
+
|
|
33
|
+
echo "Leopold installer"
|
|
34
|
+
echo " source: $SRC"
|
|
35
|
+
echo " target: $CLAUDE"
|
|
36
|
+
echo
|
|
37
|
+
|
|
38
|
+
mkdir -p "$SKILLS" "$LEO_HOME"
|
|
39
|
+
|
|
40
|
+
echo "-> installing skills"
|
|
41
|
+
for d in "$SRC"/skills/*/; do
|
|
42
|
+
name="$(basename "$d")"
|
|
43
|
+
rm -rf "${SKILLS:?}/$name"
|
|
44
|
+
cp -R "$d" "$SKILLS/$name"
|
|
45
|
+
echo " $name"
|
|
46
|
+
done
|
|
47
|
+
|
|
48
|
+
echo "-> installing hooks, templates, docs, extensions"
|
|
49
|
+
cp -R "$SRC/hooks" "$LEO_HOME/"
|
|
50
|
+
cp -R "$SRC/templates" "$LEO_HOME/"
|
|
51
|
+
cp -R "$SRC/docs" "$LEO_HOME/" 2>/dev/null || true
|
|
52
|
+
cp -R "$SRC/scripts" "$LEO_HOME/" 2>/dev/null || true
|
|
53
|
+
cp -R "$SRC/extensions" "$LEO_HOME/" 2>/dev/null || true
|
|
54
|
+
cp "$SRC/VERSION" "$LEO_HOME/" 2>/dev/null || true
|
|
55
|
+
chmod +x "$LEO_HOME"/hooks/*.sh
|
|
56
|
+
chmod +x "$LEO_HOME"/scripts/*.sh 2>/dev/null || true
|
|
57
|
+
chmod +x "$LEO_HOME"/extensions/*/manage.sh 2>/dev/null || true
|
|
58
|
+
|
|
59
|
+
STOP_HOOK="$LEO_HOME/hooks/stop-continuity.sh"
|
|
60
|
+
GUARD_HOOK="$LEO_HOME/hooks/guard-irreversible.sh"
|
|
61
|
+
|
|
62
|
+
echo "-> wiring hooks into $SETTINGS"
|
|
63
|
+
if ! command -v jq >/dev/null 2>&1; then
|
|
64
|
+
echo
|
|
65
|
+
echo " jq not found. Add this to $SETTINGS manually:"
|
|
66
|
+
sed "s#~/.claude/leopold#$LEO_HOME#g" "$SRC/settings.template.json"
|
|
67
|
+
echo
|
|
68
|
+
else
|
|
69
|
+
[ -f "$SETTINGS" ] || echo '{}' > "$SETTINGS"
|
|
70
|
+
cp "$SETTINGS" "$SETTINGS.leopold.bak"
|
|
71
|
+
tmp="$(mktemp)"
|
|
72
|
+
jq --arg stop "$STOP_HOOK" --arg guard "$GUARD_HOOK" '
|
|
73
|
+
.hooks //= {}
|
|
74
|
+
| .hooks.Stop //= []
|
|
75
|
+
| .hooks.PreToolUse //= []
|
|
76
|
+
| (if any(.hooks.Stop[]?.hooks[]?; .command == $stop)
|
|
77
|
+
then . else .hooks.Stop += [{hooks:[{type:"command",command:$stop}]}] end)
|
|
78
|
+
| (if any(.hooks.PreToolUse[]?.hooks[]?; .command == $guard)
|
|
79
|
+
then . else .hooks.PreToolUse += [{matcher:"Bash|Edit|Write|MultiEdit|NotebookEdit",hooks:[{type:"command",command:$guard}]}] end)
|
|
80
|
+
' "$SETTINGS" > "$tmp" && mv "$tmp" "$SETTINGS"
|
|
81
|
+
echo " merged (backup at $SETTINGS.leopold.bak)"
|
|
82
|
+
fi
|
|
83
|
+
|
|
84
|
+
# Serena — MANDATORY. LSP-backed code intelligence (MCP): symbol-level retrieval/editing
|
|
85
|
+
# instead of grep + whole-file reads. It is the biggest lever for code quality AND for
|
|
86
|
+
# keeping context lean (fewer tokens per operation), so Leopold sets it up for everyone.
|
|
87
|
+
echo
|
|
88
|
+
echo "-> setting up Serena (LSP code intelligence — mandatory for quality + lean context)"
|
|
89
|
+
SERENA_MGR="$LEO_HOME/extensions/serena/manage.sh"
|
|
90
|
+
if [ -f "$SERENA_MGR" ]; then
|
|
91
|
+
bash "$SERENA_MGR" install || echo " Serena setup did not finish; complete it with: make serena-install (or: make menu)"
|
|
92
|
+
else
|
|
93
|
+
echo " (serena extension missing from this build; skipping)"
|
|
94
|
+
fi
|
|
95
|
+
|
|
96
|
+
# The `leopold` CLI — so `leopold menu / watch / doctor / serena` work from anywhere,
|
|
97
|
+
# no repo and no `make`. Skip if it's already here (e.g. you ran `leopold install`).
|
|
98
|
+
echo
|
|
99
|
+
if command -v leopold >/dev/null 2>&1; then
|
|
100
|
+
echo "-> leopold CLI already installed"
|
|
101
|
+
elif command -v npm >/dev/null 2>&1; then
|
|
102
|
+
echo "-> installing the leopold CLI (npm i -g leopold-driver)"
|
|
103
|
+
if npm i -g leopold-driver >/dev/null 2>&1; then
|
|
104
|
+
if command -v leopold >/dev/null 2>&1; then
|
|
105
|
+
echo " ok 'leopold' command ready (leopold menu · watch · doctor)"
|
|
106
|
+
else
|
|
107
|
+
echo " installed, but 'leopold' isn't on PATH yet — open a new shell (or: hash -r)"
|
|
108
|
+
fi
|
|
109
|
+
else
|
|
110
|
+
echo " warn: 'npm i -g leopold-driver' failed (permissions?) — run it yourself, maybe with sudo"
|
|
111
|
+
fi
|
|
112
|
+
else
|
|
113
|
+
echo "-> npm not found — the 'leopold' CLI needs Node/npm. Then: npm i -g leopold-driver"
|
|
114
|
+
fi
|
|
115
|
+
|
|
116
|
+
echo
|
|
117
|
+
GSTACK_DIR="$SKILLS/gstack"
|
|
118
|
+
gstack_present() { [ -d "$GSTACK_DIR" ] || ls "$SKILLS" 2>/dev/null | grep -q '^spec$'; }
|
|
119
|
+
install_gstack() {
|
|
120
|
+
echo "-> installing gstack (MIT, by Garry Tan: https://github.com/garrytan/gstack)"
|
|
121
|
+
command -v bun >/dev/null 2>&1 || echo " note: gstack needs Bun v1.0+ (https://bun.sh); its setup will guide you."
|
|
122
|
+
echo " cloning gstack (shows progress) + running its setup…"
|
|
123
|
+
if git clone --progress --single-branch --depth 1 https://github.com/garrytan/gstack.git "$GSTACK_DIR" && ( cd "$GSTACK_DIR" && ./setup ); then
|
|
124
|
+
echo " gstack installed."
|
|
125
|
+
else
|
|
126
|
+
echo " gstack install did not finish; retry with: make gstack-install"
|
|
127
|
+
fi
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if gstack_present; then
|
|
131
|
+
echo "gstack detected: Leopold will conduct its planning toolchain (/spec, /autoplan, /plan-*-review, ...)."
|
|
132
|
+
elif [ "$WITH_GSTACK" = "1" ]; then
|
|
133
|
+
install_gstack
|
|
134
|
+
else
|
|
135
|
+
echo "gstack not detected. Leopold works on plain Claude Code, but it shines when it can conduct"
|
|
136
|
+
echo "gstack's planning toolchain (/autoplan, /plan-eng-review, /spec). gstack is a separate MIT"
|
|
137
|
+
echo "project by Garry Tan: https://github.com/garrytan/gstack"
|
|
138
|
+
if [ -t 0 ]; then
|
|
139
|
+
printf "Install gstack now? (clones to %s, runs its setup, needs Bun) [y/N] " "$GSTACK_DIR"
|
|
140
|
+
read -r _ans || _ans=""
|
|
141
|
+
case "$_ans" in [yY]*) install_gstack ;; *) echo "Skipped. Install later: make gstack-install" ;; esac
|
|
142
|
+
else
|
|
143
|
+
echo "Enable it later with: make gstack-install (or re-run ./install.sh --with-gstack)"
|
|
144
|
+
fi
|
|
145
|
+
fi
|
|
146
|
+
|
|
147
|
+
# Verify the install: skills, hooks, and the leopold CLI should all be in place.
|
|
148
|
+
echo
|
|
149
|
+
echo "-> verifying"
|
|
150
|
+
v_warn=0
|
|
151
|
+
sc="$(ls "$SKILLS" 2>/dev/null | grep -c '^leopold-' || true)"
|
|
152
|
+
[ "${sc:-0}" -ge 4 ] 2>/dev/null && echo " ok $sc leopold skills installed" || { echo " warn: leopold skills not found in $SKILLS"; v_warn=$((v_warn+1)); }
|
|
153
|
+
if command -v jq >/dev/null 2>&1 && [ -f "$SETTINGS" ] && jq -e '(.hooks.Stop|length>0) and (.hooks.PreToolUse|length>0)' "$SETTINGS" >/dev/null 2>&1; then
|
|
154
|
+
echo " ok Stop + PreToolUse hooks wired in settings.json"
|
|
155
|
+
else echo " warn: hooks not detected in settings.json"; v_warn=$((v_warn+1)); fi
|
|
156
|
+
command -v leopold >/dev/null 2>&1 && echo " ok leopold CLI on PATH" || { echo " warn: 'leopold' not on PATH yet (open a new shell, or: npm i -g leopold-driver)"; v_warn=$((v_warn+1)); }
|
|
157
|
+
command -v serena >/dev/null 2>&1 && echo " ok serena (LSP) present" || echo " note: serena not on PATH — run: leopold serena install"
|
|
158
|
+
[ "$v_warn" -eq 0 ] && echo " all good." || echo " $v_warn warning(s) above — see the hints."
|
|
159
|
+
|
|
160
|
+
echo
|
|
161
|
+
echo "Done. In any project:"
|
|
162
|
+
echo " /leopold-brief debate the mission, write the brief"
|
|
163
|
+
echo " /leopold-run hand over the seat"
|
|
164
|
+
echo " /leopold-status see where it is"
|
|
165
|
+
echo " /leopold-stop take the seat back"
|
|
166
|
+
echo " (or from a shell: leopold menu · leopold watch · leopold doctor)"
|
|
167
|
+
echo
|
|
168
|
+
# Offer the toolchain manager. We read from /dev/tty (the controlling terminal),
|
|
169
|
+
# not stdin, so this works even when the installer is piped: `curl ... | bash`
|
|
170
|
+
# leaves stdin as the script, but the terminal is still reachable via /dev/tty.
|
|
171
|
+
MENU="$LEO_HOME/scripts/leopold-menu.sh"
|
|
172
|
+
if exec 3<>/dev/tty 2>/dev/null; then
|
|
173
|
+
printf "Open the toolchain manager (install/manage gstack, ovmem, ...)? [Y/n] " >&3
|
|
174
|
+
read -r _ans <&3 || _ans="y"
|
|
175
|
+
case "$_ans" in
|
|
176
|
+
[nN]*) echo "Skipped. Open it anytime: bash $MENU (or: make menu)" ;;
|
|
177
|
+
*) bash "$MENU" <&3 || true ;;
|
|
178
|
+
esac
|
|
179
|
+
exec 3>&-
|
|
180
|
+
else
|
|
181
|
+
# no terminal (headless / CI): just point at it
|
|
182
|
+
echo "Manage the toolchain (gstack, ovmem, ...):"
|
|
183
|
+
echo " bash $MENU (or: make menu)"
|
|
184
|
+
fi
|
|
Binary file
|