Documentation
¶
Overview ¶
Package plan holds the plan-mode lifecycle as a real state machine. The lifecycle itself is unchanged — idle → research → present → approve → apply, driven by /plan, the enter_plan/execute_plan/cancel_plan tools, and the approval selector — but it used to live as four exported booleans whose transitions were field assignments scattered across runtime (loop.go set Presented, chat.go cleared Applying, invariants lived as comments). Every wrong-plan/clobber/stale-flag incident traced back to a write the package could not guard.
Now the Controller's fields are unexported and its named transitions are the only way to move it: the compiler rejects outside writes, illegal states like "Active and Applying at once" are unrepresentable, and each transition's guard is a table-tested rule instead of a convention.
Transitions return Effects (data) instead of performing side effects: the machine never touches the session, the observer, the event log, or the disk. The runtime applies Effects AFTER the machine's lock is released, so the lock can never wrap a printf/observer callback — the old "don't hold s.mu across EnterPlan" deadlock trap is gone structurally, and so is the unlocked Task/LastPlan reset race (every read and write now goes through c.mu).
Index ¶
- func PlanShaped(text string) bool
- type ApproveOutcome
- type Controller
- func (c *Controller) ApplyAborted()
- func (c *Controller) ApplyContract() string
- func (c *Controller) ApplyDone()
- func (c *Controller) Approve(lastTextFallback string) (Effects, ApproveOutcome)
- func (c *Controller) BeginTurn()
- func (c *Controller) Cancel() Effects
- func (c *Controller) ConsumeCommitGate() bool
- func (c *Controller) Enter(currentModel string, opts ...Opt) Effects
- func (c *Controller) Epoch() int
- func (c *Controller) InFlow() bool
- func (c *Controller) IsApplying() bool
- func (c *Controller) NotePlanTurn(hasOutput bool) Effects
- func (c *Controller) NoteReflect() int
- func (c *Controller) Phase() Phase
- func (c *Controller) PhaseEpoch() (Phase, int)
- func (c *Controller) Planning() bool
- func (c *Controller) Present(text string) (Effects, PresentOutcome)
- func (c *Controller) Presentable() bool
- func (c *Controller) RecordSaveSlug(slug string)
- func (c *Controller) ReflectRounds() int
- func (c *Controller) ResearchModel() string
- func (c *Controller) ResolveCommitGate()
- func (c *Controller) Revision() int
- func (c *Controller) SetPlannerModel(model string)
- func (c *Controller) SetResearchModel(model string)
- func (c *Controller) Slug() string
- func (c *Controller) Snapshot() (task, draft string)
- func (c *Controller) Yolo() bool
- type Effects
- type Opt
- type Phase
- type PresentOutcome
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func PlanShaped ¶
PlanShaped reports whether text carries a plan's mandated skeleton: at least two numbered step lines. A "simplify the plan" revision keeps its numbered steps no matter how short it gets; a prose answer to a follow-up has none.
Types ¶
type ApproveOutcome ¶
ApproveOutcome carries the selected apply contract. Armed=false means no contract existed (nothing pinned and no fallback) — the lifecycle returns to Idle and nothing must execute.
type Controller ¶
type Controller struct {
// contains filtered or unexported fields
}
Controller owns the plan-mode lifecycle. Held as a pointer on Session (s.planCtl); the zero value is a ready Idle machine. All fields are unexported ON PURPOSE — state moves only through the transition methods below, and reads go through the locked accessors. Nil-receiver-safe reads let callers skip the `planCtl != nil` dance.
func (*Controller) ApplyAborted ¶
func (c *Controller) ApplyAborted()
ApplyAborted clears an armed-but-never-run contract (commit-gate abort, Esc in the approval window): Applying → Idle.
func (*Controller) ApplyContract ¶
func (c *Controller) ApplyContract() string
ApplyContract returns the armed contract text ("" outside Applying).
func (*Controller) ApplyDone ¶
func (c *Controller) ApplyDone()
ApplyDone ends the apply turn: Applying → Idle, contract cleared. It is one of exactly two clears of the apply state (with ApplyAborted) — the raw tuple-assignment clears that used to live in runtime are gone.
func (*Controller) Approve ¶
func (c *Controller) Approve(lastTextFallback string) (Effects, ApproveOutcome)
Approve is the /execute transition: Researching|Presented → Applying, arming the apply contract. Contract selection lives here — the PINNED plan wins; lastTextFallback (the session's last rendered text) is used only when nothing was ever pinned. No contract at all → Idle with Armed=false (nothing may execute). From Idle/Applying → no-op (executePlanTool turns that into a model-facing error; re-arming mid-apply is structurally impossible).
func (*Controller) BeginTurn ¶
func (c *Controller) BeginTurn()
BeginTurn is the per-turn reset: a presented plan goes back to Researching at the top of the next plan-mode turn, so an interrupted turn (Ctrl-C on a clarifying question) never leaves a stale "Plan ready" selector armed with nothing rendered behind it. No-op in every other phase.
func (*Controller) Cancel ¶
func (c *Controller) Cancel() Effects
Cancel abandons the plan session: Researching|Presented → Idle. lastPlan and slug survive (recall_plan still works; only Enter wipes them). No-op outside plan mode — Cancel is called defensively from several TUI paths.
func (*Controller) ConsumeCommitGate ¶
func (c *Controller) ConsumeCommitGate() bool
ConsumeCommitGate consumes the one-shot: true exactly once per arm.
func (*Controller) Enter ¶
func (c *Controller) Enter(currentModel string, opts ...Opt) Effects
Enter starts a plan session: Idle → Researching. Already planning or applying → silent no-op (zero Effects): double /plan and enter_plan-while-planning are normal user races, and entering during an apply would corrupt the lifecycle. currentModel is saved for restore at exit.
func (*Controller) Epoch ¶
func (c *Controller) Epoch() int
Epoch identifies the current plan session for async-verdict staleness checks.
func (*Controller) InFlow ¶
func (c *Controller) InFlow() bool
InFlow reports any non-Idle phase (planning or applying).
func (*Controller) IsApplying ¶
func (c *Controller) IsApplying() bool
IsApplying reports the apply phase — the old Applying boolean.
func (*Controller) NotePlanTurn ¶
func (c *Controller) NotePlanTurn(hasOutput bool) Effects
NotePlanTurn records a proposed/revised plan after a planning turn that produced output, advancing the revision counter for the memory trail.
func (*Controller) NoteReflect ¶
func (c *Controller) NoteReflect() int
NoteReflect counts a reflection-gate research round; returns the new count for the status line. No-op (returning the current count) outside plan mode.
func (*Controller) Phase ¶
func (c *Controller) Phase() Phase
Phase returns the current lifecycle phase.
func (*Controller) PhaseEpoch ¶
func (c *Controller) PhaseEpoch() (Phase, int)
PhaseEpoch returns phase and epoch atomically — the pair the intake gate stamps on a submission so an async verdict can be staleness-checked later.
func (*Controller) Planning ¶
func (c *Controller) Planning() bool
Planning reports plan mode proper (Researching or Presented) — the old Active.
func (*Controller) Present ¶
func (c *Controller) Present(text string) (Effects, PresentOutcome)
Present lands a synthesis: Researching|Presented → Presented. The pin guard lives HERE: the contract is replaced only when nothing is pinned yet or the synthesis is plan-shaped — a conversational answer to a related follow-up must never overwrite the contract /execute will run (the clobber bug). When the pin is replaced, Effects.SavePlan carries the text for the durable copy.
func (*Controller) Presentable ¶
func (c *Controller) Presentable() bool
Presentable reports that the most recent plan turn landed an approvable plan — the TUI raises the selector on it.
func (*Controller) RecordSaveSlug ¶
func (c *Controller) RecordSaveSlug(slug string)
RecordSaveSlug records the saved-plan filename slug minted when the plan was first written to the plans store — reused across revisions so one plan session is one file.
func (*Controller) ReflectRounds ¶
func (c *Controller) ReflectRounds() int
ReflectRounds returns the reflection-gate round count for this session.
func (*Controller) ResearchModel ¶
func (c *Controller) ResearchModel() string
ResearchModel returns the scout-model override ("" = none).
func (*Controller) ResolveCommitGate ¶
func (c *Controller) ResolveCommitGate()
ResolveCommitGate arms the commit-before-work one-shot: the plan selector's commit choice was answered, so the apply turn's gate must not re-ask. Armed only while a plan is on the table (or already approved — the selector's Execute row fires ExitPlan first in some flows).
func (*Controller) Revision ¶
func (c *Controller) Revision() int
Revision returns how many times the current plan has been revised.
func (*Controller) SetPlannerModel ¶
func (c *Controller) SetPlannerModel(model string)
SetPlannerModel records the reasoning model for the executive loop + synthesis.
func (*Controller) SetResearchModel ¶
func (c *Controller) SetResearchModel(model string)
SetResearchModel records the cheaper model for explore() scout sub-agents.
func (*Controller) Slug ¶
func (c *Controller) Slug() string
Slug returns this plan session's saved-plan filename slug ("" until first saved).
func (*Controller) Snapshot ¶
func (c *Controller) Snapshot() (task, draft string)
Snapshot returns the plan anchor task + pinned draft atomically — what the intake classifier judges relevance against.
func (*Controller) Yolo ¶
func (c *Controller) Yolo() bool
Yolo reports the auto-resolve/auto-execute flag for this plan session.
type Effects ¶
type Effects struct {
SetModel string // non-empty → sess.SetModel (planner on Enter, saved on Approve/Cancel)
Emit events.Kind // non-empty → emit with EmitPayload
EmitPayload map[string]any
ClearTodos bool // Enter: a new plan is a new unit of work — drop the prior checklist
SavePlan string // Present, only when the pin was actually replaced (durable ~/.memcode/plans copy)
}
Effects is what a transition asks the caller to do. The machine returns it under no lock obligation on the caller's side; runtime applies it via Session.applyPlanEffects after the transition returns.
type Opt ¶
type Opt func(*Controller)
Opt is a functional option for Enter — the hook for plan-scoped transient state. Enter resets everything first, then applies opts, so a stale flag can never leak into the next plan session; after unexporting, opts are the ONLY write path to task/yolo, which makes reset-before-opts structural.
type Phase ¶
type Phase int
Phase is the plan lifecycle's single word of truth. Exactly one holds at any moment; the zero value is Idle so a zero Controller is out of plan mode.
type PresentOutcome ¶
type PresentOutcome struct{ Pinned bool }
PresentOutcome reports whether Present replaced the pinned contract. A non-plan-shaped synthesis still presents (the selector raises over the OLD pin, which is still valid) but does not replace it.