feat: add RFC.plan.md — session boot instructions for future agents

Step-by-step: load 3 RFCs, verify understanding, work migration,
follow session cadence. Lists what NOT to do (10 disallowed imports,
no string concat, no anonymous closures, no nested ACTION).

Co-Authored-By: Virgil <virgil@lethean.io>
This commit is contained in:
Snider 2026-03-25 20:05:25 +00:00
parent 4216dddea9
commit 96ac2d99cd
2 changed files with 327 additions and 0 deletions

65
docs/RFC.plan.md Normal file
View file

@ -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

View file

@ -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.