Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
49b80e4
fix: critical runtime bugs — setup scaffolds .walnut/, capsules path,…
willsupernormal Mar 26, 2026
f744cb3
fix: log guardian recognises MCP signatures, rules guardian uses corr…
willsupernormal Mar 26, 2026
e1f6370
fix: deploy.sh — correct paths, deploy to both cache and marketplace
willsupernormal Mar 26, 2026
36982d6
fix: GitHub configs — URLs, skill names, branding updated to walnut/s…
willsupernormal Mar 26, 2026
1b354ac
fix: branding sweep — CLAUDE.md heading, skill refs, demo output upda…
willsupernormal Mar 26, 2026
8219811
fix: stale comment in scanner.ts — .alive/ reference
willsupernormal Mar 26, 2026
89c5aeb
revert: keep "ALIVE walnut context" in Hermes skill — ALIVE is the fr…
willsupernormal Mar 26, 2026
f4d7460
refactor: rename templates/alive/ to templates/world/ — world-level c…
willsupernormal Mar 26, 2026
1739dfa
fix: CI workflow validates plugins/walnut/ not plugins/alive/
willsupernormal Mar 27, 2026
7937d1b
refactor: rename templates/alive/ directory to templates/world/
willsupernormal Mar 27, 2026
0d1e747
fix: archive enforcer — POSIX sed replacement, rename-not-deny, multi…
willsupernormal Mar 27, 2026
48b4367
fix: context-watch — add threshold injection, stash cross-pollination…
willsupernormal Mar 27, 2026
677973f
fix: session-new — inject statusline into settings.json with absolute…
willsupernormal Mar 27, 2026
146ca1c
fix: session-resume + session-compact — saves:0 stash gate + statusli…
willsupernormal Mar 27, 2026
ddcdf4c
fix: statusline — null cost crash guard + context_pct file write
willsupernormal Mar 27, 2026
7625c02
fix: squirrels rule — add instincts #10 and #11, fix save guard, save…
willsupernormal Mar 27, 2026
42e3ae5
fix: world skill reads injected index, post-write auto-regenerates, c…
willsupernormal Mar 27, 2026
27cb6cf
fix: skill descriptions + CLAUDE.md — routing, capsule skill, numbering
willsupernormal Mar 27, 2026
e3e4570
fix: cross-platform stat in post-write debounce + setup.md wording
willsupernormal Mar 27, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/FUNDING.yml
Original file line number Diff line number Diff line change
@@ -1 +1 @@
custom: ["https://alivecomputer.com"]
custom: ["https://walnut.world"]
2 changes: 1 addition & 1 deletion .github/ISSUE_TEMPLATE/config.yml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
blank_issues_enabled: true
contact_links:
- name: Community Discussion
url: https://github.com/alivecomputer/alive-claude/discussions
url: https://github.com/stackwalnuts/walnut/discussions
about: Questions, ideas, and show & tell
2 changes: 1 addition & 1 deletion .github/ISSUE_TEMPLATE/feature_request.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ assignees: ''
---

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

## Problem

Expand Down
2 changes: 1 addition & 1 deletion .github/PULL_REQUEST_TEMPLATE.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@

## Session context

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

```yaml
```
6 changes: 3 additions & 3 deletions .github/workflows/validate-plugin.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,10 @@ jobs:

- name: Validate plugin structure
run: |
PLUGIN_DIR="plugins/alive"
PLUGIN_DIR="plugins/walnut"
ERRORS=0

echo "=== Validating ALIVE plugin structure ==="
echo "=== Validating Walnut plugin structure ==="

# Check required top-level files
for file in CLAUDE.md; do
Expand Down Expand Up @@ -88,7 +88,7 @@ jobs:
# Check templates exist
echo ""
echo "=== Templates ==="
for template_dir in alive walnut capsule squirrel; do
for template_dir in world walnut capsule squirrel; do
if [ ! -d "$PLUGIN_DIR/templates/$template_dir" ]; then
echo "⚠️ Template directory '$template_dir' not found (optional)"
else
Expand Down
49 changes: 33 additions & 16 deletions deploy.sh
Original file line number Diff line number Diff line change
@@ -1,37 +1,54 @@
#!/bin/bash
# Deploy alive plugin from local clone to cache
# Deploy walnut plugin from local clone to cache + marketplace
# Usage: ./deploy.sh [--dry-run]

set -euo pipefail

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

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

if [ ! -d "$CACHE" ]; then
echo "ERROR: Cache not found at $CACHE"
exit 1
fi

DRY_RUN=""
if [ "${1:-}" = "--dry-run" ]; then
DRY_RUN="--dry-run"
echo "=== DRY RUN ==="
fi

echo "Source: $SOURCE"
echo "Cache: $CACHE"
echo "Source: $SOURCE"
echo "Cache: $CACHE"
echo "Marketplace: $MARKETPLACE"
echo ""

rsync -av --delete \
--exclude='.git' \
--exclude='.DS_Store' \
$DRY_RUN \
"$SOURCE/" "$CACHE/"
# Deploy to cache (if it exists)
if [ -d "$CACHE" ]; then
rsync -av --delete \
--exclude='.git' \
--exclude='.DS_Store' \
$DRY_RUN \
"$SOURCE/" "$CACHE/"
echo ""
echo "Cache deployed."
else
echo "Cache dir not found at $CACHE — skipping."
fi

# Deploy to marketplace (if it exists)
if [ -d "$MARKETPLACE" ]; then
rsync -av --delete \
--exclude='.git' \
--exclude='.DS_Store' \
$DRY_RUN \
"$SOURCE/" "$MARKETPLACE/"
echo ""
echo "Marketplace deployed."
else
echo "Marketplace dir not found at $MARKETPLACE — skipping."
fi

echo ""
echo "Deployed $(date '+%Y-%m-%d %H:%M:%S')"
echo "Done $(date '+%Y-%m-%d %H:%M:%S')"
7 changes: 4 additions & 3 deletions plugins/walnut/CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ version: 1.0.1-beta
runtime: squirrel.core@1.0
---

# ALIVE
# Walnut

**Personal Private Context Infrastructure**

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

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

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

---

## Twelve Skills
## Thirteen Skills

```
/walnut:world see your world
/walnut:load load a walnut (prev. open)
/walnut:save checkpoint — route stash, update state
/walnut:capture context in — store, route
/walnut:capsule create, share, graduate capsules
/walnut:find search across walnuts
/walnut:create scaffold a new walnut
/walnut:tidy system maintenance
Expand Down
2 changes: 1 addition & 1 deletion plugins/walnut/hooks/hooks.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"description": "Walnut v1.0.1-beta — 13 hooks. Session hooks read/write .walnut/_squirrels/. All read stdin JSON for session_id.",
"description": "Walnut v1.0.1-beta — 14 hooks. Session hooks read/write .walnut/_squirrels/. All read stdin JSON for session_id.",
"hooks": {
"SessionStart": [
{
Expand Down
53 changes: 48 additions & 5 deletions plugins/walnut/hooks/scripts/walnut-archive-enforcer.sh
Original file line number Diff line number Diff line change
Expand Up @@ -10,17 +10,37 @@ find_world || exit 0

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

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

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

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

# Process ALL targets — rename every World file, then deny once
RENAMED=""
NOT_FOUND=""

while IFS= read -r path; do
[ -z "$path" ] && continue

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

# Build denial message from all processed targets
if [ -n "$RENAMED" ] || [ -n "$NOT_FOUND" ]; then
REASON=""
if [ -n "$RENAMED" ]; then
REASON="Renamed to (Marked for Deletion): ${RENAMED%, }. Review in Finder and delete manually if intended."
fi
if [ -n "$NOT_FOUND" ]; then
[ -n "$REASON" ] && REASON="$REASON "
REASON="${REASON}Not found (may already be removed): ${NOT_FOUND%, }."
fi
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.\"")
echo "{\"hookSpecificOutput\":{\"hookEventName\":\"PreToolUse\",\"permissionDecision\":\"deny\",\"permissionDecisionReason\":${REASON_ESCAPED}}}"
exit 0
fi

exit 0
130 changes: 126 additions & 4 deletions plugins/walnut/hooks/scripts/walnut-context-watch.sh
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
#!/bin/bash
# Hook: Context Watch — UserPromptSubmit
# Checks if the current walnut's state files were modified by another session.
# If so, injects additionalContext suggesting a context refresh.
# Two jobs:
# 1. Context % re-injection — at every 20% threshold, re-inject rules + context
# 2. External change detection — if another session modified walnut state files

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

# ── CONTEXT % RE-INJECTION ──────────────────────────────────────

CTX_FILE="$WORLD_ROOT/.walnut/.context_pct"
if [ -f "$CTX_FILE" ]; then
CTX_PCT=$(cat "$CTX_FILE" 2>/dev/null | tr -d '[:space:]')

if [ -n "$CTX_PCT" ] && [ "$CTX_PCT" -gt 0 ] 2>/dev/null; then
# Find highest unfired threshold — inject once, not serially across prompts
FIRE_THRESHOLD=""
for THRESHOLD in 80 60 40 20; do
MARKER="/tmp/walnut-ctx-${SESSION_ID}-${THRESHOLD}"
if [ "$CTX_PCT" -ge "$THRESHOLD" ] && [ ! -f "$MARKER" ]; then
FIRE_THRESHOLD="$THRESHOLD"
break
fi
done

if [ -n "$FIRE_THRESHOLD" ]; then
# Mark all thresholds at or below the fired one
for T in 20 40 60 80; do
if [ "$T" -le "$FIRE_THRESHOLD" ]; then
touch "/tmp/walnut-ctx-${SESSION_ID}-${T}"
fi
done
THRESHOLD="$FIRE_THRESHOLD"

# Build injection content based on threshold level
if [ "$THRESHOLD" -le 40 ]; then
# Condensed refresh
REFRESH="<WALNUT_REFRESH threshold=\"${THRESHOLD}%\">
Context is at ${CTX_PCT}%. Refreshing core behaviours:
- Stash decisions, tasks, and notes. Surface on change.
- Verify past context via subagent before asserting. Never guess from memory.
- Capsule awareness: deliverable or future audience = capsule. Prefer capsules over loose files.
- Read before speaking. Never answer from memory about file contents.
- Check the world key (injected at start) for walnut registry, people, credentials.
</WALNUT_REFRESH>"
else
# Full re-injection at 60%+ — read world key and index
WORLD_KEY=""
[ -f "$WORLD_ROOT/.walnut/key.md" ] && WORLD_KEY=$(cat "$WORLD_ROOT/.walnut/key.md")
WORLD_INDEX=""
[ -f "$WORLD_ROOT/.walnut/_index.yaml" ] && WORLD_INDEX=$(cat "$WORLD_ROOT/.walnut/_index.yaml")

REFRESH="<WALNUT_REFRESH threshold=\"${THRESHOLD}%\">
Context is at ${CTX_PCT}%. Full context refresh:
- Stash decisions, tasks, and notes. Surface on change.
- Verify past context via subagent before asserting. Never guess from memory.
- Capsule awareness: deliverable or future audience = capsule.
- Read before speaking. Never answer from memory about file contents.

World Key:
${WORLD_KEY}

World Index:
${WORLD_INDEX}
</WALNUT_REFRESH>"
fi

# Scan active squirrel stashes for cross-pollination
ACTIVE_STASHES=""
if command -v python3 &>/dev/null; then
ACTIVE_STASHES=$(python3 -c "
import os, glob, re
sid = '$SESSION_ID'
squirrels = glob.glob('$WORLD_ROOT/.walnut/_squirrels/*.yaml')
for f in squirrels:
with open(f) as fh:
content = fh.read()
# Skip our own session (check filename, not content — avoids false match if SID appears in stash text)
if os.path.basename(f).replace('.yaml','') == sid:
continue
# Check if ended: null (still active) and saves: 0 (genuinely unsaved — saved stash is historical)
if 'ended: null' not in content:
continue
saves_m = re.search(r'^saves:\s*(\d+)', content, re.M)
if saves_m and int(saves_m.group(1)) > 0:
continue
# Extract walnut and stash
walnut = ''
m = re.search(r'^walnut:\s*(.+)', content, re.M)
if m:
walnut = m.group(1).strip()
if walnut == 'null' or not walnut:
continue
# Extract stash items
stash_items = re.findall(r'content:\s*\"?(.+?)\"?\s*$', content, re.M)
if stash_items:
print(f'Active session on {walnut}: ' + '; '.join(stash_items[:5]))
" 2>/dev/null || true)
fi

if [ -n "$ACTIVE_STASHES" ]; then
REFRESH="${REFRESH}

<ACTIVE_SQUIRRELS>
${ACTIVE_STASHES}
</ACTIVE_SQUIRRELS>"
fi

REFRESH_ESCAPED=$(escape_for_json "$REFRESH")

# Hook can only return one JSON response, so re-injection takes priority.
# External change detection runs on every other prompt (re-injection fires at most 4x per session).
cat <<REFRESHEOF
{
"hookSpecificOutput": {
"hookEventName": "UserPromptSubmit",
"additionalContext": "${REFRESH_ESCAPED}"
}
}
REFRESHEOF
exit 0
fi
fi
fi

# ── EXTERNAL CHANGE DETECTION ───────────────────────────────────

# Find which walnut this session is working on
SQUIRRELS_DIR="$WORLD_ROOT/.walnut/_squirrels"
ENTRY="$SQUIRRELS_DIR/$SESSION_ID.yaml"
Expand Down Expand Up @@ -64,8 +184,10 @@ date +%s > "$LASTCHECK"
[ -z "${CHANGED:-}" ] && exit 0

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

Expand Down
2 changes: 1 addition & 1 deletion plugins/walnut/hooks/scripts/walnut-log-guardian.sh
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ fi
# For Edit: check if the old_string contains a signed entry
OLD_STRING=$(echo "$HOOK_INPUT" | jq -r '.tool_input.old_string // empty')

if echo "$OLD_STRING" | grep -q 'signed: squirrel:'; then
if echo "$OLD_STRING" | grep -qE 'signed: (squirrel:|walnut-mcp:)'; then
echo '{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"deny","permissionDecisionReason":"log.md is immutable. That entry is signed — add a correction entry instead."}}'
exit 0
fi
Expand Down
Loading
Loading