diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml
index 2f38c45..a0cc428 100644
--- a/.github/FUNDING.yml
+++ b/.github/FUNDING.yml
@@ -1 +1 @@
-custom: ["https://alivecomputer.com"]
+custom: ["https://walnut.world"]
diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml
index 96d63f1..2662863 100644
--- a/.github/ISSUE_TEMPLATE/config.yml
+++ b/.github/ISSUE_TEMPLATE/config.yml
@@ -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
diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md
index 99b3846..6ecac27 100644
--- a/.github/ISSUE_TEMPLATE/feature_request.md
+++ b/.github/ISSUE_TEMPLATE/feature_request.md
@@ -7,7 +7,7 @@ assignees: ''
---
**Component:**
-**Affects:**
+**Affects:**
## Problem
diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md
index 0467acc..d020fe7 100644
--- a/.github/PULL_REQUEST_TEMPLATE.md
+++ b/.github/PULL_REQUEST_TEMPLATE.md
@@ -21,7 +21,7 @@
## Session context
-
+
```yaml
```
diff --git a/.github/workflows/validate-plugin.yml b/.github/workflows/validate-plugin.yml
index d5888c9..923fc31 100644
--- a/.github/workflows/validate-plugin.yml
+++ b/.github/workflows/validate-plugin.yml
@@ -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
@@ -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
diff --git a/deploy.sh b/deploy.sh
index a3c8969..77b1c4d 100755
--- a/deploy.sh
+++ b/deploy.sh
@@ -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')"
diff --git a/plugins/walnut/CLAUDE.md b/plugins/walnut/CLAUDE.md
index ac9c7e0..68b3471 100644
--- a/plugins/walnut/CLAUDE.md
+++ b/plugins/walnut/CLAUDE.md
@@ -3,7 +3,7 @@ version: 1.0.1-beta
runtime: squirrel.core@1.0
---
-# ALIVE
+# Walnut
**Personal Private Context Infrastructure**
@@ -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.
@@ -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
diff --git a/plugins/walnut/hooks/hooks.json b/plugins/walnut/hooks/hooks.json
index bbc6526..7d6e757 100644
--- a/plugins/walnut/hooks/hooks.json
+++ b/plugins/walnut/hooks/hooks.json
@@ -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": [
{
diff --git a/plugins/walnut/hooks/scripts/walnut-archive-enforcer.sh b/plugins/walnut/hooks/scripts/walnut-archive-enforcer.sh
index c07ff3d..e87c13b 100755
--- a/plugins/walnut/hooks/scripts/walnut-archive-enforcer.sh
+++ b/plugins/walnut/hooks/scripts/walnut-archive-enforcer.sh
@@ -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
@@ -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
diff --git a/plugins/walnut/hooks/scripts/walnut-context-watch.sh b/plugins/walnut/hooks/scripts/walnut-context-watch.sh
index 17aeb1f..287760f 100755
--- a/plugins/walnut/hooks/scripts/walnut-context-watch.sh
+++ b/plugins/walnut/hooks/scripts/walnut-context-watch.sh
@@ -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"
@@ -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="
+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.
+"
+ 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="
+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}
+"
+ 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_STASHES}
+"
+ 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 < "$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
diff --git a/plugins/walnut/hooks/scripts/walnut-log-guardian.sh b/plugins/walnut/hooks/scripts/walnut-log-guardian.sh
index 2fb17aa..94d15d2 100755
--- a/plugins/walnut/hooks/scripts/walnut-log-guardian.sh
+++ b/plugins/walnut/hooks/scripts/walnut-log-guardian.sh
@@ -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
diff --git a/plugins/walnut/hooks/scripts/walnut-post-write.sh b/plugins/walnut/hooks/scripts/walnut-post-write.sh
index f9b5196..db4a203 100644
--- a/plugins/walnut/hooks/scripts/walnut-post-write.sh
+++ b/plugins/walnut/hooks/scripts/walnut-post-write.sh
@@ -1,6 +1,8 @@
#!/bin/bash
# Hook: Post Write — PostToolUse (Write|Edit)
-# Tracks write activity for statusline display.
+# Two jobs:
+# 1. Track write activity for statusline display
+# 2. Regenerate world index after save (detected by _core/now.md write)
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
source "$SCRIPT_DIR/walnut-common.sh"
@@ -19,4 +21,31 @@ if [ -n "${HOOK_SESSION_ID}" ]; then
fi
fi
+# Regenerate world index after save
+# now.md is only written by save (per rules) — use it as the trigger
+FILE_PATH=$(echo "$HOOK_INPUT" | jq -r '.tool_input.file_path // empty')
+case "$FILE_PATH" in
+ */_core/now.md)
+ GENERATOR="$WORLD_ROOT/.walnut/scripts/generate-index.py"
+ # Fall back to plugin scripts dir
+ [ ! -f "$GENERATOR" ] && GENERATOR="${CLAUDE_PLUGIN_ROOT:-$(cd "$SCRIPT_DIR/../.." && pwd)}/scripts/generate-index.py"
+ if [ -f "$GENERATOR" ]; then
+ # Debounce — skip if we regenerated in the last 5 minutes
+ MARKER="/tmp/walnut-index-regen"
+ if [ -f "$MARKER" ]; then
+ if stat --version >/dev/null 2>&1; then
+ MARKER_MTIME=$(stat -c %Y "$MARKER" 2>/dev/null || echo "0")
+ else
+ MARKER_MTIME=$(stat -f %m "$MARKER" 2>/dev/null || echo "0")
+ fi
+ AGE=$(( $(date +%s) - MARKER_MTIME ))
+ [ "$AGE" -lt 300 ] && exit 0
+ fi
+ touch "$MARKER"
+ # Background — don't block the session
+ python3 "$GENERATOR" "$WORLD_ROOT" > /dev/null 2>&1 &
+ fi
+ ;;
+esac
+
exit 0
diff --git a/plugins/walnut/hooks/scripts/walnut-rules-guardian.sh b/plugins/walnut/hooks/scripts/walnut-rules-guardian.sh
index f106403..16c4b90 100755
--- a/plugins/walnut/hooks/scripts/walnut-rules-guardian.sh
+++ b/plugins/walnut/hooks/scripts/walnut-rules-guardian.sh
@@ -66,7 +66,7 @@ esac
# Block: anything in the Walnut plugin cache
case "$FILE_PATH" in
- */.claude/plugins/cache/alivecomputer/walnut/*)
+ */.claude/plugins/cache/stackwalnuts/walnut/*)
echo "$DENY_MSG"
exit 0
;;
diff --git a/plugins/walnut/hooks/scripts/walnut-session-compact.sh b/plugins/walnut/hooks/scripts/walnut-session-compact.sh
index 166411d..04c4a13 100755
--- a/plugins/walnut/hooks/scripts/walnut-session-compact.sh
+++ b/plugins/walnut/hooks/scripts/walnut-session-compact.sh
@@ -58,7 +58,13 @@ WALNUT=""
STASH="(empty)"
if [ -n "${ENTRY:-}" ] && [ -f "$ENTRY" ]; then
WALNUT=$(grep '^walnut:' "$ENTRY" | head -1 | sed 's/walnut: *//' || true)
- STASH=$(awk '/^stash:/{found=1; next} found && /^[a-z]/{found=0} found && /content:/{gsub(/.*content: *"?/,""); gsub(/"$/,""); print "- " $0}' "$ENTRY" 2>/dev/null || true)
+ # Only show stash if this entry was never saved (saves: 0) — saved stash items were already routed
+ SAVES=$(grep '^saves:' "$ENTRY" | head -1 | sed 's/saves: *//' | tr -d '[:space:]' || echo "0")
+ if [ "$SAVES" = "0" ]; then
+ STASH=$(awk '/^stash:/{found=1; next} found && /^[a-z]/{found=0} found && /content:/{gsub(/.*content: *"?/,""); gsub(/"$/,""); print "- " $0}' "$ENTRY" 2>/dev/null || true)
+ else
+ STASH=""
+ fi
if [ -z "${STASH:-}" ]; then
STASH="(empty)"
fi
diff --git a/plugins/walnut/hooks/scripts/walnut-session-new.sh b/plugins/walnut/hooks/scripts/walnut-session-new.sh
index 8f0695a..ee8c204 100755
--- a/plugins/walnut/hooks/scripts/walnut-session-new.sh
+++ b/plugins/walnut/hooks/scripts/walnut-session-new.sh
@@ -136,6 +136,43 @@ if [ -f "$STATUSLINE_SRC" ]; then
fi
fi
+# Inject statusline into settings.json — absolute path with self-heal
+SETTINGS_DIR="$WORLD_ROOT/.claude"
+SETTINGS_FILE="$SETTINGS_DIR/settings.json"
+mkdir -p "$SETTINGS_DIR"
+if [ ! -f "$SETTINGS_FILE" ]; then
+ cat > "$SETTINGS_FILE" << SETTINGSEOF
+{
+ "statusLine": {
+ "type": "command",
+ "command": "$WORLD_ROOT/.walnut/statusline.sh"
+ }
+}
+SETTINGSEOF
+else
+ # settings.json exists — ensure statusLine is present and uses absolute path
+ if command -v python3 &>/dev/null; then
+ WALNUT_SETTINGS_FILE="$SETTINGS_FILE" WALNUT_WORLD_ROOT="$WORLD_ROOT" python3 -c "
+import json, os, sys
+sf = os.environ['WALNUT_SETTINGS_FILE']
+wr = os.environ['WALNUT_WORLD_ROOT']
+expected = wr + '/.walnut/statusline.sh'
+try:
+ with open(sf) as f:
+ data = json.load(f)
+except (json.JSONDecodeError, ValueError):
+ print('WALNUT: settings.json is malformed, cannot inject statusLine', file=sys.stderr)
+ sys.exit(0)
+current = data.get('statusLine', {}).get('command', '')
+if current != expected:
+ data['statusLine'] = {'type': 'command', 'command': expected}
+ with open(sf, 'w') as f:
+ json.dump(data, f, indent=2)
+ f.write('\n')
+" 2>/dev/null || true
+ fi
+fi
+
# Read world key (.walnut/key.md) for injection
WORLD_KEY_CONTENT=""
WORLD_KEY_FILE="$WORLD_ROOT/.walnut/key.md"
@@ -143,9 +180,9 @@ if [ -f "$WORLD_KEY_FILE" ]; then
WORLD_KEY_CONTENT=$(cat "$WORLD_KEY_FILE")
fi
-# Read world index (.alive/_index.yaml) for injection — walnut registry
+# Read world index (.walnut/_index.yaml) for injection — walnut registry
WORLD_INDEX_CONTENT=""
-WORLD_INDEX_FILE="$WORLD_ROOT/.alive/_index.yaml"
+WORLD_INDEX_FILE="$WORLD_ROOT/.walnut/_index.yaml"
if [ -f "$WORLD_INDEX_FILE" ]; then
WORLD_INDEX_CONTENT="
$(cat "$WORLD_INDEX_FILE")
diff --git a/plugins/walnut/hooks/scripts/walnut-session-resume.sh b/plugins/walnut/hooks/scripts/walnut-session-resume.sh
index 6802f6f..b232b83 100755
--- a/plugins/walnut/hooks/scripts/walnut-session-resume.sh
+++ b/plugins/walnut/hooks/scripts/walnut-session-resume.sh
@@ -45,6 +45,41 @@ PREAMBLE="
The following are your core operating rules for the Walnut system. They are MANDATORY — not suggestions, not defaults, not guidelines. You MUST follow them in every response, every tool call, every session.
"
+# Self-heal statusline — ensure settings.json has absolute path
+SETTINGS_DIR="$WORLD_ROOT/.claude"
+SETTINGS_FILE="$SETTINGS_DIR/settings.json"
+mkdir -p "$SETTINGS_DIR"
+if [ ! -f "$SETTINGS_FILE" ]; then
+ cat > "$SETTINGS_FILE" << SETTINGSEOF
+{
+ "statusLine": {
+ "type": "command",
+ "command": "$WORLD_ROOT/.walnut/statusline.sh"
+ }
+}
+SETTINGSEOF
+else
+ if command -v python3 &>/dev/null; then
+ WALNUT_SETTINGS_FILE="$SETTINGS_FILE" WALNUT_WORLD_ROOT="$WORLD_ROOT" python3 -c "
+import json, os, sys
+sf = os.environ['WALNUT_SETTINGS_FILE']
+wr = os.environ['WALNUT_WORLD_ROOT']
+expected = wr + '/.walnut/statusline.sh'
+try:
+ with open(sf) as f:
+ data = json.load(f)
+except (json.JSONDecodeError, ValueError):
+ sys.exit(0)
+current = data.get('statusLine', {}).get('command', '')
+if current != expected:
+ data['statusLine'] = {'type': 'command', 'command': expected}
+ with open(sf, 'w') as f:
+ json.dump(data, f, indent=2)
+ f.write('\n')
+" 2>/dev/null || true
+ fi
+fi
+
# Find squirrel entry by session_id (exact match) or fall back to most recent unsigned
SQUIRRELS_DIR="$WORLD_ROOT/.walnut/_squirrels"
ENTRY=""
@@ -59,8 +94,13 @@ if [ -n "$ENTRY" ] && [ -f "$ENTRY" ]; then
ENTRY_SESSION_ID=$(grep '^session_id:' "$ENTRY" | head -1 | sed 's/session_id: *//' || true)
WALNUT=$(grep '^walnut:' "$ENTRY" | head -1 | sed 's/walnut: *//' || true)
- # Extract stash content lines from YAML
- STASH=$(awk '/^stash:/{found=1; next} found && /^[a-z]/{found=0} found && /content:/{gsub(/.*content: *"?/,""); gsub(/"$/,""); print "- " $0}' "$ENTRY" 2>/dev/null || true)
+ # Only show stash if this entry was never saved (saves: 0) — saved stash items were already routed
+ SAVES=$(grep '^saves:' "$ENTRY" | head -1 | sed 's/saves: *//' | tr -d '[:space:]' || echo "0")
+ if [ "$SAVES" = "0" ]; then
+ STASH=$(awk '/^stash:/{found=1; next} found && /^[a-z]/{found=0} found && /content:/{gsub(/.*content: *"?/,""); gsub(/"$/,""); print "- " $0}' "$ENTRY" 2>/dev/null || true)
+ else
+ STASH=""
+ fi
if [ -z "${STASH:-}" ]; then
STASH="(empty)"
fi
diff --git a/plugins/walnut/rules/squirrels.md b/plugins/walnut/rules/squirrels.md
index 8179f5f..a5fd7ec 100644
--- a/plugins/walnut/rules/squirrels.md
+++ b/plugins/walnut/rules/squirrels.md
@@ -98,7 +98,7 @@ One clear explanation. Then move on. Don't over-explain. Don't patronise. Don't
Never create or overwrite a system file without reading its template first.
-Before writing to `.walnut/` → read the corresponding template from the plugin at `templates/alive/`.
+Before writing to `.walnut/` → read the corresponding template from the plugin at `templates/world/`.
Before writing to any walnut system file (_core/key.md, _core/now.md, _core/log.md, _core/insights.md, _core/tasks.md) → read the corresponding template from `templates/walnut/`.
Before creating a capsule companion → read `templates/capsule/companion.md`.
@@ -127,6 +127,26 @@ Same standard as "read before speaking" — extended to history. If you haven't
If the world key (`.walnut/key.md`, injected at session start) is out of sync with what you're seeing — a person not listed in `## Key People`, a stale connection, outdated integrations — flag it. Offer to fix inline or suggest `walnut:tidy`.
+### 10. Load on First Walnut Mention
+
+When a walnut is mentioned by name for the first time in a session and no walnut is currently loaded, invoke `walnut:load` for that walnut. Don't wait for an explicit "load X" — if the human says "what's happening with berties" or "let's check on expedia", that's a load trigger.
+
+If a walnut IS already loaded and a different one gets mentioned, don't auto-switch. Surface it as a cross-reference and let the human decide whether to load it.
+
+### 11. Trust the Context Window
+
+Do not panic about context usage. Do not suggest ending a session, starting a fresh session, or "wrapping up" based on how long the conversation has been running or how much context you think you've used.
+
+**Never say:**
+- "This session is getting long, let's start a fresh one"
+- "We should save before context runs out"
+- "This one's earned its rest"
+- Any variation of "let's wrap up" driven by token anxiety
+
+**Context compaction is not a crisis.** It's automatic, handled by the system, and the save infrastructure exists precisely for this. If context compacts, re-read the brief pack and keep working. Nothing is lost — `_core/log.md` and `_core/now.md` have everything the next session (or post-compaction continuation) needs.
+
+**When to suggest saving:** Only when the stash is heavy (5+ items) or a natural pause in the work arrives. Never because of context window pressure. The human decides when sessions end.
+
---
## Core Read Sequence (every session, non-negotiable)
@@ -206,7 +226,7 @@ Everything else waits for save: log entries, task updates, insights, _core/now.m
**now.md is only written by save.** Save regenerates it from scratch — full replacement, not patch. Each save produces a clean snapshot. If _core/now.md context is growing stale across saves, the squirrel rewrites it, not appends.
-**Save guard:** Saving means invoking `walnut:save`. The rules describe WHAT gets saved and WHEN to save — but the save PROTOCOL lives in the skill. If the stash is heavy, context is compacting, or a natural pause arrives, surface the need:
+**Save guard:** Saving means invoking `walnut:save`. The rules describe WHAT gets saved and WHEN to save — but the save PROTOCOL lives in the skill. If the stash is heavy (5+ items) or a natural pause in the work arrives, surface the need:
```
╭─ 🐿️ stash is getting heavy (N items)
@@ -406,7 +426,7 @@ Entries accumulate. They're tiny and scannable. Don't archive them.
## Unsigned Entry Recovery
-If `.walnut/_squirrels/` has an unsaved entry with stash items from a previous session:
+If `.walnut/_squirrels/` has an entry from a previous session with stash items AND `saves: 0` (genuinely unsaved), surface it. Entries with `saves: 1` or higher have already routed their stash — those items are historical records, not unfinished work. Skip them.
```
╭─ 🐿️ previous session had 6 stash items that were never saved.
diff --git a/plugins/walnut/skills/create/SKILL.md b/plugins/walnut/skills/create/SKILL.md
index 43d7aa8..48270e0 100644
--- a/plugins/walnut/skills/create/SKILL.md
+++ b/plugins/walnut/skills/create/SKILL.md
@@ -216,6 +216,7 @@ Follow the process from `world.md § Creating a New Walnut` exactly:
7. Write first log entry: "Walnut created. {goal}" — signed with session_id
8. If sub-walnut: set `parent: [[parent-name]]` in `_core/key.md` frontmatter
9. Add `[[new-walnut-name]]` to parent's `_core/key.md` `links:` frontmatter field
+10. Update `.walnut/key.md` — add the new walnut to `## Connections`: `- [[new-walnut-name]] — {goal}`. If `.walnut/_index.yaml` exists, regenerate it too.
```
╭─ 🐿️ scaffolding...
diff --git a/plugins/walnut/skills/extend/SKILL.md b/plugins/walnut/skills/extend/SKILL.md
index 5132d59..5f33c04 100644
--- a/plugins/walnut/skills/extend/SKILL.md
+++ b/plugins/walnut/skills/extend/SKILL.md
@@ -18,7 +18,7 @@ Not about adjusting preferences or voice (that's `walnut:tune`). Extend is about
| **Skill** | Repeatable workflow with instructions | `.walnut/skills/{skill-name}/SKILL.md` |
| **Rule** | Behavioral constraint or guide | `.walnut/rules/{rule-name}.md` |
| **Hook** | Automated trigger on system events | `.walnut/hooks/` (scripts + hooks.json) |
-| **Plugin** | Distributable package of skills + rules + hooks | Hands off to `contributor@alivecomputer` |
+| **Plugin** | Distributable package of skills + rules + hooks | Hands off to `contributor@stackwalnuts` |
---
@@ -178,13 +178,13 @@ When a custom skill is polished and battle-tested:
╰─
```
-**Contributor plugin handoff:** For marketplace packaging, PII stripping, testing, and publishing -> suggest installing `contributor@alivecomputer`. This is a SEPARATE plugin, not part of walnut core. The extend skill's job ends at building working custom capabilities. The contributor plugin handles everything from packaging to publishing.
+**Contributor plugin handoff:** For marketplace packaging, PII stripping, testing, and publishing -> suggest installing `contributor@stackwalnuts`. This is a SEPARATE plugin, not part of walnut core. The extend skill's job ends at building working custom capabilities. The contributor plugin handles everything from packaging to publishing.
```
╭─ 🐿️ to publish this skill:
│
│ 1. Install the contributor plugin:
-│ claude plugin install contributor@alivecomputer
+│ claude plugin install contributor@stackwalnuts
│
│ 2. Run: walnut:contribute {skill-name}
│ It handles: PII check, packaging, testing, submission
diff --git a/plugins/walnut/skills/history/SKILL.md b/plugins/walnut/skills/history/SKILL.md
index d28628c..9d7e69d 100644
--- a/plugins/walnut/skills/history/SKILL.md
+++ b/plugins/walnut/skills/history/SKILL.md
@@ -1,5 +1,5 @@
---
-description: "The human needs context from previous sessions — what happened, when, and why. Searches squirrel YAMLs, recent stash items, and recent logs. The light version of session recall — 'what happened recently?' Filters by walnut, topic, person, or timeframe. Can suggest escalation to walnut:mine when it finds unmined sessions with rich context."
+description: "Revive sessions (quick or heavy) to reconstruct full context from transcripts. The human needs context from previous sessions — what happened, when, and why. Searches squirrel YAMLs, recent stash items, and recent logs. The light version of session recall — 'what happened recently?' Filters by walnut, topic, person, or timeframe. Can suggest escalation to walnut:mine when it finds unmined sessions with rich context."
user-invocable: true
---
@@ -36,7 +36,7 @@ Show recent sessions across all walnuts.
│ System architecture, blueprint, 8 skills built, shipped v0.1-beta
│
│ 2. a44d04aa orbit-lab yesterday opus-4-6
-│ alivecomputer.com rebuilt, whitepaper v0.3, brand locked
+│ walnut.world rebuilt, whitepaper v0.3, brand locked
│
│ 3. 5551126e orbit-lab Feb 22 opus-4-6
│ Companion app, web installer, plugin v0.1-beta released
diff --git a/plugins/walnut/skills/load/SKILL.md b/plugins/walnut/skills/load/SKILL.md
index f53a774..0be972e 100644
--- a/plugins/walnut/skills/load/SKILL.md
+++ b/plugins/walnut/skills/load/SKILL.md
@@ -1,5 +1,5 @@
---
-description: "The human has chosen a walnut to focus on (prev. open). They're ready to work. Load the brief pack, resolve the people involved, check the active capsule — then surface one observation and ask what to work on. Context loads in tiers: walnut and people are automatic, capsule depth is offered."
+description: "The human mentions a walnut to work on, asks about a specific venture/experiment/project, or wants to check status — not just explicit 'load X'. Load the brief pack, resolve the people involved, check the active capsule — then surface one observation and ask what to work on. Context loads in tiers: walnut and people are automatic, capsule depth is offered."
user-invocable: true
---
diff --git a/plugins/walnut/skills/world/SKILL.md b/plugins/walnut/skills/world/SKILL.md
index ec62368..ea83bf2 100644
--- a/plugins/walnut/skills/world/SKILL.md
+++ b/plugins/walnut/skills/world/SKILL.md
@@ -13,13 +13,14 @@ NOT a database dump. NOT a flat list. A living view of their world, grouped by w
## Load Sequence
-1. Find the ALIVE world root (walk up from PWD looking for `01_Archive/` + `02_Life/`)
-2. Scan all `_core/key.md` files — extract type, goal, phase, health, rhythm, next, updated, people, links, parent. Check `_core/key.md` first, fall back to walnut root.
-3. Scan all `_core/now.md` files — extract health status, last updated, active capsule, next action. Check `_core/now.md` first, fall back to walnut root.
-4. Scan `_core/_capsules/*/companion.md` frontmatter per walnut — count capsules by status (draft, prototype, published, done). Fall back to `_core/_working/` + `_core/_references/` counts.
-5. Build the tree — parent/child relationships from `parent:` field in `_core/key.md`
-6. Compute attention items
-7. Surface API context if configured (Gmail, Slack, Calendar via preferences.yaml)
+1. **Read the injected ``** — it's already in your session context from the SessionStart hook. Contains every walnut's type, goal, phase, rhythm, updated, next, people, links, tags, capsules, and parent relationships. Zero file reads needed. If `` is not in context, fall back to reading `.walnut/_index.yaml` directly.
+2. **If no index exists at all** — generate it first (`python3 .walnut/scripts/generate-index.py "$WORLD_ROOT"`), then read the output. If the script doesn't exist either, fall back to manual scanning: use Glob to find all `*/_core/key.md` files across the World, read each one's frontmatter (type, goal, rhythm, people, links, parent), then read matching `_core/now.md` frontmatter (phase, updated, next, capsule). Dispatch these reads as parallel subagents to keep it fast. This fallback only happens on first-time setup before the index infrastructure exists.
+3. Build the tree from the index — parent/child relationships from `parent:` field
+4. **Lightweight fresh checks** — one Bash call each, no subagents, no Explore agents:
+ - **Unsigned squirrels with stash:** `cd .walnut/_squirrels && for f in *.yaml; do grep -q "saves: 0" "$f" && ! grep -q "stash: \[\]" "$f" && echo "$f"; done 2>/dev/null` — if any files are returned, read those specific YAMLs to surface the stash items. If nothing returned, skip.
+ - **Unrouted inputs:** `ls 03_Inputs/ 2>/dev/null | grep -v '^\.' | grep -v '^Icon'` — just the filenames, no deep reads.
+ - **API context:** only if configured in preferences.yaml (Gmail, Slack, Calendar via MCP).
+5. Compute attention items from fresh checks + index staleness signals
## State Detection
@@ -178,21 +179,15 @@ What's been happening across the world. A pulse check.
---
-## Index Freshness Check
+## Index Freshness
-After rendering the dashboard, check if `.walnut/_index.yaml` exists and is recent (modified within the last 7 days). If stale or missing:
+The index regenerates automatically after every save (post-write hook detects `_core/now.md` writes). If the index is missing or the human asks for a fresh view, regenerate on demand:
-```
-╭─ 🐿️ index stale
-│ .walnut/_index.yaml is 12 days old.
-│
-│ ▸ Regenerate?
-│ 1. Yes — run generate-index.py
-│ 2. Skip
-╰─
+```bash
+python3 .walnut/scripts/generate-index.py "$WORLD_ROOT"
```
-If yes → run the index generator to rebuild `_index.yaml` from all walnut frontmatter. The index is always derived, never manually maintained.
+After regenerating, re-read `.walnut/_index.yaml` to render the updated dashboard.
---
diff --git a/plugins/walnut/skills/world/setup.md b/plugins/walnut/skills/world/setup.md
index fe5a8d5..de58099 100644
--- a/plugins/walnut/skills/world/setup.md
+++ b/plugins/walnut/skills/world/setup.md
@@ -6,7 +6,7 @@ internal: true
# Setup — Three Paths to a World
-First time. No ALIVE folders exist. You just installed alive. Make it feel like something just came alive.
+First time. No `.walnut/` folder exists. You just installed Walnut. Make it feel like something just came alive.
All three paths produce the same result: a fully scaffolded ALIVE world with domain folders, `.walnut/` config, and at least one walnut. The only difference is how we collect the information.
@@ -400,7 +400,7 @@ Show:
#### Step 2: World identity — .walnut/key.md
-Read the template from the plugin: `templates/alive/key.md`
+Read the template from the plugin: `templates/world/key.md`
Replace template variables:
- `{{name}}` → world name
@@ -434,7 +434,7 @@ Show:
#### Step 3: Preferences — .walnut/preferences.yaml
-Read the template from the plugin: `templates/alive/preferences.yaml`
+Read the template from the plugin: `templates/world/preferences.yaml`
If preferences were provided (Path A only), uncomment the relevant lines and set values.
@@ -460,7 +460,7 @@ Show:
#### Step 4: Overrides — .walnut/overrides.md
-Read the template from the plugin: `templates/alive/overrides.md`
+Read the template from the plugin: `templates/world/overrides.md`
Write as-is. No variable replacement needed.
diff --git a/plugins/walnut/statusline/walnut-statusline.sh b/plugins/walnut/statusline/walnut-statusline.sh
index e788c9a..e99c0ba 100755
--- a/plugins/walnut/statusline/walnut-statusline.sh
+++ b/plugins/walnut/statusline/walnut-statusline.sh
@@ -9,8 +9,9 @@ PARSED=$(echo "$INPUT" | python3 -c "
import sys,json
d=json.load(sys.stdin)
print(d.get('session_id',''))
-c=d.get('cost',{})
-print(f\"\${c.get('total_cost_usd',0):.2f}\")
+c=d.get('cost') or {}
+v=c.get('total_cost_usd') or 0
+print(f'\${v:.2f}')
cw=d.get('context_window',{})
p=cw.get('used_percentage')
print(f'{p:.0f}' if p is not None else '?')
@@ -54,6 +55,11 @@ while [ "$DIR" != "/" ]; do
DIR="$(dirname "$DIR")"
done
+# Write context % to file for context-watch threshold injection
+if [ -n "$WORLD_ROOT" ] && [ "$CTX_PCT" != "?" ]; then
+ echo "$CTX_PCT" > "$WORLD_ROOT/.walnut/.context_pct" 2>/dev/null || true
+fi
+
# ── DEGRADED STATES ──
if [ -z "$WORLD_ROOT" ]; then
diff --git a/plugins/walnut/templates/alive/agents.md b/plugins/walnut/templates/world/agents.md
similarity index 100%
rename from plugins/walnut/templates/alive/agents.md
rename to plugins/walnut/templates/world/agents.md
diff --git a/plugins/walnut/templates/alive/key.md b/plugins/walnut/templates/world/key.md
similarity index 96%
rename from plugins/walnut/templates/alive/key.md
rename to plugins/walnut/templates/world/key.md
index 09d232a..fa1c75e 100644
--- a/plugins/walnut/templates/alive/key.md
+++ b/plugins/walnut/templates/world/key.md
@@ -30,7 +30,7 @@ links: []
- **Slack** — via MCP server. Posts to channels, reads history.
- **Notion** — via MCP server. Task management, knowledge base.
- **Otter** — transcript sync script at ~/scripts/otter-sync.sh
- - **GitHub** — via gh CLI. Repos: alivecomputer/walnut
+ - **GitHub** — via gh CLI. Repos: stackwalnuts/walnut
-->
## Key People
diff --git a/plugins/walnut/templates/alive/overrides.md b/plugins/walnut/templates/world/overrides.md
similarity index 100%
rename from plugins/walnut/templates/alive/overrides.md
rename to plugins/walnut/templates/world/overrides.md
diff --git a/plugins/walnut/templates/alive/preferences.yaml b/plugins/walnut/templates/world/preferences.yaml
similarity index 100%
rename from plugins/walnut/templates/alive/preferences.yaml
rename to plugins/walnut/templates/world/preferences.yaml
diff --git a/src/index.ts b/src/index.ts
index 4b1cf11..84b28f3 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -263,7 +263,7 @@ server.tool(
content: [
{
type: "text" as const,
- text: `Captured to ${walnut}/_capsules/${capsule}/raw/${filename}`,
+ text: `Captured to ${walnut}/_core/_capsules/${capsule}/raw/${filename}`,
},
],
};
@@ -340,7 +340,7 @@ server.tool(
// Create directory structure
const coreDir = join(walnutDir, "_core");
- const capsulesDir = join(walnutDir, "_capsules");
+ const capsulesDir = join(coreDir, "_capsules");
mkdirSync(coreDir, { recursive: true });
mkdirSync(capsulesDir, { recursive: true });
diff --git a/src/setup.ts b/src/setup.ts
index 3e745c5..dbc8435 100644
--- a/src/setup.ts
+++ b/src/setup.ts
@@ -117,7 +117,7 @@ export async function runSetup() {
for (const d of domains) {
mkdirSync(join(worldPath, d), { recursive: true });
}
- mkdirSync(join(worldPath, ".alive"), { recursive: true });
+ mkdirSync(join(worldPath, ".walnut"), { recursive: true });
mkdirSync(join(worldPath, "02_Life", "people"), { recursive: true });
configured.push("World scaffolded at ~/world/");
} else {
diff --git a/src/world/scanner.ts b/src/world/scanner.ts
index 26ab49a..282b382 100644
--- a/src/world/scanner.ts
+++ b/src/world/scanner.ts
@@ -181,7 +181,7 @@ function walkForWalnuts(dir: string, results: FoundKeyMd[]): void {
});
}
- // Recurse into subdirectories, but skip _core/, _capsules/, _squirrels/, .alive/
+ // Recurse into subdirectories, but skip _core/, _capsules/, _squirrels/, .walnut/
for (const entry of entries) {
if (entry.startsWith(".") || entry === "_core" || entry === "_capsules" || entry === "_squirrels") {
continue;