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.
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:
| Record | Question answered |
|---|---|
PackageRevision | Which exact source and skill bytes are under review? |
IntentCard | What outcome, trigger boundary, authority, and context cost does the candidate claim? |
Lease | Who may use these bytes temporarily, where, until when, and with what authority? |
AdoptionRecord | Who accepted durable maintenance custody after which successful trial? |
SurfaceLock | Which exact adopted and leased inputs should this harness expose? |
AgentRealization | Which invariant core and evaluated overlay fit this actual agent coordinate? |
ActivationReceipt | What model, tools, policy, and projection did the harness actually expose? |
EvaluationClaim | Did this intervention help on this bounded artifact/runtime/task distribution? |
Composition vocabulary
The original composition nouns remain the machinery for authored, managed blends:
- skill
- A directory with a
SKILL.md(YAML frontmattername/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, orllm. - 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.
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).
Classify
Read the checkout's pyproject.toml/package.json
into a signal; derive an app-class cluster key.
Select
Subscribe to channels broad → narrow: system, per-cluster, repo — for this harness.
channels.pyResolve
Merge the channel layers into one blend; narrow overrides broad; record shadows; union absolutes.
composition.pyRender
Rewrite each skill for the harness dialect, then the model density. Verify guardrails survived.
render.pyLint
Check the blend for coherence — command collisions, duplicates, unsafe irreversibles. Gate.
composition.pyMaterialize
Symlink the resolved skills into the harness's native skills dir; touch only spindle-owned links.
materialize.py01 · 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.
Surface lock
- Exact adopted package and skill digests.
- Active lease IDs and expiries.
- Expected projection paths and ownership.
- Explicit foreign-conflict decisions.
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:
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.
Density
identity— keep all scaffolding (what weaker models want).trim— strip author-marked scaffold fences, thenterse.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:
- preferences — "favor X over Y," scoped, with a note and provenance.
- absolutes —
ALWAYS/NEVERhard rules, scoped and rare; densest at system scope. - meta-principles — rules about how the other principles are written.
# 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.