Suspenders — skill authoring as an app-frame experience module
Status: open/accepted
Summary
Section titled “Summary”-
Skill browsing, authoring and testing ships as
suspenders, an app-frame experience module — not a standalone application. One module, two hosts: the SDLC desktop control plane and a thin standalone product. -
The service plane is Rust (
suspenders-core+ an ontogen-generated adapter); the model plane stays in@sksizer/agent-plugin(D-HRNS-harness-model-and-exporters). Discovery, IO, indexing and drift are new work; the skill definition and its exporters already exist and are not re-implemented. -
The corpus is a set of roots, not a directory: the open project, roots the user registers, and the user-global home. Skills are grouped by identity across runtimes, so copy drift is a query result rather than a feature.
-
apps/agentpantsstays in the tree as a reference, expected to go stale and eventually be retired.
Context
Section titled “Context”An earlier decision vendored AgentPants as a fourth standalone Tauri app on the polish/pumice recipe. That recipe was sound; the target was wrong. What was wanted was the capability — authoring and browsing agent skills — inside the shared modularity framework, not another shell to maintain.
Two things changed after that decision:
| Change | Effect |
|---|---|
@sksizer/app-frame made an experience module a first-class unit | A capability can be hosted without owning a shell |
determined proved the crate / experience / product split | The layout question was already answered |
Meanwhile the skill domain was already modeled twice: once in
@sksizer/agent-plugin (D-HRNS-harness-model-and-exporters), and again in
agentpants’ own agent-adapt crate. Building a third copy was the default
outcome of continuing it.
Decision
Section titled “Decision”Build suspenders as an experience module in the SDLC solution, splitting the
work by plane.
Where each plane lives
Section titled “Where each plane lives”| Plane | Home | Contains |
|---|---|---|
| Canonical store + rails | apps/sdlc/crates/suspenders-core | SQLite store (drafts, immutable versions, projected resources, destinations, per-destination sync tracking), projection, guard, additive publish, drift, adoption, discovery scan |
| SKILL.md rendering | agent-adapt 0.2.0 (crates.io) | Per-runtime layout, frontmatter dialect, tool-name translation — the crate agentpants projects through, composed unmodified |
| TS model + exporters | @sksizer/agent-plugin | The derive/model plane for the substrate’s own harness ops; exporter-layout convergence with agent-adapt deliberately deferred |
| Service adapter | apps/sdlc/desktop/src-tauri/src/api/v1/{skill,destination}.rs | Fn signatures; ontogen emits HTTP, Tauri IPC, MCP and the TS client |
| Experience module | apps/sdlc/experiences/suspenders | Nuxt layer — pages, components, the SkillClient seam |
| Standalone host | apps/sdlc/products/suspenders | Thin Tauri shell over sdf_lib::run_with(context) |
suspenders-core knows nothing of Tauri, HTTP, or the host (SQLite is its
subject matter, not a host concern). The adapter is the only place the two
meet, and it is the only hand-written transport code — everything else is
generated (D-0017-ontogen-shared-codegen-convergence).
The store is canonical; files are projections
Section titled “The store is canonical; files are projections”The agentpants model, kept: a skill is a row you version and project, not a file you happen to find. Drafts are mirrored from adopted source trees (read-only against the source, locked while linked), versions are immutable snapshots including peer-file resources, and publishing materializes a version at a destination under a per-file manifest. Drift is hash-vs-manifest, so files the manifest does not own are structurally invisible. Three safety rails carry self-hosting: protected source trees can never be (in, or contain) a destination, re-checked at publish time; a coverage probe refuses occupied directories unless acknowledged; the writer deletes only manifest-owned files whose bytes still match what it wrote.
One module, two hosts
Section titled “One module, two hosts”Both hosts compose the same Nuxt layer and run the same sdf binary crate. The
module contributes itself through registerModule; neither host hardcodes a
page. The only file that knows both a port and a generated transport is the
host’s own client plugin.
Placement follows determined’s internal shape rather than a top-level app,
per the D-0001-project-structure gradient: suspenders-core sits at the
solution tier and promotes to packages/rs/* only when a consumer outside
SDLC appears.
The corpus is roots, not a directory
Section titled “The corpus is roots, not a directory”| Root kind | Source |
|---|---|
| Project | The open project |
| Registered | Local repos the user registers |
| User-global | $HOME |
Roots are where DISCOVERY walks: found SKILL.mds are adoption candidates
and foreign catalog entries, never authority. A scanned copy that is the
source of a managed skill, or a file a publish manifest owns, folds into its
managed roster row — one skill is one row, however many places it exists.
Phases
Section titled “Phases”| Phase | Scope | State |
|---|---|---|
| 1 | Canonical store, discovery, projection, guard, publish, drift, adoption | Built |
| 2 | Service surface + roster/destinations UI + standalone product + live-repo self-host bootstrap | Built |
| 3 | Draft authoring — editor over store drafts, detach-source, restore-version | Planned |
| 4 | Test / run a skill — terminal + agent detection, scratch project | Planned |
An earlier phase 2 (compare-and-swap edits of SKILL.md files in place) was
built and then superseded by this model: writing files inside a corpus root
is exactly what the rails exist to prevent.
- A module is cheaper than an app and worth more. A co-tenant buys a separate shell, separate CI, and a separate release, and still cannot sit beside the control plane an operator already has open. The standalone product survives as a 30-line shell because the module carries the capability.
- Rust owns the service plane because the work is IO. Walking roots, hashing manifests, and projecting versions is filesystem work that must run identically under HTTP, IPC and MCP. One ontogen-generated surface gives all three from one signature.
- Projection reuses
agent-adapt, not a re-implementation. The crate agentpants renders through already owns per-runtime layout and frontmatter dialects, and the repo’s on-disk corpus matches ITS layout. Two additions are composed around it (annotations splice, resource emission) behind a seam that an upstream 0.3 could absorb.@sksizer/agent-plugin’s TS exporters emit different non-Claude layouts; converging the two projection planes is deferred deliberately rather than resolved by this arc. - Sources import-only, store canonical. The empirical case: deriving the live plugin tree through the harness model loses 89 of 148 files (test scaffolding, excluded by design), so publishing over a source tree would destroy what the model cannot regenerate. Adoption is byte-exact read-only, pinned by test against the real repo.
Options considered
Section titled “Options considered”Option A: maintain agentpants as a co-tenant app
Section titled “Option A: maintain agentpants as a co-tenant app”Lowest immediate effort; the vendor already builds. Rejected: it keeps a fourth shell, a duplicate domain model, and a skill browser that cannot compose with the control plane. The maintenance cost recurs and the capability stays walled.
Option B: fold the capability into the SDLC desktop app only
Section titled “Option B: fold the capability into the SDLC desktop app only”Simplest wiring — no product, no layer boundary. Rejected because the user asked for a standalone host too, and because the boundary is what keeps the module portable. Absent it, host assumptions leak into the pages.
Option C: module + standalone product (adopted)
Section titled “Option C: module + standalone product (adopted)”Costs one extra Nuxt layer and a thin Tauri shell. Buys host independence, a testable framework-free core, and a second host that proves the module really is one.
Consequences
Section titled “Consequences”- The skill plane is SDF’s first substrate-adjacent mutation family. T-DBAL pins the mutating HTTP surface to an exact allowlist; the eleven suspenders POSTs (adopt, mint, destination verbs) were added as a reviewed set, every one audited and token-guarded. No route writes arbitrary files into a registered corpus root — the earlier save-detail endpoint that did was removed with its arc.
suspenders-coreis a solution-tier crate. An outside-solution consumer triggers promotion topackages/rs/*under the D-0001-project-structure gradient. Nothing depends on it today outsideapps/sdlc.- Validation is empirical, not aspirational. The first validator flagged 103
of 155 real skills by requiring a
namefield. This repo’s own skills omit it deliberately — the directory is the identity. The rule was wrong, not the corpus. - The projection-plane fork is explicit debt.
agent-adapt(Rust) andagent-plugin(TS) render different non-Claude layouts. Suspenders ships on agent-adapt; the harness ops stay on agent-plugin. One of them eventually yields. apps/agentpantsis reference-only. Do not extend it; mine it.
Open questions
Section titled “Open questions”- Remote corpora. Local roots only. Published/remote skill sources are wanted eventually and are unmodeled.
- Projection-plane convergence. Whether
agent-adaptgrows the TS exporters’ targets (cursor) and layouts, oragent-pluginadopts agent-adapt’s, is unresolved — tracked as the fork above. - Frontend verification gap.
vue-tsc/nuxi typecheckcannot run locally in this repo. The Vue surface is covered by framework-free unit tests over the roster logic and by CI, not by a local typecheck. - Test/run isolation. Phase 4 launches a coding agent against a scratch project. The sandbox boundary is unspecified.
- Filename follows D-0002-entity-identifier-shape: minted id
D-A27T, slugsuspenders-skill-experience-module. Hand-authored — there is nosdlc decision createverb. - Named
suspendersprovisionally.