From 49b80e4d8b92a2ae04d5ba4f06309708ec2c1ed7 Mon Sep 17 00:00:00 2001 From: willsupernormal Date: Fri, 27 Mar 2026 10:27:05 +1100 Subject: [PATCH 01/19] =?UTF-8?q?fix:=20critical=20runtime=20bugs=20?= =?UTF-8?q?=E2=80=94=20setup=20scaffolds=20.walnut/,=20capsules=20path,=20?= =?UTF-8?q?index=20injection?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - setup.ts: scaffold .walnut/ not .alive/ for new users - index.ts: walnut_create puts _capsules/ inside _core/ not walnut root - index.ts: walnut_capture success message includes _core/ prefix - session-new.sh: read world index from .walnut/_index.yaml not .alive/ Co-Authored-By: Claude Opus 4.6 (1M context) --- plugins/walnut/hooks/scripts/walnut-session-new.sh | 4 ++-- src/index.ts | 4 ++-- src/setup.ts | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/plugins/walnut/hooks/scripts/walnut-session-new.sh b/plugins/walnut/hooks/scripts/walnut-session-new.sh index 8f0695a..e06b2c1 100755 --- a/plugins/walnut/hooks/scripts/walnut-session-new.sh +++ b/plugins/walnut/hooks/scripts/walnut-session-new.sh @@ -143,9 +143,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/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 { From f744cb3f40a298231853ef5d6b5ce3bab0d9a32a Mon Sep 17 00:00:00 2001 From: willsupernormal Date: Fri, 27 Mar 2026 10:27:56 +1100 Subject: [PATCH 02/19] fix: log guardian recognises MCP signatures, rules guardian uses correct cache path - log-guardian: match both squirrel: and walnut-mcp: signature formats - rules-guardian: cache path updated from alivecomputer to stackwalnuts Co-Authored-By: Claude Opus 4.6 (1M context) --- plugins/walnut/hooks/scripts/walnut-log-guardian.sh | 2 +- plugins/walnut/hooks/scripts/walnut-rules-guardian.sh | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) 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-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 ;; From e1f637046132ccc472496df9fac5ebaf3db9fde1 Mon Sep 17 00:00:00 2001 From: willsupernormal Date: Fri, 27 Mar 2026 10:31:51 +1100 Subject: [PATCH 03/19] =?UTF-8?q?fix:=20deploy.sh=20=E2=80=94=20correct=20?= =?UTF-8?q?paths,=20deploy=20to=20both=20cache=20and=20marketplace?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Cache path: stackwalnuts/walnut/1.0.0 - Marketplace path: stackwalnuts/plugins/walnut - Gracefully skip missing directories instead of erroring Co-Authored-By: Claude Opus 4.6 (1M context) --- deploy.sh | 49 +++++++++++++++++++++++++++++++++---------------- 1 file changed, 33 insertions(+), 16 deletions(-) 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')" From 36982d6d1fbe3e893b134c545ebcfcbcace0180a Mon Sep 17 00:00:00 2001 From: willsupernormal Date: Fri, 27 Mar 2026 10:32:47 +1100 Subject: [PATCH 04/19] =?UTF-8?q?fix:=20GitHub=20configs=20=E2=80=94=20URL?= =?UTF-8?q?s,=20skill=20names,=20branding=20updated=20to=20walnut/stackwal?= =?UTF-8?q?nuts?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Issue template: discussions URL → stackwalnuts/walnut - Feature request: alive:save → walnut:save - Funding: alivecomputer.com → walnut.world - PR template: ALIVE session → walnut session Co-Authored-By: Claude Opus 4.6 (1M context) --- .github/FUNDING.yml | 2 +- .github/ISSUE_TEMPLATE/config.yml | 2 +- .github/ISSUE_TEMPLATE/feature_request.md | 2 +- .github/PULL_REQUEST_TEMPLATE.md | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) 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 ``` From 1b354ac689a110deb7de48135be8923dd44bcdfc Mon Sep 17 00:00:00 2001 From: willsupernormal Date: Fri, 27 Mar 2026 10:33:51 +1100 Subject: [PATCH 05/19] =?UTF-8?q?fix:=20branding=20sweep=20=E2=80=94=20CLA?= =?UTF-8?q?UDE.md=20heading,=20skill=20refs,=20demo=20output=20updated=20t?= =?UTF-8?q?o=20walnut?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - CLAUDE.md: # ALIVE → # Walnut - setup.md: "installed alive" → "installed Walnut" - extend skill: contributor@alivecomputer → contributor@stackwalnuts (3 places) - history skill: alivecomputer.com → walnut.world in demo output - Hermes skill: "Load ALIVE walnut" → "Load walnut" in description Co-Authored-By: Claude Opus 4.6 (1M context) --- plugins/walnut/CLAUDE.md | 2 +- plugins/walnut/skills/extend/SKILL.md | 6 +++--- plugins/walnut/skills/history/SKILL.md | 2 +- plugins/walnut/skills/world/setup.md | 2 +- skills/walnuts/SKILL.md | 2 +- 5 files changed, 7 insertions(+), 7 deletions(-) diff --git a/plugins/walnut/CLAUDE.md b/plugins/walnut/CLAUDE.md index ac9c7e0..c6632a4 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** 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..2e5c2ec 100644 --- a/plugins/walnut/skills/history/SKILL.md +++ b/plugins/walnut/skills/history/SKILL.md @@ -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/world/setup.md b/plugins/walnut/skills/world/setup.md index fe5a8d5..04f2d49 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 ALIVE folders exist. 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. diff --git a/skills/walnuts/SKILL.md b/skills/walnuts/SKILL.md index d21f1f2..e192f87 100644 --- a/skills/walnuts/SKILL.md +++ b/skills/walnuts/SKILL.md @@ -1,6 +1,6 @@ --- name: walnuts -description: Load ALIVE walnut context — structured project memory across sessions. Read priorities, save decisions, manage tasks across your world. +description: Load walnut context — structured project memory across sessions. Read priorities, save decisions, manage tasks across your world. version: 1.0.0 platforms: [macos, linux] metadata: From 821981156460021e5c4ae42beefe33ef35b52cd7 Mon Sep 17 00:00:00 2001 From: willsupernormal Date: Fri, 27 Mar 2026 10:35:09 +1100 Subject: [PATCH 06/19] =?UTF-8?q?fix:=20stale=20comment=20in=20scanner.ts?= =?UTF-8?q?=20=E2=80=94=20.alive/=20reference?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.6 (1M context) --- src/world/scanner.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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; From 89c5aebce9de95b1c35e0c4ed6db5bf5cbd0d3aa Mon Sep 17 00:00:00 2001 From: willsupernormal Date: Fri, 27 Mar 2026 10:36:29 +1100 Subject: [PATCH 07/19] =?UTF-8?q?revert:=20keep=20"ALIVE=20walnut=20contex?= =?UTF-8?q?t"=20in=20Hermes=20skill=20=E2=80=94=20ALIVE=20is=20the=20frame?= =?UTF-8?q?work=20name?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.6 (1M context) --- skills/walnuts/SKILL.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/skills/walnuts/SKILL.md b/skills/walnuts/SKILL.md index e192f87..d21f1f2 100644 --- a/skills/walnuts/SKILL.md +++ b/skills/walnuts/SKILL.md @@ -1,6 +1,6 @@ --- name: walnuts -description: Load walnut context — structured project memory across sessions. Read priorities, save decisions, manage tasks across your world. +description: Load ALIVE walnut context — structured project memory across sessions. Read priorities, save decisions, manage tasks across your world. version: 1.0.0 platforms: [macos, linux] metadata: From f4d74609af33a4a0886efa9bc1c68b05c0d68535 Mon Sep 17 00:00:00 2001 From: willsupernormal Date: Fri, 27 Mar 2026 10:47:25 +1100 Subject: [PATCH 08/19] =?UTF-8?q?refactor:=20rename=20templates/alive/=20t?= =?UTF-8?q?o=20templates/world/=20=E2=80=94=20world-level=20config=20templ?= =?UTF-8?q?ates?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - git mv templates/alive → templates/world - squirrels.md: template path reference updated - setup.md: 3 template path references updated - world/key.md: example repo URL → stackwalnuts/walnut Co-Authored-By: Claude Opus 4.6 (1M context) --- plugins/walnut/rules/squirrels.md | 2 +- plugins/walnut/skills/world/setup.md | 6 +++--- plugins/walnut/templates/alive/key.md | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/plugins/walnut/rules/squirrels.md b/plugins/walnut/rules/squirrels.md index 8179f5f..0a097c4 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`. diff --git a/plugins/walnut/skills/world/setup.md b/plugins/walnut/skills/world/setup.md index 04f2d49..91ca645 100644 --- a/plugins/walnut/skills/world/setup.md +++ b/plugins/walnut/skills/world/setup.md @@ -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/templates/alive/key.md b/plugins/walnut/templates/alive/key.md index 09d232a..fa1c75e 100644 --- a/plugins/walnut/templates/alive/key.md +++ b/plugins/walnut/templates/alive/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 From 1739dfad47d1bfd0c0c74820d26fe80b69086fb7 Mon Sep 17 00:00:00 2001 From: willsupernormal Date: Fri, 27 Mar 2026 12:40:46 +1100 Subject: [PATCH 09/19] fix: CI workflow validates plugins/walnut/ not plugins/alive/ MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - PLUGIN_DIR: plugins/alive → plugins/walnut - Label: ALIVE → Walnut - Template dirs: alive → world (matches directory rename) Co-Authored-By: Claude Opus 4.6 (1M context) --- .github/workflows/validate-plugin.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) 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 From 7937d1b9854cca265c7ea50da6eb70ca6e03d0ef Mon Sep 17 00:00:00 2001 From: willsupernormal Date: Fri, 27 Mar 2026 14:07:33 +1100 Subject: [PATCH 10/19] refactor: rename templates/alive/ directory to templates/world/ MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The cherry-pick captured the reference updates but not the directory rename. This commit completes the rename: templates/alive/ → templates/world/. Co-Authored-By: Claude Opus 4.6 (1M context) --- plugins/walnut/templates/{alive => world}/agents.md | 0 plugins/walnut/templates/{alive => world}/key.md | 0 plugins/walnut/templates/{alive => world}/overrides.md | 0 plugins/walnut/templates/{alive => world}/preferences.yaml | 0 4 files changed, 0 insertions(+), 0 deletions(-) rename plugins/walnut/templates/{alive => world}/agents.md (100%) rename plugins/walnut/templates/{alive => world}/key.md (100%) rename plugins/walnut/templates/{alive => world}/overrides.md (100%) rename plugins/walnut/templates/{alive => world}/preferences.yaml (100%) 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 100% rename from plugins/walnut/templates/alive/key.md rename to plugins/walnut/templates/world/key.md 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 From 0d1e7475622a0b4d1ad3e19f3decee2d37b5ac4c Mon Sep 17 00:00:00 2001 From: willsupernormal Date: Fri, 27 Mar 2026 15:43:23 +1100 Subject: [PATCH 11/19] =?UTF-8?q?fix:=20archive=20enforcer=20=E2=80=94=20P?= =?UTF-8?q?OSIX=20sed=20replacement,=20rename-not-deny,=20multi-target?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Replace broken macOS sed with python3 shlex for path extraction - Rename files to "(Marked for Deletion)" instead of hard deny - Process ALL targets, not just first match - Distinct messages for renamed vs not-found targets - Opens Finder at target directory for manual review Ported from will/statusline-and-stash-fixes (commits 2211200, 6df7bc8, e65a7e9) Co-Authored-By: Claude Opus 4.6 (1M context) --- .../hooks/scripts/walnut-archive-enforcer.sh | 53 +++++++++++++++++-- 1 file changed, 48 insertions(+), 5 deletions(-) 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 From 48b4367d18f516d71d4931afce1c7f371e7571cb Mon Sep 17 00:00:00 2001 From: willsupernormal Date: Fri, 27 Mar 2026 15:44:56 +1100 Subject: [PATCH 12/19] =?UTF-8?q?fix:=20context-watch=20=E2=80=94=20add=20?= =?UTF-8?q?threshold=20injection,=20stash=20cross-pollination,=20self-save?= =?UTF-8?q?=20fix?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add context % re-injection at 20/40/60/80% thresholds - 40% and below: condensed refresh (core behaviours only) - 60%+: full refresh with world key and index - Add active squirrel stash cross-pollination (saves:0 gate) - Fix self-save false alert: check both full UUID and 8-char prefix - Highest-first threshold firing (80→60→40→20) with mark-all-below Ported from will/statusline-and-stash-fixes (commits 2211200, d3cf116) Co-Authored-By: Claude Opus 4.6 (1M context) --- .../hooks/scripts/walnut-context-watch.sh | 130 +++++++++++++++++- 1 file changed, 126 insertions(+), 4 deletions(-) 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 From 677973f5aad1f789e19ee0b7927f3be5a9ce24e7 Mon Sep 17 00:00:00 2001 From: willsupernormal Date: Fri, 27 Mar 2026 15:46:01 +1100 Subject: [PATCH 13/19] =?UTF-8?q?fix:=20session-new=20=E2=80=94=20inject?= =?UTF-8?q?=20statusline=20into=20settings.json=20with=20absolute=20path?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Creates settings.json with statusLine if missing - Self-heals existing settings.json: checks if statusLine.command matches expected absolute path, corrects if not - Covers: fresh install, missing statusLine key, stale relative path Ported from will/statusline-and-stash-fixes (commits 2211200, 6df7bc8, b9c27d8) Co-Authored-By: Claude Opus 4.6 (1M context) --- .../hooks/scripts/walnut-session-new.sh | 37 +++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/plugins/walnut/hooks/scripts/walnut-session-new.sh b/plugins/walnut/hooks/scripts/walnut-session-new.sh index e06b2c1..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" From 146ca1c0d93f6ae83770172f73bd2c27a3e8f316 Mon Sep 17 00:00:00 2001 From: willsupernormal Date: Fri, 27 Mar 2026 15:47:19 +1100 Subject: [PATCH 14/19] =?UTF-8?q?fix:=20session-resume=20+=20session-compa?= =?UTF-8?q?ct=20=E2=80=94=20saves:0=20stash=20gate=20+=20statusline=20self?= =?UTF-8?q?-heal?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Resume: add statusline settings.json self-heal (absolute path, same as session-new) - Resume + Compact: gate stash extraction on saves: 0 — entries with saves > 0 have already routed their stash, so those items are historical records, not unfinished work. Prevents false "previous session had stash" messages. Ported from will/statusline-and-stash-fixes (commits 6df7bc8, b9c27d8) Co-Authored-By: Claude Opus 4.6 (1M context) --- .../hooks/scripts/walnut-session-compact.sh | 8 +++- .../hooks/scripts/walnut-session-resume.sh | 44 ++++++++++++++++++- 2 files changed, 49 insertions(+), 3 deletions(-) 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-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 From ddcdf4cb63e1bd1aee28e2449b9517295fe8555b Mon Sep 17 00:00:00 2001 From: willsupernormal Date: Fri, 27 Mar 2026 15:48:25 +1100 Subject: [PATCH 15/19] =?UTF-8?q?fix:=20statusline=20=E2=80=94=20null=20co?= =?UTF-8?q?st=20crash=20guard=20+=20context=5Fpct=20file=20write?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Guard against cost: null from Claude Code (was crashing on None.get()) - Write context % to .walnut/.context_pct for context-watch threshold injection Ported from will/statusline-and-stash-fixes (commits 2211200, d3cf116) Co-Authored-By: Claude Opus 4.6 (1M context) --- plugins/walnut/statusline/walnut-statusline.sh | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) 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 From 7625c0261069a47ede59ecf5680d512da23e0312 Mon Sep 17 00:00:00 2001 From: willsupernormal Date: Fri, 27 Mar 2026 15:54:06 +1100 Subject: [PATCH 16/19] =?UTF-8?q?fix:=20squirrels=20rule=20=E2=80=94=20add?= =?UTF-8?q?=20instincts=20#10=20and=20#11,=20fix=20save=20guard,=20saves:0?= =?UTF-8?q?=20recovery?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Instinct #10: Load on First Walnut Mention — auto-load when walnut mentioned by name, cross-reference if already loaded - Instinct #11: Trust the Context Window — never suggest ending session based on token anxiety, compaction is not a crisis (made model-agnostic: removed "1M tokens" claim from original) - Save guard: removed "context is compacting" as a save trigger - Unsigned entry recovery: requires saves: 0 — entries with saves > 0 have already routed their stash Ported from will/skill-routing-improvements (164402c) and will/statusline-and-stash-fixes (d75f88c, b9c27d8) Co-Authored-By: Claude Opus 4.6 (1M context) --- plugins/walnut/rules/squirrels.md | 24 ++++++++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/plugins/walnut/rules/squirrels.md b/plugins/walnut/rules/squirrels.md index 0a097c4..a5fd7ec 100644 --- a/plugins/walnut/rules/squirrels.md +++ b/plugins/walnut/rules/squirrels.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. From 42e3ae5c12315575a9db7607e9af77b77102c7fe Mon Sep 17 00:00:00 2001 From: willsupernormal Date: Fri, 27 Mar 2026 15:57:18 +1100 Subject: [PATCH 17/19] fix: world skill reads injected index, post-write auto-regenerates, create updates key.md MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - World skill: read from session context first (zero file reads), fall back to .walnut/_index.yaml, then generate-index.py, then manual scan - World skill: lightweight fresh checks via single Bash commands - World skill: index freshness simplified — auto-regeneration by post-write hook - Post-write hook: detect _core/now.md writes, regenerate index in background with 5-min debounce - Create skill: Step 10 — update .walnut/key.md Connections when scaffolding Ported from will/world-skill-index-and-post-save-hook (9aaf2dc, 5b2397c) Co-Authored-By: Claude Opus 4.6 (1M context) --- .../walnut/hooks/scripts/walnut-post-write.sh | 26 +++++++++++++++- plugins/walnut/skills/create/SKILL.md | 1 + plugins/walnut/skills/world/SKILL.md | 31 ++++++++----------- 3 files changed, 39 insertions(+), 19 deletions(-) diff --git a/plugins/walnut/hooks/scripts/walnut-post-write.sh b/plugins/walnut/hooks/scripts/walnut-post-write.sh index f9b5196..5e1f42c 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,26 @@ 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 + AGE=$(( $(date +%s) - $(stat -f%m "$MARKER" 2>/dev/null || echo "0") )) + [ "$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/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/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. --- From 27cb6cfb9e895929124b0a607038dafd5b8572f9 Mon Sep 17 00:00:00 2001 From: willsupernormal Date: Fri, 27 Mar 2026 16:18:53 +1100 Subject: [PATCH 18/19] =?UTF-8?q?fix:=20skill=20descriptions=20+=20CLAUDE.?= =?UTF-8?q?md=20=E2=80=94=20routing,=20capsule=20skill,=20numbering?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - load skill: broaden description to match natural walnut mentions - history skill: add "revive" to description for heavy revive routing - CLAUDE.md: add /walnut:capsule to skill list (Thirteen Skills) - CLAUDE.md: fix read sequence numbering (7→9 → 7→8) - hooks.json: description count 13→14 Ported from will/skill-routing-improvements (164402c) and 6df7bc8 Co-Authored-By: Claude Opus 4.6 (1M context) --- plugins/walnut/CLAUDE.md | 5 +++-- plugins/walnut/hooks/hooks.json | 2 +- plugins/walnut/skills/history/SKILL.md | 2 +- plugins/walnut/skills/load/SKILL.md | 2 +- 4 files changed, 6 insertions(+), 5 deletions(-) diff --git a/plugins/walnut/CLAUDE.md b/plugins/walnut/CLAUDE.md index c6632a4..68b3471 100644 --- a/plugins/walnut/CLAUDE.md +++ b/plugins/walnut/CLAUDE.md @@ -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/skills/history/SKILL.md b/plugins/walnut/skills/history/SKILL.md index 2e5c2ec..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 --- 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 --- From e3e4570358aa0d8f5600d7a49d3c6e91b39a2f8a Mon Sep 17 00:00:00 2001 From: willsupernormal Date: Fri, 27 Mar 2026 16:42:40 +1100 Subject: [PATCH 19/19] fix: cross-platform stat in post-write debounce + setup.md wording MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Post-write hook: use GNU/BSD stat sniff (matching context-watch pattern) instead of macOS-only stat -f%m. Prevents debounce failure on Linux. - Setup.md: "No ALIVE folders" → "No .walnut/ folder" to match runtime. Co-Authored-By: Claude Opus 4.6 (1M context) --- plugins/walnut/hooks/scripts/walnut-post-write.sh | 7 ++++++- plugins/walnut/skills/world/setup.md | 2 +- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/plugins/walnut/hooks/scripts/walnut-post-write.sh b/plugins/walnut/hooks/scripts/walnut-post-write.sh index 5e1f42c..db4a203 100644 --- a/plugins/walnut/hooks/scripts/walnut-post-write.sh +++ b/plugins/walnut/hooks/scripts/walnut-post-write.sh @@ -33,7 +33,12 @@ case "$FILE_PATH" in # Debounce — skip if we regenerated in the last 5 minutes MARKER="/tmp/walnut-index-regen" if [ -f "$MARKER" ]; then - AGE=$(( $(date +%s) - $(stat -f%m "$MARKER" 2>/dev/null || echo "0") )) + 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" diff --git a/plugins/walnut/skills/world/setup.md b/plugins/walnut/skills/world/setup.md index 91ca645..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 Walnut. 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.