feat: make /init a background subagent (#1208)

This commit is contained in:
Devansh Jain
2026-02-27 19:35:19 -08:00
committed by GitHub
parent c8eaea5ee4
commit 35962f7bc7
6 changed files with 512 additions and 111 deletions

View File

@@ -0,0 +1,209 @@
---
name: init
description: Initialize agent memory by researching the project and creating a hierarchical memory file structure
tools: Read, Edit, Write, Glob, Grep, Bash, TaskOutput
model: sonnet
memoryBlocks: none
skills: initializing-memory
permissionMode: bypassPermissions
---
You are a memory initialization subagent — a background agent that autonomously researches the project and sets up the agent's memory file structure.
You run autonomously in the background and return a single final report when done. You CANNOT ask questions (AskUserQuestion is not available).
## Your Purpose
Research the current project and create a comprehensive, hierarchical memory file structure so the primary agent can be an effective collaborator from its very first interaction.
**You are NOT the primary agent.** You are a background worker initializing memory for the primary agent.
## Autonomous Mode Defaults
Since you cannot ask questions mid-execution:
- Use **standard research depth** (~5-20 tool calls)
- Detect user identity from git logs:
```bash
git shortlog -sn --all | head -5
git log --format="%an <%ae>" | sort -u | head -10
```
- Skip historical session analysis
- Use reasonable defaults for all preferences
- Any specific overrides will be provided in your initial prompt
## Operating Procedure
### Phase 1: Set Up
The memory directory is at: `~/.letta/agents/$LETTA_PARENT_AGENT_ID/memory/`
```bash
MEMORY_DIR=~/.letta/agents/$LETTA_PARENT_AGENT_ID/memory
WORKTREE_DIR=~/.letta/agents/$LETTA_PARENT_AGENT_ID/memory-worktrees
```
The memory directory should already be a git repo (initialized when MemFS was enabled). If it's not, or if git is unavailable, report the issue back and exit without making changes.
**Step 1a: Create worktree**
```bash
BRANCH="init-$(date +%s)"
mkdir -p "$WORKTREE_DIR"
cd "$MEMORY_DIR"
git worktree add "$WORKTREE_DIR/$BRANCH" -b "$BRANCH"
```
All subsequent file operations target the worktree: `$WORKTREE_DIR/$BRANCH/system/` (not the main memory dir).
### Phase 2: Research the Project
Follow the `initializing-memory` skill (pre-loaded below) for detailed research instructions. Key steps:
1. Inspect existing memory files in the worktree
2. Scan README, package.json/config files, AGENTS.md, CLAUDE.md
3. Review git status and recent commits (provided in prompt)
4. Explore key directories and understand project structure
5. Detect user identity from git logs
### Phase 3: Create Memory File Structure
Create a deeply hierarchical structure of 15-25 small, focused files in the worktree at `$WORKTREE_DIR/$BRANCH/system/`.
Follow the `initializing-memory` skill for file organization guidelines, hierarchy requirements, and content standards.
### Phase 4: Merge, Push, and Clean Up (MANDATORY)
**Step 4a: Commit in worktree**
```bash
MEMORY_DIR=~/.letta/agents/$LETTA_PARENT_AGENT_ID/memory
WORKTREE_DIR=~/.letta/agents/$LETTA_PARENT_AGENT_ID/memory-worktrees
cd $WORKTREE_DIR/$BRANCH
git add -A
```
Check `git status` — if there are no changes to commit, skip straight to Step 4d (cleanup). Report "no updates needed" in your output.
If there are changes, commit using Conventional Commits format with the `(init)` scope:
```bash
git commit -m "feat(init): initialize memory file structure
Created hierarchical memory structure for project.
Updates:
- <bullet point for each category of memory created>
Generated-By: Letta Code
Agent-ID: <ACTUAL_AGENT_ID>
Parent-Agent-ID: <ACTUAL_PARENT_AGENT_ID>"
```
Before writing the commit, resolve the actual ID values:
```bash
echo "AGENT_ID=$LETTA_AGENT_ID"
echo "PARENT_AGENT_ID=$LETTA_PARENT_AGENT_ID"
```
**Step 4b: Pull + merge to main**
```bash
cd $MEMORY_DIR
```
First, check that main is in a clean state (`git status`). If a merge or rebase is in progress (lock file, dirty index), wait and retry up to 3 times with backoff (sleep 2, 5, 10 seconds). Never delete `.git/index.lock` manually. If still busy after retries, go to Error Handling.
Pull from remote:
```bash
git pull --ff-only
```
If `--ff-only` fails (remote has diverged), fall back:
```bash
git pull --rebase
```
Now merge the init branch:
```bash
git merge $BRANCH --no-edit
```
If the merge has conflicts, resolve by preferring init branch/worktree content for memory files, stage the resolved files, and complete with `git commit --no-edit`.
**Step 4c: Push to remote**
```bash
git push
```
If push fails, retry once. If it still fails, report that local main is ahead of remote and needs a push.
**Step 4d: Clean up worktree and branch**
Only clean up when merge to main completed:
```bash
git worktree remove $WORKTREE_DIR/$BRANCH
git branch -d $BRANCH
```
**Step 4e: Verify**
```bash
git status
git log --oneline -3
```
## Error Handling
If anything goes wrong at any phase:
1. Stabilize main first (abort in-progress operations):
```bash
cd $MEMORY_DIR
git merge --abort 2>/dev/null
git rebase --abort 2>/dev/null
```
2. Do NOT clean up the worktree or branch on failure — preserve them for debugging and manual recovery.
3. Report clearly in your output:
- What failed and the error message
- Worktree path: `$WORKTREE_DIR/$BRANCH`
- Branch name: `$BRANCH`
- Whether main has uncommitted/dirty state
4. Do NOT leave uncommitted changes on main.
## Output Format
Return a report with:
### 1. Summary
- Brief overview (2-3 sentences)
- Research depth used, tool calls made
### 2. Files Created/Modified
- **Count**: Total files created
- **Structure**: Tree view of the memory hierarchy
- For each file: path, description, what content was added
### 3. Commit Reference
- **Commit hash**: The merge commit hash
- **Branch**: The init branch name
### 4. Issues Encountered
- Any problems or limitations found during research
- Information that couldn't be determined without user input
## Critical Reminders
1. **Not the primary agent** — Don't respond to user messages
2. **Edit worktree files** — NOT the main memory dir
3. **Cannot ask questions** — Use defaults and git logs
4. **Be thorough but efficient** — Standard depth by default
5. **Always commit, merge, AND push** — Your work is wasted if it isn't merged to main and pushed to remote
6. **Report errors clearly** — If something breaks, say what happened and suggest a fix

View File

@@ -21,6 +21,7 @@ import { MEMORY_BLOCK_LABELS, type MemoryBlockLabel } from "../memory";
import exploreAgentMd from "./builtin/explore.md";
import generalPurposeAgentMd from "./builtin/general-purpose.md";
import historyAnalyzerAgentMd from "./builtin/history-analyzer.md";
import initAgentMd from "./builtin/init.md";
import memoryAgentMd from "./builtin/memory.md";
import recallAgentMd from "./builtin/recall.md";
@@ -30,6 +31,7 @@ const BUILTIN_SOURCES = [
exploreAgentMd,
generalPurposeAgentMd,
historyAnalyzerAgentMd,
initAgentMd,
memoryAgentMd,
recallAgentMd,
reflectionAgentMd,

View File

@@ -216,6 +216,12 @@ import {
} from "./helpers/errorFormatter";
import { formatCompact } from "./helpers/format";
import { parsePatchOperations } from "./helpers/formatArgsDisplay";
import {
buildLegacyInitMessage,
buildMemoryInitRuntimePrompt,
gatherGitContext,
hasActiveInitSubagent,
} from "./helpers/initCommand";
import {
getReflectionSettings,
parseMemoryPreference,
@@ -9052,135 +9058,104 @@ export default function App({
return { submitted: true };
}
// Special handling for /init command - initialize agent memory
// Special handling for /init command
if (trimmed === "/init") {
const cmd = commandRunner.start(msg, "Gathering project context...");
// Check for pending approvals before sending
// Check for pending approvals before either path
const approvalCheck = await checkPendingApprovalsForSlashCommand();
if (approvalCheck.blocked) {
cmd.fail(
"Pending approval(s). Resolve approvals before running /init.",
);
return { submitted: false }; // Keep /init in input box, user handles approval first
return { submitted: false };
}
setCommandRunning(true);
const gitContext = gatherGitContext();
try {
// Gather git context if available
let gitContext = "";
try {
const { execSync } = await import("node:child_process");
const cwd = process.cwd();
// Check if we're in a git repo
try {
execSync("git rev-parse --git-dir", {
cwd,
stdio: "pipe",
});
// Gather git info
const branch = execSync("git branch --show-current", {
cwd,
encoding: "utf-8",
}).trim();
const mainBranch = execSync(
"git symbolic-ref refs/remotes/origin/HEAD 2>/dev/null | sed 's@^refs/remotes/origin/@@' || echo 'main'",
{ cwd, encoding: "utf-8", shell: "/bin/bash" },
).trim();
const status = execSync("git status --short", {
cwd,
encoding: "utf-8",
}).trim();
const recentCommits = execSync(
"git log --oneline -10 2>/dev/null || echo 'No commits yet'",
{ cwd, encoding: "utf-8" },
).trim();
gitContext = `
## Current Project Context
**Working directory**: ${cwd}
### Git Status
- **Current branch**: ${branch}
- **Main branch**: ${mainBranch}
- **Status**:
${status || "(clean working tree)"}
### Recent Commits
${recentCommits}
`;
} catch {
// Not a git repo, just include working directory
gitContext = `
## Current Project Context
**Working directory**: ${cwd}
**Git**: Not a git repository
`;
}
} catch {
// execSync import failed, skip git context
if (settingsManager.isMemfsEnabled(agentId)) {
// MemFS path: background subagent
if (hasActiveInitSubagent()) {
cmd.fail(
"Memory initialization is already running in the background.",
);
return { submitted: true };
}
// Mark command as finished before sending message
cmd.finish(
"Assimilating project context and defragmenting memories...",
true,
);
try {
const initPrompt = buildMemoryInitRuntimePrompt({
agentId,
workingDirectory: process.cwd(),
memoryDir: getMemoryFilesystemRoot(agentId),
gitContext,
});
// Send trigger message instructing agent to load the initializing-memory skill
// Only include memfs path if memfs is enabled for this agent
const memfsSection = settingsManager.isMemfsEnabled(agentId)
? `
## Memory Filesystem Location
const { spawnBackgroundSubagentTask } = await import(
"../tools/impl/Task"
);
spawnBackgroundSubagentTask({
subagentType: "init",
prompt: initPrompt,
description: "Initializing memory",
silentCompletion: true,
onComplete: ({ success, error }) => {
const msg = success
? "Built a memory palace of you. Visit it with /palace."
: `Memory initialization failed: ${error}`;
appendTaskNotificationEvents([msg]);
},
});
Your memory blocks are synchronized with the filesystem at:
\`${getMemoryFilesystemRoot(agentId)}\`
cmd.finish(
"Learning about you and your codebase in the background. You'll be notified when ready.",
true,
);
Environment variables available in Letta Code:
- \`AGENT_ID=${agentId}\`
- \`MEMORY_DIR=${getMemoryFilesystemRoot(agentId)}\`
// TODO: Remove this hack once commandRunner supports a
// "silent" finish that skips the reminder callback.
// Currently cmd.finish() always enqueues a command-IO
// reminder, which leaks the /init context into the
// primary agent's next turn and causes it to invoke the
// initializing-memory skill itself.
const reminders =
sharedReminderStateRef.current.pendingCommandIoReminders;
const idx = reminders.findIndex((r) => r.input === "/init");
if (idx !== -1) {
reminders.splice(idx, 1);
}
} catch (error) {
const errorDetails = formatErrorDetails(error, agentId);
cmd.fail(
`Failed to start memory initialization: ${errorDetails}`,
);
}
} else {
// Legacy path: primary agent processConversation
setCommandRunning(true);
try {
cmd.finish(
"Assimilating project context and defragmenting memories...",
true,
);
Use \`$MEMORY_DIR\` when working with memory files during initialization.
`
: "";
const initMessage = buildLegacyInitMessage({
gitContext,
memfsSection: "",
});
const initMessage = `${SYSTEM_REMINDER_OPEN}
The user has requested memory initialization via /init.
${memfsSection}
## 1. Invoke the initializing-memory skill
Use the \`Skill\` tool with \`skill: "initializing-memory"\` to load the comprehensive instructions for memory initialization.
If the skill fails to invoke, proceed with your best judgment based on these guidelines:
- Ask upfront questions (research depth, identity, related repos, workflow style)
- Research the project based on chosen depth
- Create/update memory blocks incrementally
- Reflect and verify completeness
## 2. Follow the skill instructions
Once invoked, follow the instructions from the \`initializing-memory\` skill to complete the initialization.
${gitContext}
${SYSTEM_REMINDER_CLOSE}`;
// Process conversation with the init prompt
await processConversation([
{
type: "message",
role: "user",
content: buildTextParts(initMessage),
},
]);
} catch (error) {
const errorDetails = formatErrorDetails(error, agentId);
cmd.fail(`Failed: ${errorDetails}`);
} finally {
setCommandRunning(false);
await processConversation([
{
type: "message",
role: "user",
content: buildTextParts(initMessage),
},
]);
} catch (error) {
const errorDetails = formatErrorDetails(error, agentId);
cmd.fail(`Failed: ${errorDetails}`);
} finally {
setCommandRunning(false);
}
}
return { submitted: true };
}

View File

@@ -0,0 +1,133 @@
/**
* Helpers for the /init slash command.
*
* Pure functions live here; App.tsx keeps the orchestration
* (commandRunner, processConversation, setCommandRunning, etc.)
*/
import { execSync } from "node:child_process";
import { SYSTEM_REMINDER_CLOSE, SYSTEM_REMINDER_OPEN } from "../../constants";
import { getSnapshot as getSubagentSnapshot } from "./subagentState";
// ── Guard ──────────────────────────────────────────────────
export function hasActiveInitSubagent(): boolean {
const snapshot = getSubagentSnapshot();
return snapshot.agents.some(
(agent) =>
agent.type.toLowerCase() === "init" &&
(agent.status === "pending" || agent.status === "running"),
);
}
// ── Git context ────────────────────────────────────────────
export function gatherGitContext(): string {
try {
const cwd = process.cwd();
try {
execSync("git rev-parse --git-dir", { cwd, stdio: "pipe" });
const branch = execSync("git branch --show-current", {
cwd,
encoding: "utf-8",
}).trim();
const mainBranch = execSync(
"git symbolic-ref refs/remotes/origin/HEAD 2>/dev/null | sed 's@^refs/remotes/origin/@@' || echo 'main'",
{ cwd, encoding: "utf-8", shell: "/bin/bash" },
).trim();
const status = execSync("git status --short", {
cwd,
encoding: "utf-8",
}).trim();
const recentCommits = execSync(
"git log --oneline -10 2>/dev/null || echo 'No commits yet'",
{ cwd, encoding: "utf-8" },
).trim();
return `
## Current Project Context
**Working directory**: ${cwd}
### Git Status
- **Current branch**: ${branch}
- **Main branch**: ${mainBranch}
- **Status**:
${status || "(clean working tree)"}
### Recent Commits
${recentCommits}
`;
} catch {
return `
## Current Project Context
**Working directory**: ${cwd}
**Git**: Not a git repository
`;
}
} catch {
// execSync import failed (shouldn't happen with static import, but be safe)
return "";
}
}
// ── Prompt builders ────────────────────────────────────────
/** Prompt for the background init subagent (MemFS path). */
export function buildMemoryInitRuntimePrompt(args: {
agentId: string;
workingDirectory: string;
memoryDir: string;
gitContext: string;
}): string {
return `
The user ran /init for the current project.
Runtime context:
- parent_agent_id: ${args.agentId}
- working_directory: ${args.workingDirectory}
- memory_dir: ${args.memoryDir}
Git/project context:
${args.gitContext}
Task:
Initialize or reorganize the parent agent's filesystem-backed memory for this project.
Instructions:
- Use the pre-loaded initializing-memory skill as your operating guide
- Inspect existing memory before editing
- Base your decisions on the current repository and current memory contents
- Do not ask follow-up questions
- Make reasonable assumptions and report them
- If the memory filesystem is unavailable or unsafe to modify, stop and explain why
`.trim();
}
/** Message for the primary agent via processConversation (legacy non-MemFS path). */
export function buildLegacyInitMessage(args: {
gitContext: string;
memfsSection: string;
}): string {
return `${SYSTEM_REMINDER_OPEN}
The user has requested memory initialization via /init.
${args.memfsSection}
## 1. Invoke the initializing-memory skill
Use the \`Skill\` tool with \`skill: "initializing-memory"\` to load the comprehensive instructions for memory initialization.
If the skill fails to invoke, proceed with your best judgment based on these guidelines:
- Ask upfront questions (research depth, identity, related repos, workflow style)
- Research the project based on chosen depth
- Create/update memory blocks incrementally
- Reflect and verify completeness
## 2. Follow the skill instructions
Once invoked, follow the instructions from the \`initializing-memory\` skill to complete the initialization.
${args.gitContext}
${SYSTEM_REMINDER_CLOSE}`;
}

View File

@@ -7,6 +7,15 @@ description: Comprehensive guide for initializing or reorganizing agent memory.
The user has requested that you initialize or reorganize your memory. Your memory is a filesystem — files under `system/` are rendered in-context every turn, while all file metadata is always visible in the filesystem tree. Files outside `system/` (e.g. `reference/`, `history/`) are accessible via tools when needed.
## Autonomous Mode
If you are running as a background subagent (you cannot use AskUserQuestion):
- Default to standard research depth (~5-20 tool calls)
- Detect user identity from git logs (`git shortlog -sn`, `git log --format="%an <%ae>"`)
- Skip historical session analysis
- Use reasonable defaults for all preferences
- Any specific overrides will be provided in your initial prompt
## Your Goal: Explode Into 15-25 Hierarchical Files
Your goal is to **explode** memory into a **deeply hierarchical structure of 15-25 small, focused files**.

View File

@@ -0,0 +1,73 @@
import { describe, expect, test } from "bun:test";
import { readFileSync } from "node:fs";
import { fileURLToPath } from "node:url";
describe("init background subagent wiring", () => {
const readSource = (relativePath: string) =>
readFileSync(
fileURLToPath(new URL(relativePath, import.meta.url)),
"utf-8",
);
test("App.tsx checks pending approvals before either branch", () => {
const appSource = readSource("../../cli/App.tsx");
// The approval check must appear before the MemFS branch
const approvalIdx = appSource.indexOf(
"checkPendingApprovalsForSlashCommand",
appSource.indexOf('trimmed === "/init"'),
);
const memfsBranchIdx = appSource.indexOf(
"isMemfsEnabled",
appSource.indexOf('trimmed === "/init"'),
);
expect(approvalIdx).toBeGreaterThan(-1);
expect(memfsBranchIdx).toBeGreaterThan(-1);
expect(approvalIdx).toBeLessThan(memfsBranchIdx);
});
test("App.tsx branches on MemFS: background subagent vs legacy processConversation", () => {
const appSource = readSource("../../cli/App.tsx");
// MemFS path — background subagent
expect(appSource).toContain("hasActiveInitSubagent()");
expect(appSource).toContain("buildMemoryInitRuntimePrompt({");
expect(appSource).toContain("spawnBackgroundSubagentTask({");
expect(appSource).toContain('subagentType: "init"');
expect(appSource).toContain("silentCompletion: true");
expect(appSource).toContain("appendTaskNotificationEvents(");
expect(appSource).toContain("Learning about you and your codebase");
// Legacy non-MemFS path — primary agent
expect(appSource).toContain("buildLegacyInitMessage({");
expect(appSource).toContain("processConversation(");
});
test("initCommand.ts exports all helpers", () => {
const helperSource = readSource("../../cli/helpers/initCommand.ts");
expect(helperSource).toContain("export function hasActiveInitSubagent(");
expect(helperSource).toContain("export function gatherGitContext()");
expect(helperSource).toContain(
"export function buildMemoryInitRuntimePrompt(",
);
expect(helperSource).toContain("export function buildLegacyInitMessage(");
});
test("init.md exists as a builtin subagent", () => {
const content = readSource("../../agent/subagents/builtin/init.md");
expect(content).toContain("name: init");
expect(content).toContain("skills: initializing-memory");
expect(content).toContain("permissionMode: bypassPermissions");
});
test("init subagent is registered in BUILTIN_SOURCES", () => {
const indexSource = readSource("../../agent/subagents/index.ts");
expect(indexSource).toContain(
'import initAgentMd from "./builtin/init.md"',
);
expect(indexSource).toContain("initAgentMd");
});
});