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:
parent
4216dddea9
commit
96ac2d99cd
2 changed files with 327 additions and 0 deletions
65
docs/RFC.plan.md
Normal file
65
docs/RFC.plan.md
Normal 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
|
||||
262
docs/plans/2026-03-25-core-go-v0.8.0-migration.md
Normal file
262
docs/plans/2026-03-25-core-go-v0.8.0-migration.md
Normal 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.
|
||||
Loading…
Add table
Reference in a new issue