loopcommand

package
v0.15.0 Latest Latest
Warning

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

Go to latest
Published: Aug 29, 2026 License: MIT Imports: 15 Imported by: 0

Documentation

Overview

Package loopcommand is the /loop façade over the agent loop's done definition. It parses a `tasks.md`-shaped directory into a LoopSpec and returns it as a loop.DoneSet, without changing the turn cycle.

Nothing in the product calls this yet: the client-side recognition of `/loop` is Step 3 of the family's `.i` and has not been built. What ships here is the parser and the dispatch between sources, and the package is honest about that rather than reading as a delivered command.

Spec: docs/specs/architecture/loop-command/202608252000-loop-command.*.spec.md

Index

Constants

View Source
const SpecsDirName = "specs"

SpecsDirName is where spec folders are looked for. One place, not a search: walking a repository for anything that resembles a spec finds node_modules.

Variables

This section is empty.

Functions

func Load

func Load(workspace, specPath string, src Source, verifyCommand string) (loop.DoneSet, error)

Load resolves the source and returns the DoneSet.

Same inputs → same DoneSet. Reading the filesystem is the only I/O.

The verifyCommand is consulted by SourceAuto only when neither done.toml nor a LoopSpec is available. Legacy behaviour preserved.

func SessionConfig

func SessionConfig(opts SessionOptions) (loop.Config, string)

SessionConfig builds the loop.Config a /loop session is born with, and the name that session will carry.

It creates nothing. The name says so, because the name that was here before — NewSession — promised a session, and the `.p` it was written against declared it as `NewSession(ctx, srv, opts) (SessionHandle, error)`: a call that reaches the server, hands back a handle with ID() and SubmitTurn(), and owns the session's lifetime. None of that exists. A constructor that constructs nothing is the kind of name a reader trusts once and then stops trusting the package.

Creating the session, running the turn and closing it afterwards belong to the caller that does not exist yet — Step 3 of the family's `.i`. What can be built without a client is the Config that caller will pass, and this is it.

Types

type Found added in v0.12.0

type Found struct {
	// Path is relative to the workspace, as a person would type it.
	Path string
	// Criteria is how many the folder declares.
	Criteria int
	// Unmet is how many did not pass. Zero with Criteria above zero is a spec
	// that is done.
	Unmet int
	// Unavailable is how many could not be run at all.
	Unavailable int
	// Err is why the folder could not be read, when it could not.
	Err string
}

Found is one spec folder and where it stands.

Where it STANDS, not what it declares. "Pending" cannot be answered by counting checkboxes in a tasks.md — a box is marked by whoever felt like marking it. It is answered by running the folder's own criteria, which is the only definition of done this product recognises.

func Discover added in v0.12.0

func Discover(ctx context.Context, workspace string, run loop.CriterionRunner, timeout time.Duration) []Found

Discover lists the spec folders under a workspace and runs each one's criteria to find out where it stands.

It runs them. There is no cheaper honest answer to "which specs are pending": the criteria are the definition of done, and anything else — checkbox counts, file timestamps, a marker in the spec — is a second statement of something that can move.

Ordered by folder name, which for a dated spec is chronological. The sequencing is the operator's, and a list that reshuffles between runs is one nobody can act on.

func (Found) Pending added in v0.12.0

func (f Found) Pending() bool

Pending says whether there is work here.

A folder that declares nothing is pending: there is no evidence it is finished, and treating "nothing to check" as "done" is the defect this whole family exists to prevent. A folder whose criteria all pass is not.

type LoopSpec

type LoopSpec struct {
	// Path is the directory containing tasks.md.
	Path string

	// Criteria are the verifiable conditions of done for this spec.
	// Sourced exclusively from tasks.md. Prose is not a source.
	Criteria []loop.Criterion

	// Protected are paths the agent may not modify silently during execution.
	// Sourced from the user (frontmatter or argument), not inferred from
	// where the spec lives.
	Protected []string
}

LoopSpec is a parsed specification external to the workspace.

The parser is total: it never panics on malformed input and never invents data. What it CANNOT read, it refuses to read — a file with no task list is an error, not an empty DoneSet. The difference matters more than it looks: an empty DoneSet is "nothing to verify", which the agent loop reports as done. Silently turning an unreadable file into "done" is the worst outcome this package can produce, and RN-6 exists to forbid it.

func LoadSpec

func LoadSpec(path string) (LoopSpec, error)

LoadSpec reads a LoopSpec from a directory containing tasks.md. Returns an error on malformed input or a missing file.

func LoadSpecWithProtect

func LoadSpecWithProtect(path string, protect []string) (LoopSpec, error)

LoadSpecWithProtect reads a LoopSpec and adds the argument's globs to any declared in the file.

The two are a union, not a precedence. Protected is a list of paths whose modification gets surfaced, so "the file wins over the argument" would mean an argument could REMOVE a protection the file asked for — the one direction that must never be reachable by accident.

func (LoopSpec) DoneSet

func (s LoopSpec) DoneSet() loop.DoneSet

DoneSet converts a LoopSpec into the loop.DoneSet the engine consumes. Criteria == nil becomes DoneSet with no criteria, which the agent-loop reports as "no definition of done" — not an error.

type Options

type Options struct {
	SpecPath      string
	Source        Source
	Protect       []string
	SessionPrefix string
}

Options holds the four loop.* settings the daemon reads from configuration.

Typed surface for the `loop.spec_path`, `loop.source`, `loop.protect` and `loop.session_prefix` keys in KnownKeys. Keys are read by name in OptionsFromConfig so the wiring guard sees them in source.

func OptionsFromConfig

func OptionsFromConfig(values map[string]string) Options

OptionsFromConfig builds an Options from the resolved layer values.

The wiring guard looks for the literal key names ("loop.spec_path" etc.) in source; this is the function that reads them. Keeping the literals here is what makes the configuration surface checkable — a key declared in KnownKeys and not referenced by this name in any consumer would fail the wiring test even with this function present.

type SessionOptions

type SessionOptions struct {
	Spec          LoopSpec
	Limits        loop.Limits
	MaxStall      int
	DoneTimeout   time.Duration
	SandboxMode   policy.SandboxMode
	Workspace     string
	Model         string
	SessionPrefix string
}

SessionOptions configures the dedicated session /loop will create.

The session is born with the DoneSet already in place: this is what guarantees the engine sees the same definition of done the user typed /loop for, regardless of any later session-level mutation.

type Source

type Source int

Source declares where a DoneSet comes from.

SourceAuto preserves the historical behaviour: done.toml if it exists, else the legacy verifyCommand. SourceDoneFile forces done.toml. SourceLoopSpec forces a LoopSpec — failure to load is an error, not a fallback to legacy.

const (
	SourceAuto Source = iota
	SourceDoneFile
	SourceLoopSpec
)

func ParseSource

func ParseSource(s string) (Source, error)

ParseSource turns the textual value (DCODE_LOOP_SOURCE or a flag) into a Source. Anything else is an error — silent defaulting would hide a typo the same way a misread configuration would.

Jump to

Keyboard shortcuts

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