plan

package
v0.32.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Sep 5, 2026 License: MIT Imports: 4 Imported by: 0

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

Constants

This section is empty.

Variables

This section is empty.

Functions

func PlanShaped

func PlanShaped(text string) bool

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

type ApproveOutcome struct {
	Contract string
	Armed    bool
}

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) PolicyOverride added in v0.31.0

func (c *Controller) PolicyOverride(target string) map[string]any

PolicyOverride reports the per-operation policy for a target, nil when none.

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) 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) SetPolicyOverride added in v0.31.0

func (c *Controller) SetPolicyOverride(target string, fields map[string]any)

SetPolicyOverride attaches policy to the plan currently in flight. Scoped to this operation only; Enter and the terminal transitions clear it.

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.

func (Effects) Zero

func (e Effects) Zero() bool

Zero reports a no-effect result — the signature of a refused (no-op) transition.

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.

func WithTask

func WithTask(task string) Opt

WithTask anchors this plan session to the task text that started it — the intake gate classifies later submissions against this anchor.

func WithYolo

func WithYolo() Opt

WithYolo suppresses human-in-the-loop questions during planning (auto-resolving them with the model's recommended choice); the TUI also auto-executes the plan without showing the approval selector.

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.

const (
	Idle        Phase = iota // no plan lifecycle in flight
	Researching              // plan mode: drafting/revising, no approvable plan THIS turn
	Presented                // plan mode: a synthesis landed this turn — the selector may raise
	Applying                 // an approved contract is armed or executing
)

func (Phase) Planning

func (p Phase) Planning() bool

Planning reports whether the phase is inside plan mode proper (research or presented) — the old `Active` boolean.

func (Phase) String

func (p Phase) String() string

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.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL