Skip to content

Suspenders — skill authoring as an app-frame experience module

Status: open/accepted

  • 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/agentpants stays in the tree as a reference, expected to go stale and eventually be retired.

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:

ChangeEffect
@sksizer/app-frame made an experience module a first-class unitA capability can be hosted without owning a shell
determined proved the crate / experience / product splitThe 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.

Build suspenders as an experience module in the SDLC solution, splitting the work by plane.

PlaneHomeContains
Canonical store + railsapps/sdlc/crates/suspenders-coreSQLite store (drafts, immutable versions, projected resources, destinations, per-destination sync tracking), projection, guard, additive publish, drift, adoption, discovery scan
SKILL.md renderingagent-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-pluginThe derive/model plane for the substrate’s own harness ops; exporter-layout convergence with agent-adapt deliberately deferred
Service adapterapps/sdlc/desktop/src-tauri/src/api/v1/{skill,destination}.rsFn signatures; ontogen emits HTTP, Tauri IPC, MCP and the TS client
Experience moduleapps/sdlc/experiences/suspendersNuxt layer — pages, components, the SkillClient seam
Standalone hostapps/sdlc/products/suspendersThin 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.

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.

Root kindSource
ProjectThe open project
RegisteredLocal 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.

PhaseScopeState
1Canonical store, discovery, projection, guard, publish, drift, adoptionBuilt
2Service surface + roster/destinations UI + standalone product + live-repo self-host bootstrapBuilt
3Draft authoring — editor over store drafts, detach-source, restore-versionPlanned
4Test / run a skill — terminal + agent detection, scratch projectPlanned

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.

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.

  • 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-core is a solution-tier crate. An outside-solution consumer triggers promotion to packages/rs/* under the D-0001-project-structure gradient. Nothing depends on it today outside apps/sdlc.
  • Validation is empirical, not aspirational. The first validator flagged 103 of 155 real skills by requiring a name field. 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) and agent-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/agentpants is reference-only. Do not extend it; mine it.
  • Remote corpora. Local roots only. Published/remote skill sources are wanted eventually and are unmodeled.
  • Projection-plane convergence. Whether agent-adapt grows the TS exporters’ targets (cursor) and layouts, or agent-plugin adopts agent-adapt’s, is unresolved — tracked as the fork above.
  • Frontend verification gap. vue-tsc / nuxi typecheck cannot 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, slug suspenders-skill-experience-module. Hand-authored — there is no sdlc decision create verb.
  • Named suspenders provisionally.

← Back to Decisions