Skip to content

Commit 848c710

Browse files
author
supernormxl
committed
Merge remote-tracking branch 'origin/will/port-unmerged-fixes'
2 parents 3af01de + e3e4570 commit 848c710

31 files changed

Lines changed: 399 additions & 82 deletions

.github/FUNDING.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
custom: ["https://alivecomputer.com"]
1+
custom: ["https://walnut.world"]

.github/ISSUE_TEMPLATE/config.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
blank_issues_enabled: true
22
contact_links:
33
- name: Community Discussion
4-
url: https://github.com/alivecomputer/alive-claude/discussions
4+
url: https://github.com/stackwalnuts/walnut/discussions
55
about: Questions, ideas, and show & tell

.github/ISSUE_TEMPLATE/feature_request.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ assignees: ''
77
---
88

99
**Component:** <!-- skill | rule | hook | template | runtime | other -->
10-
**Affects:** <!-- e.g. alive:save, session lifecycle, capsule routing -->
10+
**Affects:** <!-- e.g. walnut:save, session lifecycle, capsule routing -->
1111

1212
## Problem
1313

.github/PULL_REQUEST_TEMPLATE.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121

2222
## Session context
2323

24-
<!-- Optional: paste your squirrel YAML entry if this was built inside an ALIVE session. -->
24+
<!-- Optional: paste your squirrel YAML entry if this was built inside a walnut session. -->
2525

2626
```yaml
2727
```

.github/workflows/validate-plugin.yml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,10 @@ jobs:
1616

1717
- name: Validate plugin structure
1818
run: |
19-
PLUGIN_DIR="plugins/alive"
19+
PLUGIN_DIR="plugins/walnut"
2020
ERRORS=0
2121
22-
echo "=== Validating ALIVE plugin structure ==="
22+
echo "=== Validating Walnut plugin structure ==="
2323
2424
# Check required top-level files
2525
for file in CLAUDE.md; do
@@ -88,7 +88,7 @@ jobs:
8888
# Check templates exist
8989
echo ""
9090
echo "=== Templates ==="
91-
for template_dir in alive walnut capsule squirrel; do
91+
for template_dir in world walnut capsule squirrel; do
9292
if [ ! -d "$PLUGIN_DIR/templates/$template_dir" ]; then
9393
echo "⚠️ Template directory '$template_dir' not found (optional)"
9494
else

deploy.sh

Lines changed: 33 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,37 +1,54 @@
11
#!/bin/bash
2-
# Deploy alive plugin from local clone to cache
2+
# Deploy walnut plugin from local clone to cache + marketplace
33
# Usage: ./deploy.sh [--dry-run]
44

55
set -euo pipefail
66

7-
SOURCE="$(cd "$(dirname "$0")/plugins/alive" && pwd)"
8-
CACHE="$HOME/.claude/plugins/cache/alivecomputer/alive/1.0.1-beta"
7+
SOURCE="$(cd "$(dirname "$0")/plugins/walnut" && pwd)"
8+
CACHE="$HOME/.claude/plugins/cache/stackwalnuts/walnut/1.0.0"
9+
MARKETPLACE="$HOME/.claude/plugins/marketplaces/stackwalnuts/plugins/walnut"
910

1011
if [ ! -d "$SOURCE" ]; then
1112
echo "ERROR: Source not found at $SOURCE"
1213
exit 1
1314
fi
1415

15-
if [ ! -d "$CACHE" ]; then
16-
echo "ERROR: Cache not found at $CACHE"
17-
exit 1
18-
fi
19-
2016
DRY_RUN=""
2117
if [ "${1:-}" = "--dry-run" ]; then
2218
DRY_RUN="--dry-run"
2319
echo "=== DRY RUN ==="
2420
fi
2521

26-
echo "Source: $SOURCE"
27-
echo "Cache: $CACHE"
22+
echo "Source: $SOURCE"
23+
echo "Cache: $CACHE"
24+
echo "Marketplace: $MARKETPLACE"
2825
echo ""
2926

30-
rsync -av --delete \
31-
--exclude='.git' \
32-
--exclude='.DS_Store' \
33-
$DRY_RUN \
34-
"$SOURCE/" "$CACHE/"
27+
# Deploy to cache (if it exists)
28+
if [ -d "$CACHE" ]; then
29+
rsync -av --delete \
30+
--exclude='.git' \
31+
--exclude='.DS_Store' \
32+
$DRY_RUN \
33+
"$SOURCE/" "$CACHE/"
34+
echo ""
35+
echo "Cache deployed."
36+
else
37+
echo "Cache dir not found at $CACHE — skipping."
38+
fi
39+
40+
# Deploy to marketplace (if it exists)
41+
if [ -d "$MARKETPLACE" ]; then
42+
rsync -av --delete \
43+
--exclude='.git' \
44+
--exclude='.DS_Store' \
45+
$DRY_RUN \
46+
"$SOURCE/" "$MARKETPLACE/"
47+
echo ""
48+
echo "Marketplace deployed."
49+
else
50+
echo "Marketplace dir not found at $MARKETPLACE — skipping."
51+
fi
3552

3653
echo ""
37-
echo "Deployed $(date '+%Y-%m-%d %H:%M:%S')"
54+
echo "Done $(date '+%Y-%m-%d %H:%M:%S')"

plugins/walnut/CLAUDE.md

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ version: 1.0.1-beta
33
runtime: squirrel.core@1.0
44
---
55

6-
# ALIVE
6+
# Walnut
77

88
**Personal Private Context Infrastructure**
99

@@ -25,7 +25,7 @@ When a walnut is active, read these in order before responding:
2525
5. `_core/log.md` — frontmatter, then first ~100 lines
2626
6. `.walnut/_squirrels/` — scan for unsaved entries
2727
7. `_core/_capsules/` — companion frontmatter only
28-
9. `.walnut/preferences.yaml` — full (if exists)
28+
8. `.walnut/preferences.yaml` — full (if exists)
2929

3030
Do not respond about a walnut without reading its core files. Never guess at file contents.
3131

@@ -44,13 +44,14 @@ Do not respond about a walnut without reading its core files. Never guess at fil
4444

4545
---
4646

47-
## Twelve Skills
47+
## Thirteen Skills
4848

4949
```
5050
/walnut:world see your world
5151
/walnut:load load a walnut (prev. open)
5252
/walnut:save checkpoint — route stash, update state
5353
/walnut:capture context in — store, route
54+
/walnut:capsule create, share, graduate capsules
5455
/walnut:find search across walnuts
5556
/walnut:create scaffold a new walnut
5657
/walnut:tidy system maintenance

plugins/walnut/hooks/hooks.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
{
2-
"description": "Walnut v1.0.1-beta — 13 hooks. Session hooks read/write .walnut/_squirrels/. All read stdin JSON for session_id.",
2+
"description": "Walnut v1.0.1-beta — 14 hooks. Session hooks read/write .walnut/_squirrels/. All read stdin JSON for session_id.",
33
"hooks": {
44
"SessionStart": [
55
{

plugins/walnut/hooks/scripts/walnut-archive-enforcer.sh

Lines changed: 48 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,17 +10,37 @@ find_world || exit 0
1010

1111
COMMAND=$(echo "$HOOK_INPUT" | jq -r '.tool_input.command // empty')
1212

13-
# Check for destructive commands
13+
# Check for destructive commands — grep -E \s works on macOS, unlike sed
1414
if ! echo "$COMMAND" | grep -qE '(^|\s|;|&&|\|)(rm|rmdir|unlink)\s'; then
1515
exit 0
1616
fi
1717

18-
# Extract target paths after the rm/rmdir/unlink command
19-
TARGET=$(echo "$COMMAND" | sed -E 's/.*\b(rm|rmdir|unlink)\s+(-[^ ]+ )*//' | tr ' ' '\n' | grep -v '^-')
18+
# Extract target paths using python3 for reliable parsing
19+
# Handles: quoted paths, spaces in filenames, flags, chained commands, multiple targets
20+
TARGET=$(echo "$COMMAND" | python3 -c "
21+
import sys, shlex, re
22+
cmd = sys.stdin.read().strip()
23+
for part in re.split(r'[;&|]+', cmd):
24+
part = part.strip()
25+
try: tokens = shlex.split(part)
26+
except ValueError: tokens = part.split()
27+
found = False
28+
for t in tokens:
29+
if not found:
30+
if t in ('rm', 'rmdir', 'unlink'):
31+
found = True
32+
continue
33+
if not t.startswith('-'):
34+
print(t)
35+
" 2>/dev/null)
2036

2137
# Use cwd from JSON input for resolving relative paths
2238
RESOLVE_DIR="${HOOK_CWD:-$PWD}"
2339

40+
# Process ALL targets — rename every World file, then deny once
41+
RENAMED=""
42+
NOT_FOUND=""
43+
2444
while IFS= read -r path; do
2545
[ -z "$path" ] && continue
2646

@@ -34,10 +54,33 @@ while IFS= read -r path; do
3454
# Check if resolved path is inside the World (protect entire root, not just subdirs)
3555
case "$resolved" in
3656
"$WORLD_ROOT"|"$WORLD_ROOT"/*)
37-
echo '{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"deny","permissionDecisionReason":"Deletion blocked inside Walnut world. Archive instead — move to 01_Archive/."}}'
38-
exit 0
57+
if [ -e "$resolved" ]; then
58+
DIRNAME=$(dirname "$resolved")
59+
BASENAME=$(basename "$resolved")
60+
MARKED="${DIRNAME}/${BASENAME} (Marked for Deletion)"
61+
python3 -c "import os,sys; os.rename(sys.argv[1], sys.argv[2])" "$resolved" "$MARKED" 2>/dev/null || true
62+
open "$DIRNAME" 2>/dev/null || true
63+
RENAMED="${RENAMED}${BASENAME}, "
64+
else
65+
NOT_FOUND="${NOT_FOUND}$(basename "$resolved"), "
66+
fi
3967
;;
4068
esac
4169
done <<< "$TARGET"
4270

71+
# Build denial message from all processed targets
72+
if [ -n "$RENAMED" ] || [ -n "$NOT_FOUND" ]; then
73+
REASON=""
74+
if [ -n "$RENAMED" ]; then
75+
REASON="Renamed to (Marked for Deletion): ${RENAMED%, }. Review in Finder and delete manually if intended."
76+
fi
77+
if [ -n "$NOT_FOUND" ]; then
78+
[ -n "$REASON" ] && REASON="$REASON "
79+
REASON="${REASON}Not found (may already be removed): ${NOT_FOUND%, }."
80+
fi
81+
REASON_ESCAPED=$(echo "$REASON" | python3 -c "import sys,json; print(json.dumps(sys.stdin.read().strip()))" 2>/dev/null || echo "\"Deletion blocked inside Walnut world.\"")
82+
echo "{\"hookSpecificOutput\":{\"hookEventName\":\"PreToolUse\",\"permissionDecision\":\"deny\",\"permissionDecisionReason\":${REASON_ESCAPED}}}"
83+
exit 0
84+
fi
85+
4386
exit 0

plugins/walnut/hooks/scripts/walnut-context-watch.sh

Lines changed: 126 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
#!/bin/bash
22
# Hook: Context Watch — UserPromptSubmit
3-
# Checks if the current walnut's state files were modified by another session.
4-
# If so, injects additionalContext suggesting a context refresh.
3+
# Two jobs:
4+
# 1. Context % re-injection — at every 20% threshold, re-inject rules + context
5+
# 2. External change detection — if another session modified walnut state files
56

67
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
78
source "$SCRIPT_DIR/walnut-common.sh"
@@ -12,6 +13,125 @@ find_world || exit 0
1213
SESSION_ID="${HOOK_SESSION_ID}"
1314
[ -z "$SESSION_ID" ] && exit 0
1415

16+
# ── CONTEXT % RE-INJECTION ──────────────────────────────────────
17+
18+
CTX_FILE="$WORLD_ROOT/.walnut/.context_pct"
19+
if [ -f "$CTX_FILE" ]; then
20+
CTX_PCT=$(cat "$CTX_FILE" 2>/dev/null | tr -d '[:space:]')
21+
22+
if [ -n "$CTX_PCT" ] && [ "$CTX_PCT" -gt 0 ] 2>/dev/null; then
23+
# Find highest unfired threshold — inject once, not serially across prompts
24+
FIRE_THRESHOLD=""
25+
for THRESHOLD in 80 60 40 20; do
26+
MARKER="/tmp/walnut-ctx-${SESSION_ID}-${THRESHOLD}"
27+
if [ "$CTX_PCT" -ge "$THRESHOLD" ] && [ ! -f "$MARKER" ]; then
28+
FIRE_THRESHOLD="$THRESHOLD"
29+
break
30+
fi
31+
done
32+
33+
if [ -n "$FIRE_THRESHOLD" ]; then
34+
# Mark all thresholds at or below the fired one
35+
for T in 20 40 60 80; do
36+
if [ "$T" -le "$FIRE_THRESHOLD" ]; then
37+
touch "/tmp/walnut-ctx-${SESSION_ID}-${T}"
38+
fi
39+
done
40+
THRESHOLD="$FIRE_THRESHOLD"
41+
42+
# Build injection content based on threshold level
43+
if [ "$THRESHOLD" -le 40 ]; then
44+
# Condensed refresh
45+
REFRESH="<WALNUT_REFRESH threshold=\"${THRESHOLD}%\">
46+
Context is at ${CTX_PCT}%. Refreshing core behaviours:
47+
- Stash decisions, tasks, and notes. Surface on change.
48+
- Verify past context via subagent before asserting. Never guess from memory.
49+
- Capsule awareness: deliverable or future audience = capsule. Prefer capsules over loose files.
50+
- Read before speaking. Never answer from memory about file contents.
51+
- Check the world key (injected at start) for walnut registry, people, credentials.
52+
</WALNUT_REFRESH>"
53+
else
54+
# Full re-injection at 60%+ — read world key and index
55+
WORLD_KEY=""
56+
[ -f "$WORLD_ROOT/.walnut/key.md" ] && WORLD_KEY=$(cat "$WORLD_ROOT/.walnut/key.md")
57+
WORLD_INDEX=""
58+
[ -f "$WORLD_ROOT/.walnut/_index.yaml" ] && WORLD_INDEX=$(cat "$WORLD_ROOT/.walnut/_index.yaml")
59+
60+
REFRESH="<WALNUT_REFRESH threshold=\"${THRESHOLD}%\">
61+
Context is at ${CTX_PCT}%. Full context refresh:
62+
- Stash decisions, tasks, and notes. Surface on change.
63+
- Verify past context via subagent before asserting. Never guess from memory.
64+
- Capsule awareness: deliverable or future audience = capsule.
65+
- Read before speaking. Never answer from memory about file contents.
66+
67+
World Key:
68+
${WORLD_KEY}
69+
70+
World Index:
71+
${WORLD_INDEX}
72+
</WALNUT_REFRESH>"
73+
fi
74+
75+
# Scan active squirrel stashes for cross-pollination
76+
ACTIVE_STASHES=""
77+
if command -v python3 &>/dev/null; then
78+
ACTIVE_STASHES=$(python3 -c "
79+
import os, glob, re
80+
sid = '$SESSION_ID'
81+
squirrels = glob.glob('$WORLD_ROOT/.walnut/_squirrels/*.yaml')
82+
for f in squirrels:
83+
with open(f) as fh:
84+
content = fh.read()
85+
# Skip our own session (check filename, not content — avoids false match if SID appears in stash text)
86+
if os.path.basename(f).replace('.yaml','') == sid:
87+
continue
88+
# Check if ended: null (still active) and saves: 0 (genuinely unsaved — saved stash is historical)
89+
if 'ended: null' not in content:
90+
continue
91+
saves_m = re.search(r'^saves:\s*(\d+)', content, re.M)
92+
if saves_m and int(saves_m.group(1)) > 0:
93+
continue
94+
# Extract walnut and stash
95+
walnut = ''
96+
m = re.search(r'^walnut:\s*(.+)', content, re.M)
97+
if m:
98+
walnut = m.group(1).strip()
99+
if walnut == 'null' or not walnut:
100+
continue
101+
# Extract stash items
102+
stash_items = re.findall(r'content:\s*\"?(.+?)\"?\s*$', content, re.M)
103+
if stash_items:
104+
print(f'Active session on {walnut}: ' + '; '.join(stash_items[:5]))
105+
" 2>/dev/null || true)
106+
fi
107+
108+
if [ -n "$ACTIVE_STASHES" ]; then
109+
REFRESH="${REFRESH}
110+
111+
<ACTIVE_SQUIRRELS>
112+
${ACTIVE_STASHES}
113+
</ACTIVE_SQUIRRELS>"
114+
fi
115+
116+
REFRESH_ESCAPED=$(escape_for_json "$REFRESH")
117+
118+
# Hook can only return one JSON response, so re-injection takes priority.
119+
# External change detection runs on every other prompt (re-injection fires at most 4x per session).
120+
cat <<REFRESHEOF
121+
{
122+
"hookSpecificOutput": {
123+
"hookEventName": "UserPromptSubmit",
124+
"additionalContext": "${REFRESH_ESCAPED}"
125+
}
126+
}
127+
REFRESHEOF
128+
exit 0
129+
fi
130+
fi
131+
fi
132+
133+
# ── EXTERNAL CHANGE DETECTION ───────────────────────────────────
134+
15135
# Find which walnut this session is working on
16136
SQUIRRELS_DIR="$WORLD_ROOT/.walnut/_squirrels"
17137
ENTRY="$SQUIRRELS_DIR/$SESSION_ID.yaml"
@@ -64,8 +184,10 @@ date +%s > "$LASTCHECK"
64184
[ -z "${CHANGED:-}" ] && exit 0
65185

66186
# Check if the change was made by US (same session_id in now.md squirrel field)
67-
LAST_SQUIRREL=$(grep '^squirrel:' "$WALNUT_CORE/now.md" 2>/dev/null | sed 's/squirrel: *//' || true)
68-
if [ "${LAST_SQUIRREL:-}" = "$SESSION_ID" ]; then
187+
# now.md uses short IDs (first 8 chars), hook gets full UUID — check both
188+
LAST_SQUIRREL=$(grep '^squirrel:' "$WALNUT_CORE/now.md" 2>/dev/null | sed 's/squirrel: *//' | tr -d '[:space:]' || true)
189+
SHORT_SID="${SESSION_ID:0:8}"
190+
if [ "${LAST_SQUIRREL:-}" = "$SESSION_ID" ] || [ "${LAST_SQUIRREL:-}" = "$SHORT_SID" ]; then
69191
exit 0
70192
fi
71193

0 commit comments

Comments
 (0)