workspace

package
v0.9.3 Latest Latest
Warning

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

Go to latest
Published: Jul 30, 2026 License: MIT Imports: 13 Imported by: 0

Documentation

Overview

Package workspace implements root detection and the .spectackle folder contract: every file the server writes lives inside a .spectackle/ folder (root or nested context dirs); the rest of the workspace is never touched.

Root detection order: walk up from the start dir for .spectackle/config.yaml (the root marker — nested context dirs also have .spectackle/ folders, so the folder alone is ambiguous), then for .git (directory or file — a linked git worktree's .git is a file, and it terminates the walk exactly like a real checkout's .git directory does), then fall back to the -root flag.

Index

Constants

View Source
const Dot = ".spectackle"

Dot is the folder name every server write is confined to.

View Source
const GitMergeMethod = "merge"

GitMergeMethod is how a task branch is folded back into Config.Git.Base: always a merge commit. Fixed, not user-configurable — squash and rebase rewrite the branch's commits, and this codebase already treats a task branch's commit history as the record of what CommitCode actually did (TouchedFiles, the unpushed-commits check) rather than something a merge is free to rewrite.

View Source
const SchemaStamp = "v1"

SchemaStamp is injected into every server-written file's frontmatter. It marks the file format of a pre-1.0 codebase: the format may break at any time and the stamp changes with it.

An unknown stamp is still a tool error ("regenerate"), and caches still simply rebuild — but "there is no migration" no longer holds unconditionally: the immediately-preceding stamp is migrated in place on open (see internal/migrate, and the hook in load below). That exception exists because ADR-0013 changed the shape of every record ID, which reaches every workspace already in users' hands; without a path forward the only honest advice would have been to regenerate and lose the history.

Its value is the one internal/migrate produces (migrate.To), and that is asserted by a test rather than left to whoever edits this line next.

Variables

This section is empty.

Functions

func DefaultSkipName

func DefaultSkipName(name string) bool

DefaultSkipName reports whether name is one of the built-in directory basenames every workspace walk skips unconditionally, regardless of config.yaml. Exposed so packages that cannot hold a full Root (e.g. internal/index, which is handed a bare root string) still share the same built-in set as Root.SkipDir.

func IsNestedGitBoundary

func IsNestedGitBoundary(dir string) bool

IsNestedGitBoundary reports whether dir is itself a separate git boundary: dir/.git exists, as either a directory (the main clone, or a nested/vendored clone) or a file (a linked worktree's or a submodule's `gitdir: ...` pointer). Any such subdirectory belongs to a different git checkout and every workspace walk must skip it wholesale — this is what a hardcoded '.claude' skip used to approximate (agent worktrees under .claude/worktrees/<name> happen to be linked git worktrees), generalized to any harness, any location, any worktree/clone/submodule layout.

func IsRecordsPath added in v0.8.8

func IsRecordsPath(rel string) bool

IsRecordsPath reports whether a repo-relative path lies inside ANY context dir's records folder — the root's `.spectackle/` or a nested one such as `internal/mcpserver/.spectackle/`. It exists because three call sites had each hand-written this test and two of them anchored it at the repo root:

f == Dot || strings.HasPrefix(f, Dot+"/")

which silently excludes every non-root context. Since a records write is the server's own unavoidable side effect, a gate that fails to recognize it blames the caller for it — and the scope gate did exactly that, refusing an item's archive because the server had just re-scoped that item into a nested context and written its own block there. No transition could clear it, and the more precisely an item scoped itself to one subtree the likelier the deadlock became (B-01KYSDBZTEF1A).

The test is per SEGMENT, not a substring: a file named ".spectacklefoo" is ordinary work, which the older HasPrefix spelling would have swallowed.

func NearestContext

func NearestContext(ctxs []string, rel string) string

NearestContext returns the closest ancestor context dir for a repo-relative path (falls back to "" root). ctxs must come from ContextDirs.

Types

type BenchCfg added in v0.5.0

type BenchCfg struct {
	// History caps retained versions per unique benchmark key. Default 1:
	// the latest is what the codebase cares about (user requirement);
	// raise it to keep a benchmark history.
	History int `yaml:"history"`
}

CompactCfg holds the compact-due thresholds surfaced by `check`. BenchCfg tunes the benchmark record store (P-01KYJMVX2Q).

type CompactCfg

type CompactCfg struct {
	JournalMax int `yaml:"journal_max"` // journal events since last compact
	DoneMax    int `yaml:"done_max"`    // done-but-unarchived items
}

type Config

type Config struct {
	Schema        string      `yaml:"schema"`
	Langs         []string    `yaml:"langs"`
	Ignore        []string    `yaml:"ignore"`       // glob patterns, repo-relative slash paths (see SkipDir)
	IgnoreRegex   []string    `yaml:"ignore_regex"` // RE2 patterns, matched against repo-relative slash paths
	BudgetDefault int         `yaml:"budget_default"`
	Compact       CompactCfg  `yaml:"compact"`
	Benchmarks    BenchCfg    `yaml:"benchmarks"`
	Verify        []string    `yaml:"verify"` // shell commands gating work-submit (e.g. "make test")
	Swarm         SwarmCfg    `yaml:"swarm"`
	WorktreesDir  string      `yaml:"worktrees_dir"` // override for .spectackle/wt (abs or root-relative)
	Feedback      FeedbackCfg `yaml:"feedback"`
	// CoverageGate: "package" makes check COUNT uncovered packages as
	// findings (CI red until backfilled) — explicit opt-in only. Top-level
	// rather than a FeedbackCfg sibling: it gates check's repository
	// hygiene, not the review loop's rounds (T-01KYD87ZN).
	CoverageGate string `yaml:"coverage_gate"`
	Git          GitCfg `yaml:"git"`
}

Config is .spectackle/config.yaml (root only).

type FeedbackCfg

type FeedbackCfg struct {
	MaxRounds int    `yaml:"max_rounds"` // reopen attempts before an item escalates to blocked
	Grill     string `yaml:"grill"`      // "require" hard-gates approval on a passing review verdict; else warn
	Validate  string `yaml:"validate"`   // "require" hard-gates archive on a passing validation verdict; else warn (T-01KYD94M3)
	// Risk-gated require (T-01KYFXDCH): when Validate is NOT "require",
	// the archive gate still demands a verdict for items whose LANDED diff
	// (never declared targets — gameable, T-0135 landed 15 files against 4
	// declared) trips either input below. An explicit require is never
	// downgraded by these.
	RiskFiles      int      `yaml:"risk_files"`      // landed-file count that trips require; 0 = default 8
	DangerousPaths []string `yaml:"dangerous_paths"` // repo-relative globs ("dir/**" prefix or path.Match); default EMPTY — this repo's vocabulary is wrong for yours
}

FeedbackCfg tunes the SDD orchestration v2 feedback loop (see internal/lifecycle: Move's done->active reopen counter and Escalate).

type GitCfg added in v0.2.0

type GitCfg struct {
	Enabled *bool  `yaml:"enabled"` // nil (key omitted) means true; see IsEnabled — a plain bool can't tell "omitted" from "explicitly false"
	Mode    string `yaml:"mode"`    // "offline" (default, GIT-DEFAULT-001: commit-only edges on the current branch) or "online" (explicit opt-in: branches, PRs, pushes to Remote); an unknown value is rejected at load, see load()
	Remote  string `yaml:"remote"`  // remote name pushed to in online mode; default "origin"
	Base    string `yaml:"base"`    // branch task branches are pushed against; default is the repository's OWN default branch, read at load (see wt.DefaultBranch) — never hardcoded "main"
	// Commits selects the edge-commit engine (T-01KYD94MG): "edges"
	// (default) commits every .spectackle-writing tool call with a
	// structured decision message derived from its journal events; "off"
	// produces zero commits and unchanged tool output (the validate
	// attribution fix that excludes spectackle( records subjects is
	// knob-independent and intended). A validator
	// argued the journal already carries eid/ag and the feature is
	// redundant; the requirement is explicit that the decision trail must
	// be readable in git log by humans, so the default stays edges and the
	// dissent is recorded on the task.
	Commits string `yaml:"commits"`

	// AwaitChecks is how many SECONDS the archive closure waits for the
	// head's CI verdict before refusing whole. It was a hardcoded 5 minutes,
	// which sat inside this repository's own CI duration distribution
	// (measured 3m43s-5m38s across ten consecutive runs) — so archives
	// refused on builds that were merely unfinished, and because a refusal
	// compensates by writing a journal event, the retry pushed a new commit
	// that started CI over. The wait could never converge
	// (B-01KYQJDJJVFC2).
	//
	// A knob rather than a bigger constant: the right value is a property of
	// the repository's CI, not of this program, and the code's own comment
	// already framed the budget as bounding damage rather than as a
	// correctness boundary. 0 or omitted means the default; see AwaitBudget.
	AwaitChecks int `yaml:"await_checks"`
}

GitCfg tunes the git integration between task worktrees and the shared remote (the primitives it configures live in internal/wt). Git stays enabled by default, but the MODE defaults to OFFLINE (GIT-DEFAULT-001, user decision 2026-07-27): an absent mode key means commit-only lifecycle edges on the current branch — no branches, no PRs, no pushes. Online operation is the explicit opt-in `git: mode: online`. This flips the original opt-out contract (zero value used to mean enabled-and-online); repositories that relied on the implicit online default must add the key. See IsEnabled for the one field (Enabled) where a plain bool can't express omitted-vs-false — Mode and Remote get their defaults the same zero-block way every other Cfg struct in this file does.

func (GitCfg) AwaitBudget added in v0.8.2

func (g GitCfg) AwaitBudget() time.Duration

AwaitBudget is how long the archive closure waits for CI, as a duration. The default is deliberately ABOVE the slowest CI run observed in this repository rather than near its median: a wait that expires on a green build costs a retry, and a retry restarts CI, so the failure is not symmetric with waiting slightly too long.

func (GitCfg) EdgeCommits added in v0.2.0

func (g GitCfg) EdgeCommits() bool

EdgeCommits reports whether the edge-commit engine is armed: empty (key omitted, incl. every pre-feature workspace) means edges — the default — and only an explicit "off" disarms.

func (GitCfg) IsEnabled added in v0.2.0

func (g GitCfg) IsEnabled() bool

IsEnabled reports whether git integration is active. nil means the key was never in config.yaml at all — which includes every workspace scaffolded before this field existed — and that must resolve to true, not false, or an opt-out feature would ship silently opted out everywhere already running. An explicit `enabled: false` is always honored.

type Locker added in v0.2.0

type Locker interface {
	// WithLock runs fn while holding name, releasing on every exit path
	// (including panic). Callers pass the ENTIRE read-modify-write as fn,
	// never just the final write: locking only the write leaves two readers
	// racing to read the same stale state, which is the defect this exists
	// to close (B-01KYD57FN3ERHBM5EQ3534YJXP).
	WithLock(name string, fn func() error) error
}

Locker serializes a read-modify-write against one named, cross-process scope. coord.DB satisfies it (WithLock, generalized from what used to be a single hardcoded 'integrate' lock); this interface exists so item/spec/ drift can call through Root.Lock without importing internal/coord — those packages persist bundle files, not swarm coordination, and coord.db is swarm coordination's package to own.

type Root

type Root struct {
	// Sink, when set, observes every journal event this Root appends —
	// the edge-commit engine's exact capture mechanism (T-01KYD94MG): the
	// server installs a per-call buffer here in its gate, so the commit
	// derives from precisely the events the call wrote, never a glob of
	// everything dirty.
	Sink func(journalPath string, raw []byte)

	Dir   string // absolute path
	Agent string // agent identity writing through this workspace ("" outside swarm contexts)
	Cfg   Config

	// Lock is nil outside a swarm-aware caller (tests, migrate, a one-shot
	// CLI invocation with no coord.db open) — those already have at most one
	// writer touching any given bundle file, so item/spec/drift run unlocked
	// exactly as they always have. mcpserver.New sets it to the same *coord.DB
	// it opens for lease/counter/event coordination (see coord.go), so a
	// server process wires this up once at construction and every write
	// through the resulting Root is automatically serialized against
	// siblings — no call site elsewhere has to remember to ask for it.
	Lock Locker
}

Root is a detected workspace.

func Detect

func Detect(start, flagRoot string) (Root, error)

Detect finds the workspace root starting at start (usually the cwd).

func LoadRoot

func LoadRoot(dir string) (Root, error)

LoadRoot reads dir/.spectackle/config.yaml (if present) and returns a Root scoped to dir, without walking up to find the workspace root. Callers that already know the root — e.g. a package that only receives a bare root string, like spec.Load — use this to get a fully configured Root (in particular one whose SkipDir honors the workspace's Config.Ignore / IgnoreRegex) without duplicating Detect's walk-up logic.

func (Root) AnchorsPath

func (r Root) AnchorsPath() string

AnchorsPath is root-only (workspace-wide bindings).

func (Root) BenchPath added in v0.5.0

func (r Root) BenchPath(ctx string) string

BenchPath is the context's benchmark record store (P-01KYJMVX2Q): keyed last-writer-wins ndjson, union-merged like the journal.

func (Root) CacheDir

func (r Root) CacheDir() string

CacheDir is root-only and excluded from git by the server-written .gitignore.

func (Root) ContextDirs

func (r Root) ContextDirs() ([]string, error)

ContextDirs returns every repo-relative dir (incl. "" for root) that has a .spectackle folder with at least one bundle file, shallow before deep.

func (Root) CoordPath

func (r Root) CoordPath() string

CoordPath is the shared multi-agent coordination DB (main repo only).

func (Root) EnsureScaffold

func (r Root) EnsureScaffold(ctx string) error

EnsureScaffold creates the .spectackle folder of a context dir with its server-written housekeeping files. For the root it additionally writes config.yaml, .gitignore (cache/) and the cache dir.

func (Root) JournalPath

func (r Root) JournalPath(ctx string) string

func (Root) SkipDir

func (r Root) SkipDir(rel, name string) bool

SkipDir is the single entry point every workspace walk (ContextDirs, spec.Load, the coverage-gap walk, and — via DefaultSkipName / IsNestedGitBoundary — the indexer) shares to decide whether to prune a directory. rel is the repo-relative, slash-separated path of the directory ("" for the workspace root, which is never itself pruned); name is its basename. True when any of these hold:

  • rel is a nested git boundary (IsNestedGitBoundary) — never checked for the root itself;
  • name is one of the built-in defaults (DefaultSkipName);
  • rel matches a configured Config.Ignore glob;
  • rel matches a configured Config.IgnoreRegex pattern;
  • rel is excluded by git itself (internal/ignore) — checked last, after every user-configurable and built-in rule, so config always wins and the cheap checks above run before the (memoized, but still map-lookup plus climb) git-backed one.

func (Root) SpecPath

func (r Root) SpecPath(ctx string) string

SpecPath / WorkPath / JournalPath locate the three bundle files of a context dir, repo-relative ("" = root).

func (Root) SpectackleDir

func (r Root) SpectackleDir(ctx string) string

SpectackleDir maps a repo-relative context dir ("" = root) to the absolute path of its .spectackle folder.

func (Root) WorkPath

func (r Root) WorkPath(ctx string) string

func (Root) WtDir

func (r Root) WtDir() string

WtDir is where agent worktrees live. NOT under cache/ — cache is disposable, in-flight work is not.

type SwarmCfg

type SwarmCfg struct {
	LeaseTTL int `yaml:"lease_ttl"` // seconds a scope lease lives without refresh
	AgentTTL int `yaml:"agent_ttl"` // seconds without heartbeat before an agent counts as gone
	// PanelMax CAPS a per-item multi-agent review panel (grill op=verdict
	// panel=n, legal only on live risk signals). Config can cap a panel,
	// never raise one that was not item-justified (P-01KYES kill list).
	PanelMax int `yaml:"panel_max"`
}

SwarmCfg tunes multi-agent coordination.

Jump to

Keyboard shortcuts

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