Kit Authoring And Distribution¶
A Cruxible kit is a versioned bundle with a cruxible-kit.yaml manifest,
an entry config, provider code, optional data, and a bundled
cruxible.lock.yaml.
For runnable examples, see Kit Walkthroughs.
Operation-Style Relationship Axes¶
Kits that model work, reviews, investigations, remediation, agent operations,
or project execution should not collapse every relationship into one generic
related_to or blocks edge. Start with explicit axes so readiness, critical
path, review, and roll-up queries can keep their meanings separate.
Use these defaults when the kit has work-like entities:
| Axis | Relationship Shape | Meaning |
|---|---|---|
| Sequencing | work_item_depends_on_work_item |
Direction: from depends on to. The target must land, be decided, or stabilize first. |
| Impediment | risk_blocks_work_item, open_question_blocks_work_item |
A durable unresolved threat or uncertainty blocks or materially delays work. |
| Resolution | work_item_mitigates_risk, work_item_answers_open_question, decision_answers_open_question |
Work or a decision resolves the impediment without pretending the impediment was sequencing. |
| Composition | work_item_part_of_work_item |
Child work is part of a larger scope. This is roll-up, not order. |
| Lineage | work_item_spawned_from_work_item |
A follow-up came out of earlier work. This is provenance, not a prerequisite. |
| Replacement | work_item_supersedes_work_item, decision_supersedes_decision |
The source replaces the target. This is not general lineage. |
| Review gate | review_request_for_work_item plus a mutation guard |
A work item cannot move to a guarded lifecycle value until the review request is approved. |
| Interpretation history | StateNote plus typed note-about relationships |
Durable corrections, field notes, implementation notes, and review notes without bloating current entity fields. |
Do not make these hidden Cruxible defaults. Put the relationships in the kit config with domain-specific names when needed, because the ontology is the contract agents and reviewers inspect.
Use direct relationships for deterministic placement and roll-up. Use governed proposal policies for interpretive claims such as dependencies, blockers, mitigations, answers, supersession, and decision impact. Documents, chats, review reports, and source sections should be evidence references for those claims, not modeled entities.
Minimal operation-axis scaffold:
enums:
work_status:
values: [planned, active, blocked, watching, closed, deferred, superseded]
work_priority:
values: [critical, high, medium, low]
note_kind:
values: [correction, field_note, rationale_update, implementation_note, review_note]
entity_types:
WorkItem:
properties:
work_item_id: {primary_key: true}
title: {required: true}
status: {enum_ref: work_status, required: true}
priority: {enum_ref: work_priority}
summary: {}
Risk:
properties:
risk_id: {primary_key: true}
title: {required: true}
status: {}
OpenQuestion:
properties:
question_id: {primary_key: true}
title: {required: true}
status: {}
Decision:
properties:
decision_id: {primary_key: true}
title: {required: true}
status: {}
ReviewRequest:
properties:
review_request_id: {primary_key: true}
title: {required: true}
status: {required: true}
summary: {}
review_notes: {}
StateNote:
properties:
state_note_id: {primary_key: true}
note_kind: {enum_ref: note_kind, required: true}
noted_at: {required: true}
body: {required: true}
relationships:
- name: work_item_depends_on_work_item
description: "Sequencing: from depends on to."
from: WorkItem
to: WorkItem
proposal_policy:
signals:
source_evidence: {role: required, always_review_on_unsure: true}
maintainer_judgment: {role: advisory, always_review_on_unsure: true}
- name: risk_blocks_work_item
description: "Impediment: a risk blocks or materially delays work."
from: Risk
to: WorkItem
proposal_policy:
signals:
source_evidence: {role: required, always_review_on_unsure: true}
maintainer_judgment: {role: advisory, always_review_on_unsure: true}
- name: open_question_blocks_work_item
description: "Impediment: an unresolved question blocks or delays work."
from: OpenQuestion
to: WorkItem
proposal_policy:
signals:
source_evidence: {role: required, always_review_on_unsure: true}
maintainer_judgment: {role: advisory, always_review_on_unsure: true}
- name: work_item_part_of_work_item
description: "Composition and roll-up, not sequencing."
from: WorkItem
to: WorkItem
- name: work_item_spawned_from_work_item
description: "Lineage and follow-up provenance, not sequencing."
from: WorkItem
to: WorkItem
- name: work_item_supersedes_work_item
description: "Replacement: source supersedes target."
from: WorkItem
to: WorkItem
proposal_policy:
signals:
source_evidence: {role: required, always_review_on_unsure: true}
maintainer_judgment: {role: advisory, always_review_on_unsure: true}
- name: review_request_for_work_item
from: ReviewRequest
to: WorkItem
- name: state_note_about_work_item
from: StateNote
to: WorkItem
Add named queries around the axes rather than broad text search: active work queue, blocked work with blocker context, work-item change context, roll-up context, lineage context, pending reviews, and recent state notes. Add quality checks or mutation guards only where they protect real operating discipline, such as one composition parent, review requests attached to work, and closing work only after an approved review.
Minimal manifest:
schema_version: cruxible.kit.v1
kit_id: kev-triage
version: 0.2.0
role: overlay
target_state: kev-reference
entry_config: config.yaml
provider_paths:
- providers
copy_paths:
- data
- skills
- README.md
requires_extras: []
Rules:
roleisbase,standalone, oroverlay.role: baseanchors composition, must be first, and may appear at most once.role: overlayrequirestarget_state.role: baseandrole: standalonemust not settarget_state.requires_basemay name the required base kit. Initialization fails when a different base, or no base, is present.- 0.2 supports one
entry_configper kit. requires_extrasis metadata only. Cruxible does not install kit dependencies automatically.min_core_versionis optional and declares the oldest cruxible core the kit supports. It must be a PEP 440 version and is compared with PEP 440 ordering, so0.3.0rc1does not satisfy a0.3.0floor. An unparseable value is a manifest error at load. The floor is enforced before any kit content is materialized — oncruxible init --kit, on governed config upload, and on overlay composition — naming both versions and the upgrade command. Cores that predate the field ignore it, so adding it does not changeschema_version.
Provider refs use kit://:
kit:// paths are relative to the materialized kit root. Absolute paths,
.., symlinks, and paths outside declared provider_paths are rejected.
Python providers run in the current Cruxible Python environment and may import
stdlib, cruxible_core, installed Cruxible dependencies or extras, and files
under declared provider paths.
Bundle behavior:
- The bundle digest covers every non-junk regular file in sorted POSIX-relative order, including path and bytes.
- Junk such as
__pycache__/,*.pyc,.DS_Store,.ruff_cache/, and.pytest_cache/is ignored. - Symlinks are rejected.
- Bundles are cached under
CRUXIBLE_KIT_CACHE_DIRor${XDG_CACHE_HOME:-~/.cache}/cruxible/kits. - Cache installs are locked and atomic by bundle digest: the staged copy is
re-hashed before it is moved into place, and a cache hit is re-hashed
before it is reused. The cache key is derivable from a kit's contents, so a
directory sitting under a known key is never trusted on the strength of the
key alone. A cache entry whose contents no longer hash to its key is refused
as poisoned — delete the named directory and re-run to re-fetch, or move the
cache somewhere only you can write with
CRUXIBLE_KIT_CACHE_DIR. oci://refs are pinned inoci-pins.jsonin that same cache directory: the first resolution records the content digest, later resolutions verify it, and a repointed tag refuses untilCRUXIBLE_OCI_REPIN=<the new digest>re-pins it explicitly.- Materialization copies the cached kit into the instance root.
- Kit bundles carry
cruxible.lock.yamlat the kit root as a portable bundle artifact, and materialization verifies that lock against its recordedlock_digestbefore installing the kit. - Initialized Cruxible instances execute workflows from
.cruxible/cruxible.lock.yaml; kit-backed initialization imports the bundled lock there when it matches the active config, or regenerates the instance-local lock when an active runtime config has been composed from the bundle. - Runtime workflow execution does not fall back to arbitrary config-root locks.
- Consumers should not silently regenerate published bundled locks. Rebuild the kit lock before publishing or distributing a changed kit.
Kit initialization composes layers in this order: one base, standalone domain
kits from left to right, then overlays. Unless --bare is supplied, CLI, MCP,
and daemon initialization prepend the deployment's default base. The product
default is agent-operation; deployments can change it with
CRUXIBLE_DEFAULT_BASE_KIT or set that variable to off. Initialization
always reports the selected base. An implicit base must have the same kit
version as the first explicitly requested kit, keeping every composed instance
on one release train. The base remains a separate digest-verified bundle; it is
not copied into domain bundles.
Standing Views And Decision Surfaces¶
A domain kit's read surface has three tiers, and shipping all three is what makes a kit feel alive rather than inert:
- At least one standing "requiring action" view — a parameterless
mode: collectionquery over the state that needs attention now (open incident impacts, postures requiring action, upcoming deadlines). This is the queue surface: it renders in UIs with no input and answers "what needs a decision today?" Order it by the fields a triager would sort by, usingorderedenums. - Parameterized decision queries — traversal queries anchored on the
entity a decision is about (
entry_point), supplying the evidence a decision report pulls: scope, exposure, alternatives, obligations. - Proposal workflows — where interpretive claims enter as governed candidates. The queue view tells you which entity needs judgment; the decision queries justify it; the proposal/resolution flow records it.
Reference kits (published state that consumers subscribe to) ship no standing views: the actionable views belong to the overlay that composes local judgment over the reference, not to the reference itself.
Keep each query in its layer: a query touching only operating-layer types (work items, reviews, actors) belongs in the operating-state kit; queries joining domain types to operating types must live in the overlay, since only the overlay can see both vocabularies.
Seed Data Is A Worked Example¶
A kit's data/ ships as a pinned workflow artifact (lock-covered,
digest-attested), and for demo kits it is fictional: a worked example so the
kit runs end to end in minutes, not a starter dataset to build on. Adopting a
kit for real use means replacing the seed with your own exports — the
ingest workflows are the interface, the seed files are just one example input:
- Point the ingest/refresh workflows at your own exports (or swap the pinned artifact for your data directory and re-lock).
- Do not run seed workflows (
build_seed_stateand friends) against an instance that already carries real state; they exist to demonstrate the pipeline shape. - Real reference data (a public catalog snapshot) is the exception: that is kit content, not an example, and updates with the kit version.
Published Kit Bundles¶
Installed distributions resolve built-in aliases such as kev-reference from
digest-pinned release bundles, so cruxible init --kit <name> works without a
source checkout. scripts/build_kit_bundles.py builds a deterministic
dist/kits/<id>-<version>.tar.gz for every kits/<id>/ (byte-identical across
runs: sorted members, zeroed timestamps and ownership, normalized permissions)
and regenerates src/cruxible_core/kit_distribution/manifest.json, which ships
in the wheel and pins each bundle by tarball sha256 and by the extracted
directory digest — the same compute_path_sha256 discipline kit locks use.
The release sequence is:
- Stamp the package and kit version, run
scripts/build_kit_bundles.py, and commit the regenerated manifest (plus any refreshed kit locks). - Run
scripts/check_kit_lockfiles.py, then create and push thev<version>tag. - The tag-triggered publish workflow publishes the client package in parallel with rebuilding the bundles from tagged source; the core package waits for both. The bundle job refuses to upload unless the regenerated manifest, including every tarball digest, exactly matches the committed manifest. It creates the GitHub release when absent and replaces same-named bundle assets on reruns, so a partial or manually created release is repaired without changing its authored notes.
- CI runs
scripts/check_kit_release_assets.py. Before the exact version tag exists onorigin, the check emits a notice and skips (the valid state between stamping and tagging). Once the tag exists, every manifest asset URL must answer an HTTP HEAD request with status 200.
Alias resolution order:
- Local source-checkout
kits/directories always win (development). - Published release bundles from the packaged manifest: the tarball sha256 is
verified before extraction, members are safe-extracted (files and
directories only, no absolute paths,
.., or links), the extracted tree must match the pinned directory digest, and the verified kit is installed atomically into the kit cache keyed by that digest. Any mismatch deletes the artifacts and refuses; a cache hit skips the network entirely.
There is no shipped alias-to-oci:// table; an explicit oci:// ref (a
third-party kit) still resolves through oras.
An overlay kit's target_state base resolves through the same order, so a
fetched overlay composes over its fetched base with no local kits present.
CRUXIBLE_KIT_MANIFEST_URL_BASE overrides the manifest's release base_url
(pre-publish smoke runs against a local file server); asset names and digests
still come from the packaged manifest.
Refresh a bundled lock directly from the kit root before publishing:
The lock pins the kit's own config layer only — its providers and artifacts,
with URIs kept relative to the kit root — so an overlay kit locks without its
target_state base present. Base-layer content is pinned by the base kit's
own lock. CI asserts every bundled kit's committed lock matches a fresh regen,
so run this after any config, provider, or seed-data change.
A lock records its own content digest, and materializing a kit verifies the
bundled lock against it. A lock whose body no longer hashes to its recorded
lock_digest was edited after generation — it pins something other than what
the kit author locked — so materialization refuses rather than installing a kit
against unverified pins:
Kit bundle lock at <path> failed digest verification: the lock records
lock_digest=sha256:..., but its contents hash to sha256:...
Recover by re-running cruxible lock --kit-dir <kit> upstream, re-publishing
the kit, and materializing again. Never hand-edit a lock file.
Verification runs against the staged cache copy, before anything is written into the instance root: a refused kit leaves the target directory exactly as it was, with no half-installed config, provider tree, or metadata to clean up.
oci:// kit refs are pinned to the content they first resolved to¶
An oci:// tag is mutable: the same ref can be re-pushed to point at different
bytes. The first resolution of a ref records the bundle content digest it
delivered, in oci-pins.json under the kit cache directory. Every later
resolution of that ref must match. A repointed tag refuses:
Kit ref 'oci://.../kit:1.0.0' is pinned to content digest sha256:..., but the
registry now serves sha256:... for that same tag.
If the new content is expected, re-pin explicitly by naming the digest you are accepting — there is no silent acceptance path and no blanket "yes":
Naming the digest makes the escape hatch self-expiring. A CRUXIBLE_OCI_REPIN=1
left in a shell profile or a CI job definition would authorize every future
repoint of every ref; a digest stops authorizing anything the moment the pin it
named is taken, so the next repoint is refused again. Overwriting a pin is
logged with the ref and both digests. Subsequent resolutions verify against the
newly recorded digest. Pulling an immutable digest ref (...@sha256:...)
instead of a tag avoids the situation entirely.
Vocabulary:
- Use overlay for a local instance tracking a published upstream state.
- Use clone for a point-in-time state copy from a snapshot.
- Use local for customer-owned seeded or runtime state.
- Do not use clone for kit distribution. Use pull, cache, materialize, or install.