headless

package
v1.35.0 Latest Latest
Warning

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

Go to latest
Published: Jul 6, 2026 License: AGPL-3.0 Imports: 14 Imported by: 0

Documentation

Overview

Package headless provides a subprocess-based interface for running claude -p headlessly. It manages session pools for prefix-cache reuse, streaming output, and clean subprocess lifecycle management.

Index

Constants

View Source
const DefaultCallTimeout = 900 * time.Second

DefaultCallTimeout is the default headless call timeout applied when timeout_seconds is 0.

View Source
const MaxCallTimeout = 1800 * time.Second

MaxCallTimeout caps timeout_seconds.

View Source
const MaxDiffSizeReview = 40_000

MaxDiffSizeReview is the maximum number of bytes included in a review prompt diff.

Variables

View Source
var (
	// ErrClaudeNotFound is returned when the claude binary is not in PATH.
	ErrClaudeNotFound = errors.New("claude binary not found in PATH")
	// ErrLLMError is returned when claude exits with code 1 (LLM-level error).
	ErrLLMError = errors.New("claude LLM error (exit 1)")
	// ErrUsageError is returned when claude exits with code 2 (bad usage / bad flags).
	ErrUsageError = errors.New("claude usage error (exit 2)")
	// ErrInterrupted is returned when claude exits with code 130 (SIGINT).
	ErrInterrupted = errors.New("claude interrupted (exit 130)")
)

Error sentinels returned in StreamChunk.Err or from CallBlocking.

AllowedFeatureKeys is the set of feature keys accepted by the MCP-exposed RunHeadlessCall path (server/services/headless_service.go). FeatureKeyTriage is intentionally excluded — triage calls go through BacklogService.TriggerTriage → Pool.CallBlockingWithOptions directly, bypassing the MCP gate. This prevents triage from being triggered via the public headless API.

Functions

func AllowedFeatureKeyList

func AllowedFeatureKeyList() string

AllowedFeatureKeyList returns a sorted comma-separated list of allowed feature keys for use in error messages. Generated from AllowedFeatureKeys to stay in sync.

func DraftPRDescription

func DraftPRDescription(ctx context.Context, pool *Pool, diff, branchName string) (string, error)

DraftPRDescription calls the LLM to draft a pull request description. Diffs longer than maxDiffSizePR bytes are truncated before sending.

func GenerateAcceptanceCriteria

func GenerateAcceptanceCriteria(ctx context.Context, pool *Pool, title, description string) ([]string, error)

GenerateAcceptanceCriteria calls the LLM to generate acceptance criteria. Returns a slice of criterion strings.

func HeadlessReviewSystemPrompt

func HeadlessReviewSystemPrompt() string

HeadlessReviewSystemPrompt returns the system prompt for headless (no-tool) review calls. Requests JSON output so the caller can parse the verdict without tool execution.

func HeadlessTriageSystemPrompt

func HeadlessTriageSystemPrompt() string

HeadlessTriageSystemPrompt returns the stable system prompt for headless triage calls. Requests JSON output so the caller can parse the result without MCP tool execution.

func ReviewSystemPrompt

func ReviewSystemPrompt() string

ReviewSystemPrompt returns the stable system prompt for review gate calls. Exported so session/backlog_lifecycle.go can use it without embedding the prompt inline.

func SetDefaultPool

func SetDefaultPool(p *Pool)

SetDefaultPool sets the package-level default pool. Safe to call concurrently.

func SuggestCommitMessage

func SuggestCommitMessage(ctx context.Context, pool *Pool, diff string) (string, error)

SuggestCommitMessage calls the LLM to generate a Conventional Commit message. Diffs longer than maxDiffSizeCommit bytes are truncated before sending.

func SummarizeBacklogItem

func SummarizeBacklogItem(ctx context.Context, pool *Pool, title, description string) (string, error)

SummarizeBacklogItem calls the LLM to summarize a backlog item. Returns the summary text from the JSON response.

Types

type CallOptions

type CallOptions struct {
	// WorkDir sets the subprocess working directory (for git operations).
	WorkDir string
	// Model overrides the pool's DefaultModel for this call only.
	Model string
	// TimeoutSecs is unused by Pool directly — callers wrap ctx with WithTimeout.
	TimeoutSecs int
}

CallOptions configures an individual pool call with overrides.

type ClaudeRunner

type ClaudeRunner interface {
	// Run starts claude -p with the given args. stdin provides the user prompt so
	// it does not appear in /proc/<pid>/cmdline. Returns a ReadCloser for stdout,
	// a stop function to kill the process, and an error if the process fails to start.
	// The caller must call stop() to release resources even when the ReadCloser is drained.
	Run(ctx context.Context, args []string, stdin io.Reader) (stdout io.ReadCloser, stop func() error, err error)
}

ClaudeRunner abstracts how claude -p subprocesses are started. Implementors: ProcessRunner (real), FakeRunner (tests).

type FakeRunner

type FakeRunner struct {

	// Calls records every set of args passed to Run, in order.
	Calls [][]string
	// contains filtered or unexported fields
}

FakeRunner is a test double for ClaudeRunner. It returns scripted responses and records call arguments for inspection.

When the args contain "--output-format" followed by "json", the response must be valid JSON matching firstCallJSONResult schema:

{"session_id":"...","result":"...","cost_usd":0.0}

Otherwise the response is returned as plain text, line by line.

func NewFakeRunner

func NewFakeRunner(responses ...string) *FakeRunner

NewFakeRunner creates a FakeRunner that returns responses in order. If responses is empty the runner returns an empty string for each call.

func (*FakeRunner) ArgsContainSequence

func (f *FakeRunner) ArgsContainSequence(n int, seq ...string) bool

ArgsContainSequence returns true if the nth call's args contain the given sequence.

func (*FakeRunner) ArgsForCall

func (f *FakeRunner) ArgsForCall(n int) []string

ArgsForCall returns the args recorded for the nth call (0-indexed). Returns nil if call n has not happened yet.

func (*FakeRunner) CallCount

func (f *FakeRunner) CallCount() int

CallCount returns how many times Run has been called.

func (*FakeRunner) HasArg

func (f *FakeRunner) HasArg(arg string) bool

HasArg returns true if any recorded call contains arg.

func (*FakeRunner) Run

func (f *FakeRunner) Run(_ context.Context, args []string, _ io.Reader) (io.ReadCloser, func() error, error)

Run returns the next scripted response (or error). It records args in Calls. stdin is accepted to satisfy the ClaudeRunner interface but is not inspected. The stop function is a no-op.

func (*FakeRunner) SetErrors

func (f *FakeRunner) SetErrors(errs ...error)

SetErrors configures per-call errors. A nil entry means no error for that call.

type FeatureKey

type FeatureKey string

FeatureKey is a named type for feature identifiers. Using a named type (not an alias) prevents accidental string injection at call sites.

const (
	FeatureKeyReview             FeatureKey = "review"
	FeatureKeySummarize          FeatureKey = "summarize"
	FeatureKeyAC                 FeatureKey = "acceptance-criteria"
	FeatureKeyPRDescription      FeatureKey = "pr-description"
	FeatureKeyCommitMessage      FeatureKey = "commit-message"
	FeatureKeyCustom             FeatureKey = "custom"
	FeatureKeyAutonomousFix      FeatureKey = "autonomous_fix"
	FeatureKeyAutonomousApproval FeatureKey = "autonomous_approval"
	FeatureKeyTriage             FeatureKey = "triage"
)

Feature key constants for well-known AI features.

type Pool

type Pool struct {
	// contains filtered or unexported fields
}

Pool manages a map of named LLM feature sessions, providing session reuse for prefix-cache optimization and bounded concurrency.

func DefaultPool

func DefaultPool() *Pool

DefaultPool returns the package-level default pool. Returns nil if SetDefaultPool has not been called.

func NewPool

func NewPool(cfg PoolConfig) (*Pool, error)

NewPool constructs a Pool by looking up the claude binary in PATH, falling back to well-known install locations if PATH lookup fails. Returns ErrClaudeNotFound if the binary is not found anywhere.

func NewPoolWithRunner

func NewPoolWithRunner(cfg PoolConfig, runner ClaudeRunner) *Pool

NewPoolWithRunner constructs a Pool with a custom runner (no PATH lookup). Used in tests to inject a FakeRunner.

func (*Pool) Call

func (p *Pool) Call(ctx context.Context, key FeatureKey, systemPrompt, userPrompt string) (<-chan StreamChunk, error)

Call starts a streaming headless LLM call for the given feature key. It returns a channel that receives StreamChunk values. The channel is closed when the subprocess exits (or the context is cancelled).

The caller should drain the channel until Done=true or Err!=nil.

func (*Pool) CallBlocking

func (p *Pool) CallBlocking(ctx context.Context, key FeatureKey, systemPrompt, userPrompt string) (string, error)

CallBlocking runs a headless LLM call and blocks until the result is complete. Returns the concatenated text from all chunks and the first non-nil error.

func (*Pool) CallBlockingWithOptions

func (p *Pool) CallBlockingWithOptions(ctx context.Context, key FeatureKey, systemPrompt, userPrompt string, opts CallOptions) (string, error)

CallBlockingWithOptions is like CallBlocking but supports WorkDir and Model overrides.

func (*Pool) CallWithOptions

func (p *Pool) CallWithOptions(ctx context.Context, key FeatureKey, systemPrompt, userPrompt string, opts CallOptions) (<-chan StreamChunk, error)

CallWithOptions is like Call but allows overriding model and working directory.

When opts.WorkDir is non-empty a fresh one-shot subprocess is used (bypassing session caching, which is invalid across directory changes). The parent pool's concurrency semaphore is still acquired so WorkDir calls count against the pool-level cap.

When opts.WorkDir is empty, opts.Model is forwarded to the pool's acquireSession so the correct model is used for the first-call (session-initialisation) request.

type PoolClient

type PoolClient interface {
	CallBlockingWithOptions(ctx context.Context, key FeatureKey, systemPrompt, userPrompt string, opts CallOptions) (string, error)
}

PoolClient is the narrow interface BacklogService uses for headless triage calls. Satisfied by *Pool; allows test injection without needing FakeRunner WorkDir support.

type PoolConfig

type PoolConfig struct {
	// MaxCallsPerSession is the maximum number of calls before a session is rotated.
	// Defaults to 25 if zero.
	MaxCallsPerSession int

	// MaxConcurrentSessions is the maximum number of concurrent subprocess calls.
	// Defaults to 5 if zero.
	MaxConcurrentSessions int

	// DefaultModel overrides the claude model used when no model is specified per-call.
	DefaultModel string
}

PoolConfig configures a Pool.

type ProcessRunner

type ProcessRunner struct {
	// contains filtered or unexported fields
}

ProcessRunner implements ClaudeRunner using executor.StartProcess.

func (*ProcessRunner) Run

func (r *ProcessRunner) Run(ctx context.Context, args []string, stdin io.Reader) (io.ReadCloser, func() error, error)

Run starts the claude binary with args and returns a ReadCloser for stdout. stdin provides the user prompt to the subprocess so it does not appear in /proc/<pid>/cmdline. The stop function terminates the subprocess and must always be called.

func (*ProcessRunner) WithWorkDir

func (r *ProcessRunner) WithWorkDir(workDir string) *ProcessRunner

WithWorkDir returns a copy of this ProcessRunner that sets the subprocess working directory to workDir. Used by CallBlockingWithOptions for per-call directory override.

type StreamChunk

type StreamChunk struct {
	Text    string
	Err     error
	Done    bool
	CostUSD float64 // non-zero only on the final chunk from a first-call JSON response
}

StreamChunk is a single unit of output from a headless LLM call.

Jump to

Keyboard shortcuts

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