- README.MD: add original-source-code and nano-claude-code sections, update overview table (4 subprojects), add v3.0 news entry, expand comparison table with memory/multi-agent/skills dimensions - nano-claude-code v3.0: multi-agent package (multi_agent/), memory package (memory/), skill package (skill/) with built-in /commit and /review skills, context compression (compaction.py), tool registry plugin system, diff view, 17 slash commands, 18 built-in tools, 101 tests (~5000 lines total) - original-source-code/src: add raw TypeScript source tree (1884 files) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
18 KiB
Nano Claude Code — Update Notes
This document describes three major feature additions to nano-claude-code: Multi-Agent, Memory, and Skill. Each feature is organized as a self-contained Python package, follows the same architectural pattern, and includes a backward-compatibility shim so existing code continues to work.
Architecture Overview
All three packages follow the same pattern:
feature/
__init__.py — public re-exports
<core>.py — data model, loading, business logic
tools.py — registers tools into the central tool_registry
...
feature.py — backward-compat shim (re-exports from feature/)
The tool registry (tool_registry.py) is the central hub. Each feature's
tools.py calls register_tool(ToolDef(...)) at import time. The top-level
tools.py imports all three feature tool modules, triggering auto-registration.
The agent loop (agent.py) injects _depth and _system_prompt into the
config dict on every call, so tool functions can read them via config.get(...).
1. Multi-Agent (multi_agent/)
What it does
Allows Claude to spawn sub-agents — nested agent loops that run concurrently in background threads. Sub-agents can share the parent's context or run in an isolated git worktree. The user can send follow-up messages to named background agents and retrieve their results.
Package structure
multi_agent/
__init__.py — re-exports AgentDefinition, SubAgentTask, SubAgentManager, etc.
subagent.py — core: AgentDefinition, SubAgentTask, SubAgentManager, worktree helpers
tools.py — registers: Agent, SendMessage, CheckAgentResult, ListAgentTasks, ListAgentTypes
subagent.py — backward-compat shim
Key classes and functions
AgentDefinition (multi_agent/subagent.py)
@dataclass
class AgentDefinition:
name: str
description: str
system_prompt: str # prepended to base prompt for this agent type
model: str # "" = inherit from parent
tools: list # [] = all tools
source: str # "built-in" | "user" | "project"
Built-in agent types: general-purpose, coder, reviewer, researcher, tester
Custom agent definitions — place a .md file with YAML frontmatter in:
~/.nano_claude/agents/<name>.md(user-level).nano_claude/agents/<name>.md(project-level, takes priority)
Frontmatter format:
---
name: my-agent
description: What this agent does
model: claude-opus-4-6
tools: [Read, Glob, Grep]
---
Extra system prompt instructions for this agent.
SubAgentManager (multi_agent/subagent.py)
spawn(prompt, config, agent_def, isolation, name, wait)— runs agent in thread poolsend_message(task_id_or_name, message)— enqueues message to a running background agentget_result(task_id)— returns final text or statuslist_tasks()— returns all SubAgentTask objects
Git worktree isolation:
When isolation="worktree" is passed to Agent, a temporary git worktree is
created on a fresh branch. The sub-agent works in isolation; if it makes no
changes the worktree is cleaned up automatically.
Tools registered
| Tool | Description |
|---|---|
Agent |
Spawn a sub-agent (sync or background with wait=false) |
SendMessage |
Send a follow-up message to a named background agent |
CheckAgentResult |
Poll status / retrieve result of a background agent |
ListAgentTasks |
List all active and finished sub-agent tasks |
ListAgentTypes |
List all available agent type definitions |
Agent tool parameters
Agent(
prompt="...", # required — task description
subagent_type="coder", # optional — use a specialized agent
isolation="worktree", # optional — isolated git branch
name="my-agent", # optional — name for SendMessage later
wait=False, # optional — run in background
model="...", # optional — model override
)
How it was wired in
multi_agent/subagent.pyuses absolute imports (import agent as _agent_mod) because the project root is insys.pathwhen running from that directory.agent.pywas updated to inject_system_promptintoconfig:config = {**config, "_depth": depth, "_system_prompt": system_prompt}tools.py(top-level) was updated to passconfigthrough to the registry:and at the bottom:return _registry_execute(name, inputs, cfg)import multi_agent.tools as _multiagent_toolscontext.pysystem prompt template lists Agent, SendMessage, etc. under## Multi-Agent.nano_claude.py/agentscommand callsget_agent_manager().list_tasks()and prints status/worktree info. A_print_background_notifications()function checks for newly completed background agents before each user prompt.
Files changed
| File | Change |
|---|---|
multi_agent/__init__.py |
Created (re-exports) |
multi_agent/subagent.py |
Created (moved + enhanced from subagent.py) |
multi_agent/tools.py |
Created (tool registrations) |
subagent.py |
Converted to backward-compat shim |
agent.py |
Inject _system_prompt into config |
tools.py |
Pass config to registry; import multi_agent.tools |
context.py |
Add Multi-Agent section to system prompt |
nano_claude.py |
/agents command; background notification; _tool_desc() |
tests/test_subagent.py |
Update imports to multi_agent.subagent |
2. Memory (memory/)
What it does
Provides persistent, file-based memory across sessions. Memories are stored as
markdown files with YAML frontmatter. There are two scopes — user (global,
~/.nano_claude/memory/) and project (per-repo, .nano_claude/memory/).
A MEMORY.md index is auto-rebuilt after every save/delete and injected into
the system prompt so Claude knows what memories exist.
Package structure
memory/
__init__.py — re-exports all public symbols
types.py — MEMORY_TYPES, type descriptions, format guidance
store.py — MemoryEntry, save/load/delete/search, index rebuilding
scan.py — MemoryHeader, scan_memory_dir, age/freshness helpers
context.py — get_memory_context(), find_relevant_memories(), truncation
tools.py — registers: MemorySave, MemoryDelete, MemorySearch, MemoryList
memory.py — backward-compat shim
Memory types
Defined in memory/types.py, mirrors the four types from Claude Code:
| Type | Purpose |
|---|---|
user |
User's role, goals, preferences |
feedback |
Corrections and confirmed approaches |
project |
Ongoing work, decisions, deadlines |
reference |
Pointers to external resources |
Storage layout
~/.nano_claude/memory/
MEMORY.md ← auto-generated index (<=200 lines, <=25 KB)
my_note.md
feedback_testing.md
...
.nano_claude/memory/ ← project-local (relative to cwd)
MEMORY.md
...
Each memory file format:
---
name: My Note
description: one-line description for relevance decisions
type: user
created: 2026-04-02
---
Memory content goes here.
**Why:** ...
**How to apply:** ...
Key API
memory/store.py
save_memory(entry: MemoryEntry, scope="user") # save or update (same name = update)
delete_memory(name: str, scope="user") # remove entry + rebuild index
load_entries(scope="user") -> list[MemoryEntry] # load all entries for scope
load_index(scope="all") -> list[MemoryEntry] # "all" merges user + project
search_memory(query: str, scope="all") -> list # keyword search across content+name
get_index_content(scope="all") -> str # raw MEMORY.md text
memory/scan.py
scan_memory_dir(mem_dir, scope) -> list[MemoryHeader] # newest-first, capped at 200
scan_all_memories() -> list[MemoryHeader] # user + project merged
memory_age_str(mtime_s) -> str # "today" | "yesterday" | "N days ago"
memory_freshness_text(mtime_s) -> str # staleness warning for memories >1 day old
format_memory_manifest(headers) -> str # formatted list for display
memory/context.py
get_memory_context() -> str # injected into system prompt
truncate_index_content(raw) -> str # enforces <=200 lines / <=25 KB
find_relevant_memories(query, max_results=5, use_ai=False, config=None)
find_relevant_memories supports optional AI ranking: when use_ai=True it
makes a small API call to rank candidates by relevance to the query.
Tools registered
| Tool | Parameters | Description |
|---|---|---|
MemorySave |
name, description, type, content, scope |
Save or update a memory |
MemoryDelete |
name, scope |
Delete a memory by name |
MemorySearch |
query, scope, use_ai, max_results |
Search by keyword (or AI) |
MemoryList |
scope |
List all memories with age and metadata |
Index truncation
The MEMORY.md index is truncated before being injected into the system prompt:
- Hard limit: 200 lines (mirrors Claude Code's limit)
- Byte limit: 25 000 bytes (mirrors Claude Code's limit)
- A
WARNING:line is appended when either limit is hit
How it was wired in
memory/store.pyexportsUSER_MEMORY_DIRandget_project_memory_diras module-level names so tests can monkeypatch them cleanly.context.py(system prompt builder) callsget_memory_context()at the end ofbuild_system_prompt()and appends the result.tools.py(top-level) adds:import memory.tools as _memory_toolsmemory.py(top-level) is now a shim:from memory.store import MemoryEntry, save_memory, ... from memory.context import get_memory_contextnano_claude.py/memorycommand usesscan_all_memories()to display a mtime-sorted list with freshness warnings.
Files changed
| File | Change |
|---|---|
memory/__init__.py |
Created (re-exports) |
memory/types.py |
Created (MEMORY_TYPES, descriptions, format guidance) |
memory/store.py |
Created (replaced top-level memory.py logic) |
memory/scan.py |
Created (MemoryHeader, age/freshness, manifest) |
memory/context.py |
Created (context injection, truncation, AI search) |
memory/tools.py |
Created (MemorySave, MemoryDelete, MemorySearch, MemoryList) |
memory.py |
Converted to backward-compat shim |
tools.py |
Import memory.tools |
context.py |
Call get_memory_context() in build_system_prompt() |
nano_claude.py |
/memory command uses scan_all_memories() |
tests/test_memory.py |
Completely rewritten (101 tests total) |
3. Skill (skill/)
What it does
Skills are reusable prompt templates stored as markdown files. A user types
/commit or /review pr-123 in the REPL and the skill's prompt (with
arguments substituted) is injected into the conversation. Skills can run
inline (current conversation context) or forked (isolated sub-agent).
Two built-in skills (/commit, /review) are registered programmatically.
Package structure
skill/
__init__.py — re-exports all public symbols; imports builtin to register them
loader.py — SkillDef dataclass, file parsing, load_skills, find_skill, substitute_arguments
builtin.py — built-in skills: /commit, /review
executor.py — execute_skill() (inline or forked)
tools.py — registers: Skill, SkillList
skills.py — backward-compat shim
Skill file format
Place .md files in:
~/.nano_claude/skills/<name>.md(user-level).nano_claude/skills/<name>.md(project-level, takes priority)
---
name: deploy
description: Deploy to an environment
triggers: [/deploy]
allowed-tools: [Bash, Read]
when_to_use: Use when the user wants to deploy. Examples: '/deploy staging v1.2'
argument-hint: [env] [version]
arguments: [env, version]
context: inline
---
Deploy $VERSION to $ENV.
Full args provided: $ARGUMENTS
Frontmatter fields
| Field | Default | Description |
|---|---|---|
name |
required | Skill identifier |
description |
"" |
One-line description shown in /skills |
triggers |
[/<name>] |
Slash commands or phrases that activate this skill |
allowed-tools / tools |
[] |
Tools the skill is allowed to use |
when_to_use |
"" |
Guidance for when Claude should auto-invoke |
argument-hint |
"" |
Hint shown in /skills list, e.g. [branch] [desc] |
arguments |
[] |
Named argument list for $ARG_NAME substitution |
model |
"" |
Model override (fork context only) |
user-invocable |
true |
Show in /skills list |
context |
inline |
inline = current conversation, fork = isolated sub-agent |
Argument substitution
substitute_arguments(prompt, args, arg_names) in skill/loader.py:
$ARGUMENTS→ the full raw args string$ARG_NAME→ positional substitution (first word → first arg name, etc.)- Missing args become empty strings
prompt: "Deploy $VERSION to $ENV. Full: $ARGUMENTS"
args: "1.0 staging"
arg_names: ["env", "version"]
result: "Deploy staging to 1.0. Full: 1.0 staging"
Execution modes
Inline (context: inline, default):
- Skill prompt is injected into the current
AgentState - History is shared — the user can see and continue the conversation
Fork (context: fork):
- A fresh
AgentStateis created (no shared history) - Optional
modelandallowed-toolsoverrides are applied - Good for self-contained tasks that don't need mid-process user input
Built-in skills
Defined in skill/builtin.py and registered via register_builtin_skill():
| Trigger | Name | Description |
|---|---|---|
/commit |
commit | Review staged changes and create a well-structured git commit |
/review, /review-pr |
review | Review code or PR diff with structured feedback |
Project-level skill files with the same name override built-ins.
Tools registered
| Tool | Parameters | Description |
|---|---|---|
Skill |
name, args |
Invoke a skill by name from inside a conversation |
SkillList |
— | List all available skills with triggers and metadata |
Priority order
When multiple skill sources define the same name, the highest priority wins:
builtin < user (~/.nano_claude/skills/) < project (.nano_claude/skills/)
REPL usage
/commit # run built-in commit skill
/review 123 # review PR #123 (args = "123")
/deploy staging 2.1.0 # custom skill with named args
/skills # list all skills
The /skills command output includes source label, triggers, argument hint,
and the first 80 chars of when_to_use per skill.
How it was wired in
skill/__init__.pyimportsskill.builtinwhich callsregister_builtin_skill()for each built-in — just importing the package registers them.tools.py(top-level) adds:import skill.tools as _skill_toolsskills.py(top-level) becomes a shim re-exporting fromskill/.context.pyadds a## Skillssection listingSkillandSkillList.nano_claude.py:cmd_skillsimports fromskill, showswhen_to_useand source labelhandle_slashimportsfind_skillfromskill; returns(skill, args)tuple- REPL loop calls
substitute_argumentsbefore building the injected message
Files changed
| File | Change |
|---|---|
skill/__init__.py |
Created (re-exports; imports builtin) |
skill/loader.py |
Created (SkillDef, parse, load, find, substitute) |
skill/builtin.py |
Created (/commit, /review built-ins) |
skill/executor.py |
Created (inline + fork execution) |
skill/tools.py |
Created (Skill, SkillList tool registration) |
skills.py |
Converted to backward-compat shim |
tools.py |
Import skill.tools |
context.py |
Add Skills section to system prompt |
nano_claude.py |
cmd_skills, handle_slash, REPL loop updated |
tests/test_skills.py |
Rewritten (22 tests; patches skill.loader) |
How to add custom agents, memories, and skills
Custom agent type
Create ~/.nano_claude/agents/myagent.md:
---
name: myagent
description: Does specialized work
model: claude-haiku-4-5-20251001
tools: [Read, Grep, Bash]
---
You are specialized in X. Focus on Y. Never do Z.
Then use: Agent(prompt="...", subagent_type="myagent")
Custom memory
Use the REPL MemorySave tool or write a file directly to
~/.nano_claude/memory/my_note.md with frontmatter:
---
name: my note
description: short description
type: feedback
created: 2026-04-02
---
Memory content here.
Custom skill
Create ~/.nano_claude/skills/myskill.md (user-level) or
.nano_claude/skills/myskill.md (project-level):
---
name: myskill
description: Does something useful
triggers: [/myskill]
arguments: [target]
argument-hint: [target]
when_to_use: Use when the user wants to do X with a target.
---
Do something useful with $TARGET.
Full context: $ARGUMENTS
Then invoke with /myskill some-target.
Running tests
cd nano-claude-code
# All tests
python -m pytest tests/ -v
# Per-feature
python -m pytest tests/test_subagent.py -v # multi-agent
python -m pytest tests/test_memory.py -v # memory
python -m pytest tests/test_skills.py -v # skills
Total: 101 tests, all passing. Each feature's tests use monkeypatch to
redirect file system paths to tmp_path so no real ~/.nano_claude/
directories are touched during testing.