From eecf267935050d1c6e088d4ba43230b7f666473f Mon Sep 17 00:00:00 2001 From: Snider Date: Fri, 30 Jan 2026 10:05:35 +0000 Subject: [PATCH] docs(plans): add Core IPC design for CLI commands Documents the four dispatch patterns (ACTION, QUERY, QUERYALL, PERFORM), worker bundle architecture, and permissions-through-presence model. Co-Authored-By: Claude Opus 4.5 --- docs/plans/2026-01-30-core-ipc-design.md | 373 +++++++++++++++++++++++ 1 file changed, 373 insertions(+) create mode 100644 docs/plans/2026-01-30-core-ipc-design.md diff --git a/docs/plans/2026-01-30-core-ipc-design.md b/docs/plans/2026-01-30-core-ipc-design.md new file mode 100644 index 0000000..ec3c9c3 --- /dev/null +++ b/docs/plans/2026-01-30-core-ipc-design.md @@ -0,0 +1,373 @@ +# Core Framework IPC Design + +> Design document for refactoring CLI commands to use the Core framework's IPC system. + +## Overview + +The Core framework provides a dependency injection and inter-process communication (IPC) system originally designed for orchestrating services. This design extends the framework with request/response patterns and applies it to CLI commands. + +Commands build "worker bundles" - sandboxed Core instances with specific services. The bundle configuration acts as a permissions layer: if a service isn't registered, that capability isn't available. + +## Dispatch Patterns + +Four patterns for service communication: + +| Method | Behaviour | Returns | Use Case | +|--------|-----------|---------|----------| +| `ACTION` | Broadcast to all handlers | `error` | Events, notifications | +| `QUERY` | First responder wins | `(any, bool, error)` | Get data | +| `QUERYALL` | Broadcast, collect all | `([]any, error)` | Aggregate from multiple services | +| `PERFORM` | First responder executes | `(any, bool, error)` | Execute a task with side effects | + +### ACTION (existing) + +Fire-and-forget broadcast. All registered handlers receive the message. Errors are aggregated. + +```go +c.ACTION(ActionServiceStartup{}) +``` + +### QUERY (new) + +Request data from services. Stops at first handler that returns `handled=true`. + +```go +result, handled, err := c.QUERY(git.QueryStatus{Paths: paths}) +if !handled { + // No service registered to handle this query +} +statuses := result.([]git.RepoStatus) +``` + +### QUERYALL (new) + +Broadcast query to all handlers, collect all responses. Useful for aggregating results from multiple services (e.g., multiple QA/lint tools). + +```go +results, err := c.QUERYALL(qa.QueryLint{Paths: paths}) +for _, r := range results { + lint := r.(qa.LintResult) + fmt.Printf("%s found %d issues\n", lint.Tool, len(lint.Issues)) +} +``` + +### PERFORM (new) + +Execute a task with side effects. Stops at first handler that returns `handled=true`. + +```go +result, handled, err := c.PERFORM(agentic.TaskCommit{ + Path: repo.Path, + Name: repo.Name, +}) +if !handled { + // Agentic service not in bundle - commits not available +} +``` + +## Architecture + +``` +┌─────────────────────────────────────────────────────────────┐ +│ cmd/dev/dev_work.go │ +│ - Builds worker bundle │ +│ - Triggers PERFORM(TaskWork{}) │ +└─────────────────────┬───────────────────────────────────────┘ + │ +┌─────────────────────▼───────────────────────────────────────┐ +│ cmd/dev/bundles.go │ +│ - NewWorkBundle() - git + agentic + dev │ +│ - NewStatusBundle() - git + dev only │ +│ - Bundle config = permissions │ +└─────────────────────┬───────────────────────────────────────┘ + │ +┌─────────────────────▼───────────────────────────────────────┐ +│ pkg/dev/service.go │ +│ - Orchestrates workflow │ +│ - QUERY(git.QueryStatus{}) │ +│ - PERFORM(agentic.TaskCommit{}) │ +│ - PERFORM(git.TaskPush{}) │ +└─────────────────────┬───────────────────────────────────────┘ + │ + ┌─────────────┴─────────────┐ + ▼ ▼ +┌───────────────────┐ ┌───────────────────┐ +│ pkg/git/service │ │ pkg/agentic/svc │ +│ │ │ │ +│ Queries: │ │ Tasks: │ +│ - QueryStatus │ │ - TaskCommit │ +│ - QueryDirtyRepos │ │ - TaskPrompt │ +│ - QueryAheadRepos │ │ │ +│ │ │ │ +│ Tasks: │ │ │ +│ - TaskPush │ │ │ +│ - TaskPull │ │ │ +└───────────────────┘ └───────────────────┘ +``` + +## Permissions Model + +Permissions are implicit through bundle configuration: + +```go +// Full capabilities - can commit and push +func NewWorkBundle(opts WorkBundleOptions) (*framework.Runtime, error) { + return framework.NewWithFactories(nil, map[string]framework.ServiceFactory{ + "dev": func() (any, error) { return dev.NewService(opts.Dev)(nil) }, + "git": func() (any, error) { return git.NewService(opts.Git)(nil) }, + "agentic": func() (any, error) { return agentic.NewService(opts.Agentic)(nil) }, + }) +} + +// Read-only - status queries only, no commits +func NewStatusBundle(opts StatusBundleOptions) (*framework.Runtime, error) { + return framework.NewWithFactories(nil, map[string]framework.ServiceFactory{ + "dev": func() (any, error) { return dev.NewService(opts.Dev)(nil) }, + "git": func() (any, error) { return git.NewService(opts.Git)(nil) }, + // No agentic service - TaskCommit will be unhandled + }) +} +``` + +Service options provide fine-grained control: + +```go +agentic.NewService(agentic.ServiceOptions{ + AllowEdit: false, // Claude can only use read-only tools +}) + +agentic.NewService(agentic.ServiceOptions{ + AllowEdit: true, // Claude can use Write/Edit tools +}) +``` + +**Key principle**: Code never checks permissions explicitly. It dispatches actions and either they're handled or they're not. The bundle configuration is the single source of truth for what's allowed. + +## Framework Changes + +### New Types (interfaces.go) + +```go +type Query interface{} +type Task interface{} + +type QueryHandler func(*Core, Query) (any, bool, error) +type TaskHandler func(*Core, Task) (any, bool, error) +``` + +### Core Struct Additions (interfaces.go) + +```go +type Core struct { + // ... existing fields + + queryMu sync.RWMutex + queryHandlers []QueryHandler + + taskMu sync.RWMutex + taskHandlers []TaskHandler +} +``` + +### New Methods (core.go) + +```go +// QUERY - first responder wins +func (c *Core) QUERY(q Query) (any, bool, error) + +// QUERYALL - broadcast, collect all responses +func (c *Core) QUERYALL(q Query) ([]any, error) + +// PERFORM - first responder executes +func (c *Core) PERFORM(t Task) (any, bool, error) + +// Registration +func (c *Core) RegisterQuery(h QueryHandler) +func (c *Core) RegisterTask(h TaskHandler) +``` + +### Re-exports (framework.go) + +```go +type Query = core.Query +type Task = core.Task +type QueryHandler = core.QueryHandler +type TaskHandler = core.TaskHandler +``` + +## Service Implementation Pattern + +Services register handlers during startup: + +```go +func (s *Service) OnStartup(ctx context.Context) error { + s.Core().RegisterAction(s.handleAction) + s.Core().RegisterQuery(s.handleQuery) + s.Core().RegisterTask(s.handleTask) + return nil +} + +func (s *Service) handleQuery(c *framework.Core, q framework.Query) (any, bool, error) { + switch m := q.(type) { + case QueryStatus: + result := s.getStatus(m.Paths, m.Names) + return result, true, nil + case QueryDirtyRepos: + return s.DirtyRepos(), true, nil + } + return nil, false, nil // Not handled +} + +func (s *Service) handleTask(c *framework.Core, t framework.Task) (any, bool, error) { + switch m := t.(type) { + case TaskPush: + err := s.push(m.Path) + return nil, true, err + case TaskPull: + err := s.pull(m.Path) + return nil, true, err + } + return nil, false, nil // Not handled +} +``` + +## Git Service Queries & Tasks + +```go +// pkg/git/queries.go +type QueryStatus struct { + Paths []string + Names map[string]string +} + +type QueryDirtyRepos struct{} +type QueryAheadRepos struct{} + +// pkg/git/tasks.go +type TaskPush struct { + Path string + Name string +} + +type TaskPull struct { + Path string + Name string +} + +type TaskPushMultiple struct { + Paths []string + Names map[string]string +} +``` + +## Agentic Service Tasks + +```go +// pkg/agentic/tasks.go +type TaskCommit struct { + Path string + Name string + CanEdit bool +} + +type TaskPrompt struct { + Prompt string + WorkDir string + AllowedTools []string +} +``` + +## Dev Workflow Service + +```go +// pkg/dev/tasks.go +type TaskWork struct { + RegistryPath string + StatusOnly bool + AutoCommit bool +} + +type TaskCommitAll struct { + RegistryPath string +} + +type TaskPushAll struct { + RegistryPath string + Force bool +} +``` + +## Command Simplification + +Before (dev_work.go - 327 lines of orchestration): + +```go +func runWork(registryPath string, statusOnly, autoCommit bool) error { + // Load registry + // Get git status + // Display table + // Loop dirty repos, shell out to claude + // Re-check status + // Confirm push + // Push repos + // Handle diverged branches + // ... +} +``` + +After (dev_work.go - minimal): + +```go +func runWork(registryPath string, statusOnly, autoCommit bool) error { + bundle, err := NewWorkBundle(WorkBundleOptions{ + RegistryPath: registryPath, + }) + if err != nil { + return err + } + + ctx := context.Background() + bundle.Core.ServiceStartup(ctx, nil) + defer bundle.Core.ServiceShutdown(ctx) + + _, _, err = bundle.Core.PERFORM(dev.TaskWork{ + StatusOnly: statusOnly, + AutoCommit: autoCommit, + }) + return err +} +``` + +All orchestration logic moves to `pkg/dev/service.go` where it can be tested independently and reused. + +## Implementation Tasks + +1. **Framework Core** - Add Query, Task types and QUERY/QUERYALL/PERFORM methods +2. **Framework Re-exports** - Update framework.go with new types +3. **Git Service** - Add query and task handlers +4. **Agentic Service** - Add task handlers +5. **Dev Service** - Create workflow orchestration service +6. **Bundles** - Create bundle factories in cmd/dev/ +7. **Commands** - Simplify cmd/dev/*.go to use bundles + +## Future: CLI-Wide Runtime + +Phase 2 will add a CLI-wide Core instance that: + +- Handles signals (SIGINT, SIGTERM) +- Manages UI state +- Spawns worker bundles as "interactable elements" +- Provides cross-bundle communication + +Worker bundles become sandboxed children of the CLI runtime, with the runtime controlling what capabilities each bundle receives. + +## Testing + +Each layer is independently testable: + +- **Framework**: Unit tests for QUERY/QUERYALL/PERFORM dispatch +- **Services**: Unit tests with mock Core instances +- **Bundles**: Integration tests with real services +- **Commands**: E2E tests via CLI invocation + +The permission model is testable by creating bundles with/without specific services and verifying behaviour.