plan#

import "github.com/jbcom/radioactive-ralph/internal/plan"

Package plan implements the heuristic markdown plan engine described in docs/superpowers/specs/2026-07-16-supervisor-architecture-design.md §11.

Plans are markdown documents parsed with goldmark into an AST and decomposed heuristically – no LLM, no structured output, no vectors. The grammar:

  • A heading of level N is a nesting group. Its “section” runs from the heading to the next heading of level <= N.

  • Heading order encodes group dependency: “# Do first” then “# Do next” means the first group completes before the second (sequential groups at the same level).

  • Under a leaf heading (no child subheadings in its section): an unordered list is parallelizable steps; an ordered list is sequential steps. A step may carry paragraphs of detail (bullets + paragraphs together form one step with detail).

  • Do not descend past a heading that has child subheadings – the subheadings carry the ordering.

Index#

func DecomposeRefs#

func DecomposeRefs(p *Plan, done map[string]bool) (readyNow []Step, refs []StepRef, parallel bool)

DecomposeRefs is like Decompose but also returns the StepRef for each ready step, in the same order, so a caller can mark individual steps done via StepRef.ID() without recomputing positions.

func Slug#

func Slug(title string) string

Slug lower-cases a title and collapses every run of non-alphanumeric characters into a single hyphen, trimming leading/trailing hyphens — a stable, filesystem-and-URL-safe plan slug. Returns “plan” for an all-punctuation/empty title.

func Title#

func Title(markdown, fallback string) string

Title returns the plan markdown’s first level-1 heading, or fallback (e.g. a filename sans extension) when there is none or it is blank. Shared by the `plan import` CLI and the supervisor’s plan-import IPC handler so both derive identical titles.

type Group#

Group is a single heading’s section. A Group either carries Steps (it is a leaf: no child subheadings appear in its section) or SubGroups (it has child subheadings, which carry the ordering) – never both.

type Group struct {
    // Heading is the trimmed text of the heading line.
    Heading string

    // Level is the heading level (1-6).
    Level int

    // Parallel is true when this leaf group's steps come from an
    // unordered list (dispatchable together). It is false for an
    // ordered-list leaf (steps run one at a time) and is meaningless
    // (left false) for a non-leaf group.
    Parallel bool

    // Steps holds this leaf group's steps in document order. Empty for
    // a non-leaf group.
    Steps []Step

    // SubGroups holds this group's child subheadings in document order.
    // Empty for a leaf group.
    SubGroups []Group
}

type Plan#

Plan is the parsed, nested representation of a plan document.

type Plan struct {
    // Groups holds the top-level (heading level 1) groups in document
    // order. Document order is dependency order: Groups[0] completes
    // before Groups[1] starts, and so on.
    Groups []Group
}

func Parse#

func Parse(md []byte) (*Plan, error)

Parse parses plan markdown into a Plan. Parse uses goldmark’s core parser only (block + inline); GFM extensions (tables, strikethrough, autolinks, task-list checkboxes, etc.) are deliberately not enabled – the plan grammar is intentionally small.

func (*Plan) StepAt#

func (p *Plan) StepAt(ref StepRef) (Step, Group, error)

StepAt resolves a StepRef back to its Step and owning Group, primarily for callers that received a StepRef from DecomposeRefs and need to re-fetch the current Step/Group (e.g. after a re-parse).

func (*Plan) StepIDs#

func (p *Plan) StepIDs() []string

StepIDs returns the stable ID (see StepRef.ID) for every step in the plan, in document order. This is the full universe of valid keys for a done-set map, and is useful for validating/seeding one.

type PlanError#

PlanError describes one advisory ambiguity found in a plan document. Line is 1-based, matching editor conventions; it is 0 when the finding applies to the document as a whole rather than one location.

part of this package’s specified public API (see the Phase 6a plan engine grammar); calling it plan.Error would collide with the “Error() string” convention for the error interface, which this type deliberately does not implement (findings are advisory, not errors).

type PlanError struct {
    Line int
    Msg  string
}

func Validate#

func Validate(md []byte) []PlanError

Validate parses md and flags grammar ambiguities the heuristic decomposer (Parse/Decompose) has to guess through. Validate is advisory: it never blocks Parse from running, but a plan with findings is a plan whose dispatch order may not be what its author intended. Findings are:

  • a section with both a list and a leading bare paragraph, which is ambiguous under the disambiguation rule (list => step-group, bare paragraph with no list => narrative) when the paragraph precedes the list and could be misread as an intended first step;

  • a section that mixes an ordered and an unordered list – Parse picks the first list’s orderedness for Group.Parallel and silently folds the rest in, which is very likely not what the author meant;

  • an empty group: a heading whose section (recursing into subheadings) has no steps at all.

func (PlanError) String#

func (e PlanError) String() string

type Step#

Step is a single unit of work: the list item text plus any trailing paragraph(s) of detail found alongside the list under the same heading.

type Step struct {
    // Text is the trimmed text of the list item itself, with any recognized
    // trailing marker (see RequiresApproval) stripped off.
    Text string

    // Detail is the trimmed, newline-joined text of any paragraphs found
    // in the same section as the list (narrative elaborating the step).
    // Empty when there is no such detail.
    Detail string

    // RequiresApproval is true when the step carries the `[approval]` marker
    // (case-insensitive, at the end of the list-item text). Such a step is
    // materialized as a task in status 'ready_pending_approval': it is held
    // out of dispatch until an operator approves it (GUI/IPC ApproveTask),
    // which transitions it to 'ready' so it becomes claimable. This is the
    // human-in-the-loop gate — the producer for the approval flow the
    // observe/drive surface already exposes.
    RequiresApproval bool
}

func Decompose#

func Decompose(p *Plan, done map[string]bool) (readyNow []Step, parallel bool)

Decompose computes the PRESENT: what is dispatchable right now, given the plan structure and a done-set keyed by StepRef.ID().

It walks groups in document order (document order encodes dependency: an earlier group must complete before a later one starts). Within the first not-fully-done group:

  • if it has subgroups, Decompose recurses into the first incomplete subgroup (subheadings carry the ordering, so earlier subgroups gate later ones exactly like top-level groups do);

  • at a leaf, if the group is Parallel, every not-done step is returned together (they are dispatchable concurrently); otherwise (sequential) only the first not-done step is returned, since later steps depend on it completing.

Decompose returns (nil, false) when every step in the plan is done.

type StepRef#

StepRef identifies one Step’s position in a Plan: the path of zero-based indices through Group/SubGroups from the plan root, followed by the zero-based index into that leaf Group’s Steps.

type StepRef struct {
    GroupPath []int
    Index     int
}

func (StepRef) ID#

func (r StepRef) ID() string

ID returns a stable, deterministic string key for this step, suitable for use in a done-set. It is derived purely from position in the plan tree (e.g. “0.1.2”), not from step text, so it stays stable across re-parses of the same document and is independent of wording edits that don’t change structure.

Generated by gomarkdoc