From 5644ae01bd7255a33293412a06fa4f49438b2823 Mon Sep 17 00:00:00 2001 From: Grivn Date: Fri, 3 Jul 2026 12:03:46 +0000 Subject: [PATCH] fix(setup): keep Claude memory subagents as leaf writers Clarify the Claude memory instructions so the main conversation still delegates memory writes, but the delegated memory subagent runs mnemon remember/link directly instead of spawning another Task agent. The Stop hook now gives SubagentStop contexts leaf-writer guidance, and setup tests lock the guardrail wording across the Claude guide, skill, and hook assets. Validation: go test ./internal/setup; go build -o mnemon . --- internal/setup/assets/claude/SKILL.md | 3 +- internal/setup/assets/claude/guide.md | 7 ++-- internal/setup/assets/claude/stop.sh | 5 +++ internal/setup/claude_test.go | 48 +++++++++++++++++++++++++++ 4 files changed, 59 insertions(+), 4 deletions(-) diff --git a/internal/setup/assets/claude/SKILL.md b/internal/setup/assets/claude/SKILL.md index 0896600f..7395758e 100644 --- a/internal/setup/assets/claude/SKILL.md +++ b/internal/setup/assets/claude/SKILL.md @@ -50,7 +50,8 @@ Check the output `errors` field because imports can partially succeed. ## Guardrails -- Never run `remember` or `link` in the main conversation — always delegate to a sub-agent. +- Main conversation: never run `remember` or `link` directly; delegate to a Task sub-agent. +- Memory sub-agent: you are the leaf writer. Run `mnemon remember` and any justified `mnemon link` commands directly; do not spawn another sub-agent. - Do not store secrets, passwords, or tokens. - Categories: `preference` · `decision` · `insight` · `fact` · `context` - Edge types: `temporal` · `semantic` · `causal` · `entity` diff --git a/internal/setup/assets/claude/guide.md b/internal/setup/assets/claude/guide.md index a626f4fd..841f8d2e 100644 --- a/internal/setup/assets/claude/guide.md +++ b/internal/setup/assets/claude/guide.md @@ -53,9 +53,10 @@ Aim for a rough distribution: ~20% at 4-5, ~50% at 2-3, ~30% at 1. Avoid defaulting everything to 4-5 — that defeats the scoring system. **What to store**: both conclusions AND context. Prefer storing a little too much over missing something useful. -**How to store**: delegate to a Task sub-agent (`subagent_type="general-purpose"`, `model="sonnet"`). +**How to store from the main conversation**: delegate to a Task sub-agent (`subagent_type="general-purpose"`, `model="sonnet"`). Only provide what to store — content, category, importance, entities, and create/update intent. -The sub-agent will read the mnemon skill and execute the correct commands itself. +The sub-agent is the leaf memory writer: it will read the mnemon skill and execute `mnemon remember` / `mnemon link` itself. Do NOT: write CLI commands or workflow steps in the sub-agent prompt (the sub-agent has access to the skill docs and will use the correct flags). -Do NOT run memory writes in the main conversation, or remember operational/public/git-tracked/transient info. \ No newline at end of file +Do NOT run memory writes in the main conversation, or remember operational/public/git-tracked/transient info. +Do NOT ask the memory sub-agent to delegate again; nested memory sub-agents waste tokens and can fail in Claude Code. diff --git a/internal/setup/assets/claude/stop.sh b/internal/setup/assets/claude/stop.sh index 830d89d2..0c9266e1 100644 --- a/internal/setup/assets/claude/stop.sh +++ b/internal/setup/assets/claude/stop.sh @@ -12,4 +12,9 @@ if echo "$MSG" | grep -qiE "mnemon remember|sub-agent.*remember|Stored.*imp="; t exit 0 fi +if echo "$INPUT" | grep -qE '"hook_event_name"[[:space:]]*:[[:space:]]*"SubagentStop"|"agent_id"[[:space:]]*:'; then + echo "[mnemon] If this sub-agent was delegated memory work, complete it directly with mnemon remember/link; do not spawn another sub-agent. If no durable memory is needed, say so and finish." + exit 0 +fi + echo "[mnemon] Consider: does this exchange warrant a remember sub-agent?" diff --git a/internal/setup/claude_test.go b/internal/setup/claude_test.go index b3f2c0a6..d5037aa7 100644 --- a/internal/setup/claude_test.go +++ b/internal/setup/claude_test.go @@ -3,7 +3,10 @@ package setup import ( "os" "path/filepath" + "strings" "testing" + + "github.com/mnemon-dev/mnemon/internal/setup/assets" ) func TestPromptDirHonorsMnemonDataDir(t *testing.T) { @@ -61,6 +64,51 @@ func TestWritePromptFilesWritesUnderMnemonDataDir(t *testing.T) { } } +func TestClaudeMemoryInstructionsKeepSubagentsLeafWriters(t *testing.T) { + tests := []struct { + name string + body string + want []string + }{ + { + name: "guide", + body: string(assets.ClaudeGuide), + want: []string{ + "The sub-agent is the leaf memory writer", + "Do NOT ask the memory sub-agent to delegate again", + }, + }, + { + name: "skill", + body: string(assets.ClaudeSkill), + want: []string{ + "Main conversation: never run `remember` or `link` directly", + "Memory sub-agent: you are the leaf writer", + "do not spawn another sub-agent", + }, + }, + { + name: "stop hook", + body: string(assets.ClaudeStopHook), + want: []string{ + `"hook_event_name"[[:space:]]*:[[:space:]]*"SubagentStop"`, + "complete it directly with mnemon remember/link", + "do not spawn another sub-agent", + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + for _, want := range tt.want { + if !strings.Contains(tt.body, want) { + t.Fatalf("%s is missing %q", tt.name, want) + } + } + }) + } +} + func TestCollidesWithUserConfigHomeInstall(t *testing.T) { home := t.TempDir() t.Setenv("HOME", home)