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
- func DefaultSkipName(name string) bool
- func IsNestedGitBoundary(dir string) bool
- func IsRecordsPath(rel string) bool
- func NearestContext(ctxs []string, rel string) string
- type BenchCfg
- type CompactCfg
- type Config
- type FeedbackCfg
- type GitCfg
- type Locker
- type Root
- func (r Root) AnchorsPath() string
- func (r Root) BenchPath(ctx string) string
- func (r Root) CacheDir() string
- func (r Root) ContextDirs() ([]string, error)
- func (r Root) CoordPath() string
- func (r Root) EnsureScaffold(ctx string) error
- func (r Root) JournalPath(ctx string) string
- func (r Root) SkipDir(rel, name string) bool
- func (r Root) SpecPath(ctx string) string
- func (r Root) SpectackleDir(ctx string) string
- func (r Root) WorkPath(ctx string) string
- func (r Root) WtDir() string
- type SwarmCfg
Constants ¶
const Dot = ".spectackle"
Dot is the folder name every server write is confined to.
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.
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 ¶
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 ¶
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
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 ¶
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 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
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
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
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 LoadRoot ¶
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 ¶
AnchorsPath is root-only (workspace-wide bindings).
func (Root) BenchPath ¶ added in v0.5.0
BenchPath is the context's benchmark record store (P-01KYJMVX2Q): keyed last-writer-wins ndjson, union-merged like the journal.
func (Root) CacheDir ¶
CacheDir is root-only and excluded from git by the server-written .gitignore.
func (Root) ContextDirs ¶
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) EnsureScaffold ¶
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 (Root) SkipDir ¶
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 ¶
SpecPath / WorkPath / JournalPath locate the three bundle files of a context dir, repo-relative ("" = root).
func (Root) SpectackleDir ¶
SpectackleDir maps a repo-relative context dir ("" = root) to the absolute path of its .spectackle folder.
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.