diff --git a/docs/RFC.plan.md b/docs/RFC.plan.md new file mode 100644 index 0000000..ce99d49 --- /dev/null +++ b/docs/RFC.plan.md @@ -0,0 +1,65 @@ +# RFC Plan — How to Start a core/agent Session + +> For future Claude sessions. Do this FIRST before touching code. + +## Step 1: Load the Domain + +Read these files in order using ReadFile. Yes, all of them. The ~2000 tokens of boot cost pays for itself immediately — zero corrections, zero rediscovery. + +``` +1. ReadFile /Users/snider/Code/core/go/docs/RFC.md (1278 lines — core/go contract, 21 sections) +2. ReadFile /Users/snider/Code/core/agent/docs/RFC.md (~500 lines — core/agent contract, 22 sections) +3. ReadFile /Users/snider/Code/core/go-process/docs/RFC.md (~224 lines — go-process contract, 8 sections) +``` + +After loading all three, you have the full domain model: +- Every core/go primitive and how core/agent uses it +- The current state of core/agent (what's migrated, what isn't) +- The file layout with per-file migration actions +- The quality gates (10 disallowed imports, test naming, string concat) +- The completion pipeline architecture +- The entitlement/permission model + +## Step 2: Verify Context + +After loading, you should be able to answer without looking at code: +- What does `c.Action("agentic.dispatch").Run(ctx, opts)` do? +- Why is `proc.go` being deleted? +- What replaces the ACTION cascade in `handlers.go`? +- Which imports are disallowed and what replaces each one? +- What does `c.Entitled("agentic.concurrency", 1)` check? + +If you can't answer these, re-read the RFCs. + +## Step 3: Work the Migration + +The core/agent RFC Section "Current State" has the annotated file layout. Each file is marked DELETE, REWRITE, or MIGRATE with the specific action. + +Priority order: +1. `OnStartup`/`OnShutdown` return `Result` (breaking, do first) +2. Replace `unsafe.Pointer` → `Fs.NewUnrestricted()` (paths.go) +3. Replace `os.WriteFile` → `Fs.WriteAtomic` (status.go) +4. Replace `core.ValidateName` / `core.SanitisePath` (prep.go, plan.go) +5. Replace `core.ID()` (plan.go) +6. Register capabilities as named Actions (OnStartup) +7. Replace ACTION cascade with Task pipeline (handlers.go) +8. Delete `proc.go` → `s.Core().Process()` (after go-process v0.8.0) +9. AX-7 test rename + gap fill +10. Example tests per source file + +## Step 4: Session Cadence + +Follow the CLAUDE.md session cadence: +- **0-50%**: Build — implement the migration +- **50%**: Feature freeze — finish what's in progress +- **60%+**: Refine — review passes on RFC.md, docs, CLAUDE.md, llm.txt +- **80%+**: Save state — update RFCs with what shipped + +## What NOT to Do + +- Don't guess the architecture — it's in the RFCs +- Don't use `os`, `os/exec`, `fmt`, `errors`, `io`, `path/filepath`, `encoding/json`, `strings`, `log`, `unsafe` — Core has primitives for all of these +- Don't use string concat with `+` — use `core.Concat()` or `core.Path()` +- Don't add `fmt.Println` — use `core.Println()` +- Don't write anonymous closures in command registration — extract to named methods +- Don't nest `c.ACTION()` calls — use `c.Task()` composition diff --git a/docs/plans/2026-03-25-core-go-v0.8.0-migration.md b/docs/plans/2026-03-25-core-go-v0.8.0-migration.md new file mode 100644 index 0000000..40fac06 --- /dev/null +++ b/docs/plans/2026-03-25-core-go-v0.8.0-migration.md @@ -0,0 +1,262 @@ +# core/agent — core/go v0.8.0 Migration + +> Written by Cladius with full core/go + core/agent domain context (2026-03-25). +> Read core/go docs/RFC.md for the full spec. This plan covers what core/agent needs to change. + +## What Changed in core/go + +core/go v0.8.0 shipped: +- `Startable.OnStartup()` returns `core.Result` (not `error`) — BREAKING +- `Stoppable.OnShutdown()` returns `core.Result` (not `error`) — BREAKING +- `c.Action("name")` — named action registry with panic recovery +- `c.Task("name", TaskDef{Steps})` — composed action sequences +- `c.Process()` — managed execution (sugar over Actions) +- `Registry[T]` — universal collection, all registries migrated +- `Fs.WriteAtomic()` — write-to-temp-then-rename +- `Fs.NewUnrestricted()` — legitimate sandbox bypass (replaces unsafe.Pointer) +- `core.ID()` — unique identifier primitive +- `core.ValidateName()` / `core.SanitisePath()` — reusable validation +- `CommandLifecycle` removed → `Command.Managed` string field +- `c.Entitled()` — permission primitive (Section 21, implementation pending) + +## Priority 1: Fix Breaking Changes + +### 1a. OnStartup Returns Result + +Every service implementing `Startable` needs updating: + +```go +// Before: +func (s *PrepSubsystem) OnStartup(ctx context.Context) error { + s.registerCommands(ctx) + return nil +} + +// After: +func (s *PrepSubsystem) OnStartup(ctx context.Context) core.Result { + s.registerCommands(ctx) + return core.Result{OK: true} +} +``` + +Files to change: +- `pkg/agentic/prep.go` — PrepSubsystem.OnStartup +- `pkg/brain/brain.go` — Brain.OnStartup (if Startable) +- `pkg/monitor/monitor.go` — Monitor.OnStartup (if Startable) + +### 1b. OnShutdown Returns Result + +Same pattern for `Stoppable`: + +```go +// Before: +func (s *PrepSubsystem) OnShutdown(ctx context.Context) error { ... } + +// After: +func (s *PrepSubsystem) OnShutdown(ctx context.Context) core.Result { ... } +``` + +## Priority 2: Replace unsafe.Pointer Fs Hacks (P11-2) + +Two files use `unsafe.Pointer` to bypass `Fs.root`: + +```go +// Current (paths.go, detect.go): +type fsRoot struct{ root string } +f := &core.Fs{} +(*fsRoot)(unsafe.Pointer(f)).root = root +``` + +Replace with: + +```go +// Target: +f := c.Fs().NewUnrestricted() +// or for a specific root: +f := (&core.Fs{}).New(root) +``` + +Files: +- `pkg/agentic/paths.go` +- `pkg/agentic/detect.go` (if present) + +## Priority 3: Migrate proc.go to c.Process() (Plan 4 Phase C) + +**Requires:** go-process v0.7.0 (registers process.* Actions) + +Once go-process is updated, delete `pkg/agentic/proc.go` entirely and replace all callers: + +```go +// Current (proc.go helpers): +out, err := runCmd(ctx, dir, "git", "log") +ok := gitCmdOK(ctx, dir, "rev-parse", "--git-dir") +output := gitOutput(ctx, dir, "log", "--oneline", "-20") + +// Target (Core methods): +r := s.core.Process().RunIn(ctx, dir, "git", "log") +r := s.core.Process().RunIn(ctx, dir, "git", "rev-parse", "--git-dir") +// r.OK replaces err == nil +``` + +Helper methods on PrepSubsystem: + +```go +func (s *PrepSubsystem) gitCmd(ctx context.Context, dir string, args ...string) core.Result { + return s.core.Process().RunIn(ctx, dir, "git", args...) +} + +func (s *PrepSubsystem) gitOK(ctx context.Context, dir string, args ...string) bool { + return s.gitCmd(ctx, dir, args...).OK +} + +func (s *PrepSubsystem) gitOutput(ctx context.Context, dir string, args ...string) string { + r := s.gitCmd(ctx, dir, args...) + if !r.OK { return "" } + return core.Trim(r.Value.(string)) +} +``` + +Delete after migration: +- `pkg/agentic/proc.go` — all standalone helpers +- `pkg/agentic/proc_test.go` — tests (rewrite as method tests) +- `ensureProcess()` — the lazy init bridge + +## Priority 4: Replace syscall.Kill Calls (Plan 4 Phase D) + +5 call sites use `syscall.Kill(pid, 0)` and `syscall.Kill(pid, SIGTERM)`. + +These already have wrapper functions in proc.go (`processIsRunning`, `processKill`). Once go-process v0.7.0 provides `process.Get(id).IsRunning()`, replace: + +```go +// Current: +processIsRunning(st.ProcessID, st.PID) +processKill(st.ProcessID, st.PID) + +// Target (after go-process v0.7.0): +handle := s.core.Process().Get(st.ProcessID) +handle.IsRunning() +handle.Kill() +``` + +## Priority 5: Replace ACTION Cascade with Task (P6-1) + +**This is the root cause of "agents finish but queue doesn't drain."** + +Current `handlers.go` — nested `c.ACTION()` cascade 4 levels deep: +``` +AgentCompleted → QA → c.ACTION(QAResult) → PR → c.ACTION(PRCreated) → Verify → c.ACTION(PRMerged) +``` + +Target — flat Task pipeline: +```go +c.Task("agent.completion", core.TaskDef{ + Description: "Agent completion pipeline", + Steps: []core.Step{ + {Action: "agentic.qa"}, + {Action: "agentic.auto-pr"}, + {Action: "agentic.verify"}, + {Action: "agentic.ingest", Async: true}, // doesn't block + {Action: "agentic.poke", Async: true}, // doesn't block + }, +}) +``` + +Register named Actions in `agentic.Register()`: +```go +func (s *PrepSubsystem) OnStartup(ctx context.Context) core.Result { + c := s.core + + // Register capabilities as named Actions + c.Action("agentic.qa", s.handleQA) + c.Action("agentic.auto-pr", s.handleAutoPR) + c.Action("agentic.verify", s.handleVerify) + c.Action("agentic.ingest", s.handleIngest) + c.Action("agentic.poke", s.handlePoke) + c.Action("agentic.dispatch", s.handleDispatch) + + // Register the completion pipeline as a Task + c.Task("agent.completion", core.TaskDef{ ... }) + + // ... register commands ... + return core.Result{OK: true} +} +``` + +Then in the ACTION handler, instead of the cascade: +```go +c.RegisterAction(func(c *core.Core, msg core.Message) core.Result { + if _, ok := msg.(messages.AgentCompleted); ok { + go c.Task("agent.completion").Run(ctx, c, opts) + } + return core.Result{OK: true} +}) +``` + +## Priority 6: Migrate writeStatus to WriteAtomic (P4-9) + +51 read-modify-write sites on status.json with no locking. `Fs.WriteAtomic` fixes the underlying I/O race. + +```go +// Current: +os.WriteFile(statusPath, data, 0644) + +// Target: +c.Fs().WriteAtomic(statusPath, string(data)) +``` + +## Priority 7: Use core.ValidateName / core.SanitisePath + +Replace copy-pasted validation: + +```go +// Current (prep.go): +repoName := core.PathBase(input.Repo) +if repoName == "." || repoName == ".." || repoName == "" { + return core.E("prep", "invalid repo name", nil) +} + +// Target: +r := core.ValidateName(input.Repo) +if !r.OK { return r.Value.(error) } +``` + +Files: `prep.go`, `plan.go`, command handlers. + +## Priority 8: Use core.ID() + +Replace ad-hoc ID generation: + +```go +// Current (plan.go): +b := make([]byte, 3) +rand.Read(b) +return slug + "-" + hex.EncodeToString(b) + +// Target: +return core.ID() +``` + +## Implementation Order + +``` +Phase 1 (no go-process dependency): + 1a. Fix OnStartup/OnShutdown return types + 1b. Replace unsafe.Pointer with NewUnrestricted() + 6. Migrate writeStatus to WriteAtomic + 7. Replace validation with ValidateName/SanitisePath + 8. Replace ID generation with core.ID() + +Phase 2 (after go-process v0.7.0): + 3. Migrate proc.go to c.Process() + 4. Replace syscall.Kill + +Phase 3 (architecture): + 5. Replace ACTION cascade with Task pipeline + +Phase 4 (AX-7): + Fill remaining 8% test gaps (92% → 100%) +``` + +Phase 1 can ship immediately — it only depends on core/go v0.8.0 (already done). +Phase 2 is blocked on go-process v0.7.0. +Phase 3 is independent but architecturally significant — needs careful testing.