// Package bootstrap loads workspace persona/context files and injects them // into the agent's system prompt. Matching TS agents/workspace.ts + bootstrap-files.ts. // // Bootstrap files are loaded from the workspace directory at startup: // // AGENTS.md — operating instructions (every session) // SOUL.md — persona, tone, boundaries // USER.md — user profile // IDENTITY.md— agent name, emoji, creature, vibe // TOOLS.md — local tool notes // BOOTSTRAP.md— first-run ritual (deleted after completion) // MEMORY.md — long-term curated memory package bootstrap import ( "os" "path/filepath" "strings" ) // Bootstrap filenames (matching TS workspace.ts constants). const ( AgentsFile = "AGENTS.md" SoulFile = "SOUL.md" ToolsFile = "TOOLS.md" IdentityFile = "IDENTITY.md" UserFile = "USER.md" UserPredefinedFile = "USER_PREDEFINED.md" BootstrapFile = "BOOTSTRAP.md" DelegationFile = "DELEGATION.md" TeamFile = "TEAM.md" AvailabilityFile = "AVAILABILITY.md" HeartbeatFile = "HEARTBEAT.md" MemoryFile = "MEMORY.md" MemoryAltFile = "memory.md" MemoryJSONFile = "MEMORY.json" ) // standardFiles is the ordered list of bootstrap files to load. var standardFiles = []string{ AgentsFile, SoulFile, ToolsFile, IdentityFile, UserFile, BootstrapFile, } // minimalAllowlist is the set of files loaded for subagent/cron sessions. // Matching TS MINIMAL_BOOTSTRAP_ALLOWLIST. var minimalAllowlist = map[string]bool{ AgentsFile: true, ToolsFile: true, } // File represents a workspace bootstrap file loaded from disk. type File struct { Name string // filename (e.g. "AGENTS.md") Path string // absolute path Content string // file content (empty if missing) Missing bool // true if file doesn't exist on disk } // ContextFile is the truncated version ready for system prompt injection. // Matches TS EmbeddedContextFile type. type ContextFile struct { Path string // display path (e.g. "SOUL.md") Content string // truncated content } // LoadWorkspaceFiles reads all recognized bootstrap files from a workspace directory. // Files are returned in a fixed order matching the TS implementation. // Missing files are included with Missing=true and empty Content. func LoadWorkspaceFiles(workspaceDir string) []File { var files []File // Load standard files for _, name := range standardFiles { f := loadFile(workspaceDir, name) files = append(files, f) } // Load MEMORY.md (try MEMORY.md first, then memory.md) memFile := loadFile(workspaceDir, MemoryFile) if memFile.Missing { memFile = loadFile(workspaceDir, MemoryAltFile) } files = append(files, memFile) return files } // FilterForSession filters bootstrap files based on session type. // Normal sessions get all files. Subagent and cron sessions get only // AGENTS.md and TOOLS.md (minimal mode), matching TS filterBootstrapFilesForSession(). func FilterForSession(files []File, sessionKey string) []File { if !IsSubagentSession(sessionKey) && !IsCronSession(sessionKey) && !IsHeartbeatSession(sessionKey) { return files } var filtered []File for _, f := range files { if minimalAllowlist[f.Name] { filtered = append(filtered, f) } } return filtered } // IsSubagentSession checks if a session key indicates a subagent session. // Session key format: agent:{agentId}:{rest} // Subagent sessions have "subagent:" in the rest part. func IsSubagentSession(sessionKey string) bool { rest := sessionRest(sessionKey) return strings.HasPrefix(strings.ToLower(rest), "subagent:") } // IsCronSession checks if a session key indicates a cron session. // Session key format: agent:{agentId}:{rest} // Cron sessions have "cron:" in the rest part. func IsCronSession(sessionKey string) bool { rest := sessionRest(sessionKey) return strings.HasPrefix(strings.ToLower(rest), "cron:") } // IsHeartbeatSession checks if a session key indicates a heartbeat session. func IsHeartbeatSession(sessionKey string) bool { rest := sessionRest(sessionKey) return strings.HasPrefix(rest, "heartbeat") } // IsTeamSession checks if a session key indicates a team-dispatched task session. // Session key format: agent:{agentId}:team:{teamID}:{chatID} // Team sessions have "team:" in the rest part. func IsTeamSession(sessionKey string) bool { rest := sessionRest(sessionKey) return strings.HasPrefix(strings.ToLower(rest), "team:") } // sessionRest extracts the rest part after "agent:{agentId}:" from a session key. func sessionRest(sessionKey string) string { // Format: agent:{agentId}:{rest} parts := strings.SplitN(sessionKey, ":", 3) if len(parts) < 3 || parts[0] != "agent" { return "" } return parts[2] } func loadFile(dir, name string) File { path := filepath.Join(dir, name) data, err := os.ReadFile(path) if err != nil { return File{Name: name, Path: path, Missing: true} } return File{Name: name, Path: path, Content: string(data), Missing: false} }