exec

package
v0.0.15 Latest Latest
Warning

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

Go to latest
Published: Jul 8, 2026 License: MIT Imports: 10 Imported by: 0

Documentation

Overview

Package exec is marunage's backend-agnostic execution layer. It hides how a Claude session is launched, fed a prompt, and watched for completion behind a small interface so the rest of marunage (dispatch / reflection / reaper / web) never speaks a concrete backend's vocabulary. cmux is the first implementation (internal/exec/cmux); tmux / local-process / docker land in later PRs without touching any consumer.

Design (docs/redesign_layering.md §4): every backend MUST implement the three Executor methods — that is the minimum a "fire-and-forget" run needs. Richer abilities (human attach, live output) differ per backend and are expressed as optional capability interfaces a caller type-asserts for, mirroring the io.WriterTo / http.Flusher idiom:

if a, ok := executor.(exec.Attachable); ok {
    link, _ := a.Attach(ctx, sess)
}

Index

Constants

View Source
const SentinelFile = ".exit_code"

SentinelFile is the atomic exit-code file a dispatched Claude writes when it finishes (echo $? > .exit_code.tmp && mv .exit_code.tmp .exit_code). Every backend whose AwaitExit is sentinel-based polls this name, so the constant lives in the shared exec package rather than being re-declared per backend. It must agree with the filename the dispatcher's prompt embeds.

Variables

View Source
var (
	// ErrAwaitTimeout is returned by AwaitSentinel when the await timeout
	// elapses before the sentinel appears. Distinct from context
	// cancellation so a caller can tell "the session is taking too long"
	// from "we were asked to stop".
	ErrAwaitTimeout = errors.New("exec: timed out waiting for exit sentinel")

	// ErrNoSentinelDir is returned by AwaitSentinel when no control
	// directory was wired, so there is no file to poll. Surfacing it loudly
	// keeps a mis-wired caller from silently blocking forever.
	ErrNoSentinelDir = errors.New("exec: session has no sentinel directory")
)

Functions

func AwaitSentinel

func AwaitSentinel(ctx context.Context, dir string, pollInterval, awaitTimeout time.Duration) (int, error)

AwaitSentinel polls dir/.exit_code until it appears, ctx is cancelled, or awaitTimeout elapses. It is the backend-agnostic completion mechanism the sentinel-based executors (cmux, tmux) share: the dispatched Claude writes the exit code atomically, so a reader sees either the final value or no file at all. The parsed exit code is returned even when non-zero; a non-nil error is reserved for I/O / timeout / cancellation. An awaitTimeout of zero means "no cap" (wait until ctx is cancelled).

func ReadExitCode

func ReadExitCode(path string) (int, bool, error)

ReadExitCode performs a single bounded, symlink-refusing read of the exit sentinel at path. It returns (code, true, nil) once the file is present and parses, (0, false, nil) while it is still absent, and a non-nil error only for a genuine I/O / parse failure. The O_NOFOLLOW + size cap harden the read so a prompt-injected Claude cannot make the reader follow a symlink off the control dir or slurp a huge file.

Types

type Attachable

type Attachable interface {
	Attach(ctx context.Context, s Session) (deeplink string, err error)
}

Attachable is the optional capability for backends that can hand a human an interactive view of a running session. The returned string is opaque attach instructions: cmux returns a URI deeplink, tmux returns a runnable `tmux attach` command. A consumer must render it as plain text rather than assume a clickable URI. A bare local process cannot attach and simply does not implement this interface.

type Executor

type Executor interface {
	// Start launches an isolated session in spec.Cwd and runs spec.Command
	// inside it (the claude invocation derived from the permission mode).
	//
	// Readiness is part of Start: when it returns a nil error the session
	// is ready to accept Send. When the session was created but never
	// became ready, Start returns a non-nil error AND a Session whose ID
	// is populated, so the caller can tell "nothing was created, retry"
	// (empty ID) apart from "a session leaked, mark it failed and let the
	// reaper reclaim it" (non-empty ID). A failure before anything was
	// created returns the zero Session.
	Start(ctx context.Context, spec SessionSpec) (Session, error)

	// Send delivers prompt to an already-started session. Newline folding
	// (so a multi-line prompt is not submitted line-by-line) and any
	// submit keystroke are the implementation's concern.
	Send(ctx context.Context, s Session, prompt string) error

	// AwaitExit blocks until the session's process exits and returns its
	// exit code. The completion mechanism (cmux's atomic .exit_code
	// sentinel, a local cmd.Wait, ...) is hidden behind this method so
	// callers never poll a backend-specific artefact themselves.
	AwaitExit(ctx context.Context, s Session) (int, error)
}

Executor is the backend-agnostic minimum every execution target must provide. A backend that satisfies only this interface still supports marunage's core "dispatch a task, wait for it to finish" loop.

type Lister

type Lister interface {
	ListSessions(ctx context.Context) ([]Session, error)
}

Lister is the optional capability for backends that can enumerate the sessions they currently consider live. The reaper diffs this against the running rows in tasks.db to detect sessions that vanished out from under marunage (invariant #5 "Crash safety").

type OutputReader

type OutputReader interface {
	ReadOutput(ctx context.Context, s Session) (string, error)
}

OutputReader is the optional capability for backends that can return a point-in-time snapshot of a session's visible output. The web live- stream endpoint polls this and emits a diff; it is split from Streamable so a poll-based consumer does not have to manage a channel lifecycle.

type Session

type Session struct {
	ID string
	// contains filtered or unexported fields
}

Session is the opaque handle a backend returns from Start (and Lister). ID is the backend's stable reference (cmux's "workspace:NNN"); handle carries any richer backend-internal value the public API should not expose. Callers pass Session back into Send / AwaitExit / capabilities verbatim.

func NewSession

func NewSession(id string, handle any) Session

NewSession builds a Session from a backend reference and an optional backend-internal handle. Consumers that already hold a stored session id (dispatch re-sending, the web send endpoint) use NewSession(id, nil) to address an existing session without going through Start.

func (Session) Handle

func (s Session) Handle() any

Handle returns the backend-internal value stashed at construction, or nil. Only the backend that created the Session knows the concrete type; it type-asserts the value back when it needs it.

type SessionSpec

type SessionSpec struct {
	// Cwd is the working directory the session is launched in.
	Cwd string
	// Command is the literal command line the session runs (the claude
	// invocation derived from the configured permission mode).
	Command string
	// Name is the human-readable label a backend shows in its dashboard.
	Name string
	// Env are extra environment variables for the session. Optional;
	// nil/empty means "inherit the launcher's environment unchanged".
	Env map[string]string
}

SessionSpec is the backend-agnostic launch request. It deliberately carries no cmux vocabulary: a backend translates these fields into its own create call (cmux maps Cwd/Command/Name onto NewWorkspaceOptions).

type Streamable

type Streamable interface {
	Stream(ctx context.Context, s Session) (<-chan []byte, error)
}

Streamable is the optional capability for backends that can publish a session's live terminal output. The returned channel delivers output chunks until it is closed (session gone or ctx cancelled).

Directories

Path Synopsis
Package backend is the one place that maps the [execution].executor config value onto a concrete exec.Executor.
Package backend is the one place that maps the [execution].executor config value onto a concrete exec.Executor.
Package cmux adapts marunage's existing cmux client (internal/cmux) to the backend-agnostic exec.Executor contract.
Package cmux adapts marunage's existing cmux client (internal/cmux) to the backend-agnostic exec.Executor contract.
Package exectest holds the backend-agnostic conformance suite every exec.Executor implementation must pass.
Package exectest holds the backend-agnostic conformance suite every exec.Executor implementation must pass.
Package herdr adapts the herdr CLI (https://herdr.dev, github.com/ogulcancelik/herdr — "tmux for agents") to marunage's backend-agnostic exec.Executor contract.
Package herdr adapts the herdr CLI (https://herdr.dev, github.com/ogulcancelik/herdr — "tmux for agents") to marunage's backend-agnostic exec.Executor contract.
Package local implements exec.Executor by launching the claude command as a direct child process (os/exec) instead of inside a cmux workspace or tmux pane.
Package local implements exec.Executor by launching the claude command as a direct child process (os/exec) instead of inside a cmux workspace or tmux pane.
Package tmux adapts the system tmux CLI to marunage's backend-agnostic exec.Executor contract.
Package tmux adapts the system tmux CLI to marunage's backend-agnostic exec.Executor contract.

Jump to

Keyboard shortcuts

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