how it works

From candidate intent to verified activation.

Spindle is a control loop. Exact package bytes receive bounded authority, composition resolves desired state, startup reconciles the harness, each agent gets its own realization, and evidence determines whether the skill should stay.

On this page
  1. The lifecycle record chain
  2. Composition vocabulary
  3. The bind pipeline
  4. Startup and activation
  5. Rendering & profiles
  6. Doctrine
  7. The safety floor
  8. Determinism & caching

The lifecycle record chain

No single “installed skill” record can answer where bytes came from, why they are authorized, what an agent loaded, and whether they helped. Spindle keeps those questions separate:

SourceRef PackageRevision IntentCard Lease / Adoption SurfaceLock Realization Activation Evaluation
RecordQuestion answered
PackageRevisionWhich exact source and skill bytes are under review?
IntentCardWhat outcome, trigger boundary, authority, and context cost does the candidate claim?
LeaseWho may use these bytes temporarily, where, until when, and with what authority?
AdoptionRecordWho accepted durable maintenance custody after which successful trial?
SurfaceLockWhich exact adopted and leased inputs should this harness expose?
AgentRealizationWhich invariant core and evaluated overlay fit this actual agent coordinate?
ActivationReceiptWhat model, tools, policy, and projection did the harness actually expose?
EvaluationClaimDid this intervention help on this bounded artifact/runtime/task distribution?

Composition vocabulary

The original composition nouns remain the machinery for authored, managed blends:

skill package distribution channel blend profile surface
skill
A directory with a SKILL.md (YAML frontmatter name/description + a Markdown body). The canonical, authored-once unit. Kept small on purpose.
package
A pip-installable project that ships skills and advertises capabilities, declared with [tool.spindle.package]. Spindle discovers installed packages through that metadata.
distribution
A pip-installable bundle that groups packages (as ordinary dependencies) and carries surface-wide assets — doctrine, channels, profiles — declared with [tool.spindle.distribution].
channel
A versioned (scope × harness) manifest listing which skills a surface should receive at that scope, plus the absolutes in force and optional routing tiers.
surface
A repo or agent that consumes a blend. Carries a name, a harness, an autonomy mode, one or more app-class clusters, and a target model.
profile
A data-driven, versioned renderer. A harness profile sets dialect; a model profile sets density. Each names a transform: identity, terse, trim, or llm.
blend
The resolved skill subset for one surface — the composition — together with the absolutes in force and a record of every override (shadow). The thing that actually lands.
doctrine
The versioned first-principles set (preferences, absolutes, meta-principles) that skills are checked against. One coherent frame per distribution.
Scope A channel or doctrine entry has a scope: system, cluster, or repo. Scope drives both the order in which channels are selected (broad to narrow) and which layer wins when two define the same skill (narrow wins). Guardrails are the exception — they accumulate across all scopes.

The bind composition pipeline

spindle bind <repo> --harness claude runs six stages. It is a fail-closed pipeline: if rendering drops a guardrail or the lint finds a problem, nothing is materialized (unless you pass --force).

01

Classify

Read the checkout's pyproject.toml/package.json into a signal; derive an app-class cluster key.

appclass.py
02

Select

Subscribe to channels broad → narrow: system, per-cluster, repo — for this harness.

channels.py
03

Resolve

Merge the channel layers into one blend; narrow overrides broad; record shadows; union absolutes.

composition.py
04

Render

Rewrite each skill for the harness dialect, then the model density. Verify guardrails survived.

render.py
05

Lint

Check the blend for coherence — command collisions, duplicates, unsafe irreversibles. Gate.

composition.py
06

Materialize

Symlink the resolved skills into the harness's native skills dir; touch only spindle-owned links.

materialize.py

01 · Classify

Spindle reads the repo's manifests (pyproject.toml, package.json) into a RepoSignal — language, dependencies, notable files — and classifies it into an app-class with a single stable cluster key like lang:python|network-service|uses-llms. The surface's clusters come from that key; a repo that changes shape gets re-clustered on the next bind.

02 · Select

The surface subscribes to an ordered list of channels, least- to most-specific: (system, system), then one (cluster, <key>) per cluster, then (repo, <name>). Each resolves to a file on disk under the active distribution's source_dir:

channels/<scope>/<name>/<harness>/channel.toml

The harness is applied here: a Claude surface only ever reads the claude/ manifests. Missing manifests are simply skipped — a surface legitimately has only a system channel. A channel manifest is small:

# channels/system/system/claude/channel.toml
version = "0.1.0"
absolutes = ["A1", "A2"]        # doctrine ids in force at this scope
skills = ["clarify", "design", "tasks", "review"]

[tiers]                         # optional routing hints (advisory)
clarify = "judgment"
design  = "judgment"
tasks   = "judgment"
review  = "judgment"

Skill names are resolved through an index built from the installed packages. A name with no installed package is dropped — which is exactly why a bind that finds nothing prints resolved 0 skills: the channel named skills that aren't installed.

03 · Resolve

Resolution merges the selected layers into one Composition. Each skill occupies a slot keyed by its command (falling back to its name); a more-specific layer overrides a less-specific one for the same slot, and every override is recorded as a shadow rather than silently dropped. Absolutes are unioned across all layers and never lost to precedence — the guardrail floor only rises. The default precedence is system → cluster → repo.

05 · Lint

With the blend assembled (and already rendered), the linter runs pure coherence checks over its metadata: no two skills may claim the same command, no skill may appear twice, and a genuinely irreversible skill must carry the matching absolute. Problems stop the bind unless forced. The linter inspects structure, not prose — which is why the guardrail check lives in rendering (below), not here.

06 · Materialize

Finally, Spindle symlinks each resolved skill into the harness's native location and reports a per-skill action — linked · updated · kept · removed · skipped. It reads the surface's previous binding to know which links it owns, and removes only those: a file you added by hand is never touched.

Then it records the binding: a stable coordinate over the skills, the doctrine, and the channel versions, appended to a per-surface history so any prior coordinate is a rollback target. spindle unbind reverses it, removing exactly the links Spindle owns.

Startup and activation

Materialization is desired state, not proof of what a running harness saw. Before discovery, spindle bootstrap resolves active adoptions and leases, inventories every observable scope, compares exact content identity, and plans any ownership-safe repair. The portable spindle launch wrapper performs that work before starting Claude or Codex.

Stable native hooks then recheck session start, resume/compaction, and subagent start. Hook definitions contain no mutable pins: they invoke the same Spindle bootstrap command while desired state stays in the surface lock. The normal hook path is local and cache-only, emits one compact status marker, and keeps full findings in a startup receipt.

desired

Surface lock

  • Exact adopted package and skill digests.
  • Active lease IDs and expiries.
  • Expected projection paths and ownership.
  • Explicit foreign-conflict decisions.
observed

Activation receipt

  • Projection actually loaded.
  • Model actually served to this agent.
  • Harness build and effective tool envelope.
  • Effective policy and permission denials.

A pre-discovery mismatch can be repaired only when exact ownership matches. The same mismatch found after discovery returns restart-required. Foreign state is preserved in both cases.

Rendering & profiles

Selection decides which skills; rendering decides what text each one becomes for this surface. Because rendering produces new text, its output can't be a symlink — it's written to a content-addressed store and the blend is repointed at it. Two profile axes compose, dialect then density:

axis 1 · harness

Dialect

  • identity — verbatim (Claude is the reference dialect).
  • terse — deterministic whitespace collapse; content-preserving by construction.
  • llm — an actual model rewrite into the harness dialect; skipped if no client is configured.
axis 2 · model

Density

  • identity — keep all scaffolding (what weaker models want).
  • trim — strip author-marked scaffold fences, then terse.
  • llm — model-driven densification.

Profiles are tiny, versioned TOML files:

# profiles/claude/profile.toml — harness dialect
version = "1"
transform = "identity"

# profiles/models/frontier/profile.toml — model density
version = "1"
transform = "trim"
tier = "frontier"

The trim transform is the "optimize things out" half of tuning. An author marks a passage that only smaller models need, and a frontier profile removes it — the same canonical skill, rendered denser for a model that reads faster without it:

<!-- scaffold:examples -->
Worked example a smaller model benefits from…
<!-- /scaffold -->

Doctrine

Doctrine is the coherent first-principles frame, kept as a versioned, hand-editable, machine-readable TOML artifact. It has three entry kinds:

# doctrine/doctrine.toml (excerpt)
version = "0.1.0"

[[preference]]
id = "P1"
scope = "system"
favor = "clear acceptance evidence"
over  = "performative process"

[[absolute]]
id = "A2"
scope = "system"
mode = "NEVER"
statement = "fabricate missing facts"

Doctrine matters because it makes "coherence" a defined, versioned, testable property instead of a vibe. Its coordinate<version>+<hash> — is pinned into every render cache key and every binding record, so a rollback can re-fetch the exact doctrine that produced a blend. spindle doctrine validate refuses a malformed doctrine before it can ever render into skills.

The safety floor

Two mechanisms make it structurally hard to compose away safety. First, absolutes accumulate: because resolve unions them across every layer, a narrow channel can add a guardrail but never remove one. Second, rendering verifies preservation:

Every guardrail clause in a skill's source must survive, normalized, in the rendered output — or the bind fails closed.render.verify_preserved

Guardrail clauses are lines matching ALWAYS, NEVER, MUST NOT, or DO NOT. The coherence linter reads metadata and can't catch a paraphrase that quietly drops one — so this deterministic check is the backstop, and it holds even when an llm profile ignores its instructions. A dropped clause raises an error and the binder materializes nothing.

Determinism & caching

Everything is content-addressed, which is what makes the "extra step" cheap to repeat. A rendered skill is cached under sha256(doctrine × harness-profile version × model-profile version × skill content); change any input and the cache invalidates, change nothing and the render is reused. A binding's coordinate is sha256(skills + doctrine + channel versions). Re-binding an unchanged surface is nearly free, and every past coordinate is a precise rollback target.