P0: stand up executus harness module above majordomo
executus CI / test (push) Failing after 24s

Batteries-included agent-harness base, extracted from mort's agent layer.
This first cut establishes the module + the zero-coupling core primitives:

- lane, dispatchguard, pendingattach, run/progress.go: moved verbatim from mort
- config: host config Source seam + env-var default (nil-safe helpers)
- deliver: output-egress seam + Discard/Stdout defaults
- identity: AdminPolicy + MemberResolver seams (nil-safe)
- fanout: programmatic N×M swarm (bounded global + per-key concurrency)
- README/CLAUDE.md with the vibe-coded banner; CI with Go gates +
  the "core stays majordomo+stdlib only" invariant

Core builds with stdlib only today; majordomo enters at P1 (model/structured).
go build/vet/test -race all green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-26 19:18:37 -04:00
parent 25feb63c00
commit ca243a2d50
31 changed files with 5042 additions and 18 deletions
+196
View File
@@ -0,0 +1,196 @@
package lane
import (
"context"
"sync"
)
// ConvarReader is the narrow surface the registry uses to read
// per-lane concurrency caps from convars at startup and on Reload.
//
// Why an interface (not pkg/convar directly): registry is a generic
// primitive and shouldn't import the application convar package.
// Production wires a thin adapter; tests pass a fake.
type ConvarReader interface {
Int(ctx context.Context, name string, def int) int
}
// ConvarReaderFunc adapts a closure into a ConvarReader.
type ConvarReaderFunc func(ctx context.Context, name string, def int) int
// Int satisfies ConvarReader.
func (f ConvarReaderFunc) Int(ctx context.Context, name string, def int) int {
if f == nil {
return def
}
return f(ctx, name, def)
}
// Registry is a manager of named lanes. The default policy is
// fair-share; lanes are created lazily on first GetOrCreate, with
// concurrency read from convar `lanes.<name>.max_concurrent` (default
// 1). Reload re-reads convars and updates each lane's MaxConcurrent
// in place — useful for runtime tuning without losing in-flight work.
//
// Why a singleton-ish manager vs constructing lanes ad-hoc: the
// registry is the integration point where mort.go wires lanes once
// and every subsystem (LLM transport, skill runner) looks them up by
// name. Lazy creation lets the registry stay schema-free — adding a
// new lane is just "ask for it by name".
//
// Test: registry_test.go covers GetOrCreate identity, convar read,
// and Reload.
type Registry struct {
mu sync.RWMutex
lanes map[string]Lane
convars ConvarReader
// policyFactory is the queue policy constructor used for new
// lanes. Defaults to NewFairSharePolicy. Tests substitute FIFO
// when they want deterministic ordering.
policyFactory func() queuePolicy
}
// NewRegistry constructs a registry. convars may be nil — lanes
// fall back to the registry's default concurrency (1).
func NewRegistry(convars ConvarReader) *Registry {
return &Registry{
lanes: make(map[string]Lane),
convars: convars,
policyFactory: NewFairSharePolicy,
}
}
// SetPolicyFactory overrides the default policy used for new lanes.
// Existing lanes are unchanged. Used by tests; production keeps the
// fair-share default.
func (r *Registry) SetPolicyFactory(f func() queuePolicy) {
if f == nil {
f = NewFairSharePolicy
}
r.mu.Lock()
r.policyFactory = f
r.mu.Unlock()
}
// Get returns the named lane or nil if it has not been created.
// Useful in admin/UI code that wants to show only existing lanes
// without creating new ones as a side effect.
func (r *Registry) Get(name string) Lane {
r.mu.RLock()
defer r.mu.RUnlock()
return r.lanes[name]
}
// StatsReader is the read-only stats surface exposed to admin / user
// dashboards (Discord queue commands, /skills/admin/queues web view).
// *Registry satisfies it; tests substitute a fake.
//
// Why a narrow interface (vs passing *Registry around): the consumers
// only need stats and lane lookup — no creation or mutation surface.
// Keeping the dep narrow makes mocks trivial in webui + skills tests.
type StatsReader interface {
// List returns a snapshot of every registered lane.
List() []Lane
// Lookup returns the lane by name, or nil. Mirrors Registry.Get
// (named differently to avoid the "Get" verb confusion in
// dashboards that primarily call Stats).
Lookup(name string) Lane
}
// Lookup satisfies the StatsReader surface alongside Registry.Get. We
// expose both verbs so the dashboard code reads naturally without
// forcing existing call sites that use Get() to migrate.
func (r *Registry) Lookup(name string) Lane { return r.Get(name) }
// GetOrCreate returns the named lane, creating it lazily on first
// call. Concurrency is read from convar `lanes.<name>.max_concurrent`
// (default 1). The policy is the registry's policy factory (default
// fair-share).
//
// Why convar name `lanes.<name>.max_concurrent` (not
// `skills.lane.<name>.max_concurrent`): pkg/lane is generic — the
// skills system happens to be the first caller, but the LLM transport
// wrapper (Phase 3) and other future runners will use the same
// registry. The convar namespace `lanes.*` keeps lane configuration
// in one place. The skills system can adopt different convar names
// if it prefers; in that case, mort.go reads them and calls
// SetMaxConcurrent on the resulting lanes after creation.
func (r *Registry) GetOrCreate(ctx context.Context, name string) Lane {
r.mu.RLock()
if l, ok := r.lanes[name]; ok {
r.mu.RUnlock()
return l
}
r.mu.RUnlock()
r.mu.Lock()
defer r.mu.Unlock()
// Double-check after upgrading the lock.
if l, ok := r.lanes[name]; ok {
return l
}
maxConcurrent := r.readConcurrency(ctx, name)
policy := r.policyFactory()
if policy == nil {
policy = NewFairSharePolicy()
}
l := NewWithPolicy(name, maxConcurrent, policy)
r.lanes[name] = l
return l
}
// List returns a snapshot of all registered lanes. Iteration order is
// not guaranteed (Go map randomization).
func (r *Registry) List() []Lane {
r.mu.RLock()
defer r.mu.RUnlock()
out := make([]Lane, 0, len(r.lanes))
for _, l := range r.lanes {
out = append(out, l)
}
return out
}
// Names returns the registered lane names. Used for the admin
// "list all lanes" surface.
func (r *Registry) Names() []string {
r.mu.RLock()
defer r.mu.RUnlock()
out := make([]string, 0, len(r.lanes))
for n := range r.lanes {
out = append(out, n)
}
return out
}
// Reload re-reads convars for every registered lane and calls
// SetMaxConcurrent on each. Existing running jobs continue to run;
// new dispatches respect the updated cap.
//
// Why a manual Reload instead of reading convars at every dispatch:
// dispatch is on the hot path; reading a convar there for every
// queued job is wasteful. A periodic Reload (every minute, say) is
// cheap and good enough for human-driven config changes.
func (r *Registry) Reload(ctx context.Context) {
r.mu.RLock()
defer r.mu.RUnlock()
for name, l := range r.lanes {
n := r.readConcurrency(ctx, name)
l.SetMaxConcurrent(n)
}
}
// readConcurrency reads `lanes.<name>.max_concurrent` with a default
// of 1. Defensive against a nil ConvarReader and against negative
// values (clamped to 1).
func (r *Registry) readConcurrency(ctx context.Context, name string) int {
if r.convars == nil {
return 1
}
n := r.convars.Int(ctx, "lanes."+name+".max_concurrent", 1)
if n <= 0 {
return 1
}
return n
}