From e99ff8b96bacdcfba7654c8c6669f402a58a85de Mon Sep 17 00:00:00 2001 From: dj-oyu <68707227+dj-oyu@users.noreply.github.com> Date: Fri, 13 Mar 2026 16:06:52 +0900 Subject: [PATCH] refactor: extract instance.go fork fields into instanceExt embedded struct Move fork-specific AgentInstance fields and worktree methods to instance_ext.go: - SubagentMgr, Subagents, SkillsFilter - Interview staleness tracking fields - Per-session worktree isolation (worktrees map, all worktree methods) Co-Authored-By: Claude Opus 4.6 --- pkg/agent/instance.go | 111 ++-------------------------------- pkg/agent/instance_ext.go | 122 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 128 insertions(+), 105 deletions(-) create mode 100644 pkg/agent/instance_ext.go diff --git a/pkg/agent/instance.go b/pkg/agent/instance.go index 809c89d49..0ac3d8894 100644 --- a/pkg/agent/instance.go +++ b/pkg/agent/instance.go @@ -7,7 +7,6 @@ import ( "path/filepath" "regexp" "strings" - "sync" "github.com/sipeed/picoclaw/pkg/config" "github.com/sipeed/picoclaw/pkg/git" @@ -20,6 +19,8 @@ import ( // AgentInstance represents a fully configured agent with its own workspace, // session manager, context builder, and tool registry. type AgentInstance struct { + instanceExt // fork-specific fields (see instance_ext.go) + ID string Name string Model string @@ -37,8 +38,6 @@ type AgentInstance struct { Sessions *session.LegacyAdapter ContextBuilder *ContextBuilder Tools *tools.ToolRegistry - Subagents *config.SubagentsConfig - SkillsFilter []string Candidates []providers.FallbackCandidate PlanModel string PlanFallbacks []string @@ -51,18 +50,6 @@ type AgentInstance struct { // LightCandidates holds the resolved provider candidates for the light model. // Pre-computed at agent creation to avoid repeated model_list lookups at runtime. LightCandidates []providers.FallbackCandidate - - // SubagentMgr is set during registerSharedTools when orchestration is enabled. - // Used by runAgentLoop to wait for spawned subagents before worktree cleanup. - SubagentMgr *tools.SubagentManager - - // Interview staleness tracking: consecutive turns where MEMORY.md was not updated. - interviewStaleCount int - interviewMemoryLen int - - // Per-session worktree isolation - worktrees map[string]*git.WorktreeInfo // sessionKey → worktree - worktreeMu sync.RWMutex } // Close releases resources held by the agent instance. @@ -295,6 +282,10 @@ func NewAgentInstance( } return &AgentInstance{ + instanceExt: instanceExt{ + Subagents: subagents, + SkillsFilter: skillsFilter, + }, ID: agentID, Name: agentName, Model: model, @@ -312,8 +303,6 @@ func NewAgentInstance( Sessions: sessionsManager, ContextBuilder: contextBuilder, Tools: toolsRegistry, - Subagents: subagents, - SkillsFilter: skillsFilter, Candidates: candidates, PlanModel: planModel, PlanFallbacks: planFallbacks, @@ -370,94 +359,6 @@ func resolvePlanFallbacks(agentCfg *config.AgentConfig, defaults *config.AgentDe return defaults.PlanModelFallbacks } -// ActivateWorktree creates a worktree for a session. -// projectDir is the git repository to create the worktree in. -// If empty, falls back to ai.Workspace. -// Worktree path: /.worktrees// -func (ai *AgentInstance) ActivateWorktree(sessionKey, taskName, projectDir string) (*git.WorktreeInfo, error) { - if projectDir == "" { - projectDir = ai.Workspace - } - - repoRoot := git.FindRepoRoot(projectDir) - if repoRoot == "" { - return nil, fmt.Errorf("directory is not a git repository: %s", projectDir) - } - - branchName := git.SanitizeBranchName(taskName) - baseName := git.BranchBaseName(branchName) - wtPath := filepath.Join(ai.Workspace, ".worktrees", baseName) - - wt, err := git.CreateWorktree(repoRoot, wtPath, branchName) - if err != nil { - return nil, err - } - - ai.worktreeMu.Lock() - if ai.worktrees == nil { - ai.worktrees = make(map[string]*git.WorktreeInfo) - } - ai.worktrees[sessionKey] = wt - ai.worktreeMu.Unlock() - - return wt, nil -} - -// DeactivateWorktree safe-disposes the session's worktree. -func (ai *AgentInstance) DeactivateWorktree(sessionKey, commitMsg string, discard bool) (*git.DisposeResult, error) { - ai.worktreeMu.Lock() - wt, ok := ai.worktrees[sessionKey] - if ok { - delete(ai.worktrees, sessionKey) - } - ai.worktreeMu.Unlock() - - if !ok || wt == nil { - return nil, nil - } - - repoRoot := git.FindRepoRoot(ai.Workspace) - if repoRoot == "" { - return nil, fmt.Errorf("workspace is not a git repository") - } - - // Even on discard, SafeDispose auto-commits first for safety - if commitMsg != "" && git.HasUncommittedChanges(wt.Path) { - _ = git.AutoCommit(wt.Path, commitMsg) - } - - result := git.SafeDispose(repoRoot, wt) - return &result, nil -} - -// GetWorktree returns the session's active worktree, or nil. -func (ai *AgentInstance) GetWorktree(sessionKey string) *git.WorktreeInfo { - ai.worktreeMu.RLock() - defer ai.worktreeMu.RUnlock() - return ai.worktrees[sessionKey] -} - -// IsInWorktree returns true if the session has an active worktree. -func (ai *AgentInstance) IsInWorktree(sessionKey string) bool { - return ai.GetWorktree(sessionKey) != nil -} - -// EffectiveWorkspace returns worktree path for session, or original Workspace. -func (ai *AgentInstance) EffectiveWorkspace(sessionKey string) string { - if wt := ai.GetWorktree(sessionKey); wt != nil { - return wt.Path - } - return ai.Workspace -} - -// GetWorktreeBranch returns the branch name for the session's worktree, or "". -func (ai *AgentInstance) GetWorktreeBranch(sessionKey string) string { - if wt := ai.GetWorktree(sessionKey); wt != nil { - return wt.Branch - } - return "" -} - func compilePatterns(patterns []string) []*regexp.Regexp { compiled := make([]*regexp.Regexp, 0, len(patterns)) for _, p := range patterns { diff --git a/pkg/agent/instance_ext.go b/pkg/agent/instance_ext.go new file mode 100644 index 000000000..10aa835e9 --- /dev/null +++ b/pkg/agent/instance_ext.go @@ -0,0 +1,122 @@ +package agent + +import ( + "fmt" + "sync" + + "github.com/sipeed/picoclaw/pkg/config" + "github.com/sipeed/picoclaw/pkg/git" + "github.com/sipeed/picoclaw/pkg/tools" +) + +// instanceExt holds fork-specific fields for AgentInstance. +// Embedded in AgentInstance so existing field access continues to work. +type instanceExt struct { + // SubagentMgr is set during registerSharedTools when orchestration is enabled. + // Used by runAgentLoop to wait for spawned subagents before worktree cleanup. + SubagentMgr *tools.SubagentManager + + Subagents *config.SubagentsConfig + SkillsFilter []string + + // Interview staleness tracking: consecutive turns where MEMORY.md was not updated. + interviewStaleCount int + interviewMemoryLen int + + // Per-session worktree isolation + worktrees map[string]*git.WorktreeInfo // sessionKey → worktree + worktreeMu sync.RWMutex +} + +// ActivateWorktree creates a worktree for a session. +// projectDir is the git repository to create the worktree in. +// If empty, falls back to ai.Workspace. +// Worktree path: /.worktrees// +func (ai *AgentInstance) ActivateWorktree(sessionKey, taskName, projectDir string) (*git.WorktreeInfo, error) { + if projectDir == "" { + projectDir = ai.Workspace + } + + repoRoot := git.FindRepoRoot(projectDir) + if repoRoot == "" { + return nil, fmt.Errorf("directory is not a git repository: %s", projectDir) + } + + branchName := git.SanitizeBranchName(taskName) + baseName := git.BranchBaseName(branchName) + wtPath := ai.worktreePath(baseName) + + wt, err := git.CreateWorktree(repoRoot, wtPath, branchName) + if err != nil { + return nil, err + } + + ai.worktreeMu.Lock() + if ai.worktrees == nil { + ai.worktrees = make(map[string]*git.WorktreeInfo) + } + ai.worktrees[sessionKey] = wt + ai.worktreeMu.Unlock() + + return wt, nil +} + +// worktreePath returns the standard path for a worktree under the workspace. +func (ai *AgentInstance) worktreePath(baseName string) string { + return ai.Workspace + "/.worktrees/" + baseName +} + +// DeactivateWorktree safe-disposes the session's worktree. +func (ai *AgentInstance) DeactivateWorktree(sessionKey, commitMsg string, discard bool) (*git.DisposeResult, error) { + ai.worktreeMu.Lock() + wt, ok := ai.worktrees[sessionKey] + if ok { + delete(ai.worktrees, sessionKey) + } + ai.worktreeMu.Unlock() + + if !ok || wt == nil { + return nil, nil + } + + repoRoot := git.FindRepoRoot(ai.Workspace) + if repoRoot == "" { + return nil, fmt.Errorf("workspace is not a git repository") + } + + // Even on discard, SafeDispose auto-commits first for safety + if commitMsg != "" && git.HasUncommittedChanges(wt.Path) { + _ = git.AutoCommit(wt.Path, commitMsg) + } + + result := git.SafeDispose(repoRoot, wt) + return &result, nil +} + +// GetWorktree returns the session's active worktree, or nil. +func (ai *AgentInstance) GetWorktree(sessionKey string) *git.WorktreeInfo { + ai.worktreeMu.RLock() + defer ai.worktreeMu.RUnlock() + return ai.worktrees[sessionKey] +} + +// IsInWorktree returns true if the session has an active worktree. +func (ai *AgentInstance) IsInWorktree(sessionKey string) bool { + return ai.GetWorktree(sessionKey) != nil +} + +// EffectiveWorkspace returns worktree path for session, or original Workspace. +func (ai *AgentInstance) EffectiveWorkspace(sessionKey string) string { + if wt := ai.GetWorktree(sessionKey); wt != nil { + return wt.Path + } + return ai.Workspace +} + +// GetWorktreeBranch returns the branch name for the session's worktree, or "". +func (ai *AgentInstance) GetWorktreeBranch(sessionKey string) string { + if wt := ai.GetWorktree(sessionKey); wt != nil { + return wt.Branch + } + return "" +}