☰
VSuite

Architecture

vsuite is a pnpm monorepo. Only the CLI, @krizic/vsuite, is published; every other package is a private, source-only workspace package that is bundled into the CLI at build time.

Overview

Directory Package Published Purpose
packages/json @vsuite/json No JSON object helpers (deepMerge, isJsonObject).
packages/migrations @vsuite/migrations No Versioned vsuite.json migrations (migrateConfig).
packages/catalog @vsuite/catalog No Agent, skill, capability, and target definitions; neutral tool vocabulary; MCP server catalog; stack registry.
packages/templates @vsuite/templates No Restricted LiquidJS renderer, built-in agent/skill templates and vsuite:* partials (templates/), Markdown lint, job-metrics and metrics scripts.
packages/packs @vsuite/packs No Template packs: source fetch (path:, npm:, git:), manifest/front-matter validation, vsuite.lock.json integrity, registry merge with the catalog, pack entry rendering.
packages/config @vsuite/config No vsuite.json zod schema and validation, derived agent graph, reader, JSON Schema export.
packages/detection @vsuite/detection No Stack detection from project manifests (npm, Python, Go, Maven, Gradle, .NET).
packages/targets @vsuite/targets No Per-target adapters (OpenCode, Claude Code, GitHub Copilot) and OpenCode config merge.
packages/core @vsuite/core No loadConfig (migrate, validate, write), planGeneration and generate: rendering, the atomic write plan, .vsuite/.env handling, and legacy metrics migration.
packages/workflows @vsuite/workflows No Headless wizard model, stack changes, graph rendering, and project workflows shared by the CLI and the UI.
packages/cli @krizic/vsuite Yes The vsuite binary (init, generate, migrate, graph, stack, pack, agent, skill) and the published JSON Schema.
packages/ui @vsuite/ui No Electron desktop app; app at the top of the graph, never bundled into the CLI.
packages/test-utils @vsuite/test-utils No Dev-only test helpers; never bundled.

Dependency graph

graph TD
  ui["@vsuite/ui (app)"] --> workflows & config & catalog & detection & packs
  cli["@krizic/vsuite (cli)"] --> workflows & core & config & catalog & detection
  workflows["@vsuite/workflows"] --> core & config & catalog & detection & packs & templates
  core["@vsuite/core"] --> catalog & templates & config & targets & json & migrations & packs
  packs["@vsuite/packs"] --> catalog & config & templates
  migrations["@vsuite/migrations"] --> json
  targets["@vsuite/targets"] --> catalog & json
  templates["@vsuite/templates"] --> catalog
  config["@vsuite/config"] --> catalog
  detection["@vsuite/detection"] --> catalog
  catalog["@vsuite/catalog"]
  json["@vsuite/json"]

@vsuite/test-utils is a dev dependency of package tests only and is not part of the runtime graph.

Layering rules

  • No cycles. The graph above is the complete allowed set of edges. A new edge is a design change, not a convenience. Allowed edges: ui → workflows, config, catalog, detection, packs (main process only); cli → workflows, core, config, catalog, detection; workflows → core, config, catalog, detection, packs, templates; core → catalog, templates, config, targets, json, migrations, packs; packs → catalog, config, templates; migrations → json; targets → catalog, json; templates → catalog; config → catalog; detection → catalog.
  • Renderer isolation. In packages/ui, only the main process (src/main) may import @vsuite/* at runtime. src/renderer, src/preload, and src/shared may use import type or inline import("@vsuite/…") types only, so no workflow code reaches the sandboxed renderer bundle.
  • Index-only imports. Cross-package imports use the package entry (from "@vsuite/<name>"), never deep paths such as @vsuite/catalog/src/.... Each package’s src/index.ts is its public API.
  • Where new code goes. Put a symbol in the lowest package that every consumer already depends on. Shared types used by both templates and core (for example RenderableSkill) live in catalog. If a change seems to need a new edge, move the symbol down instead of adding the dependency.

Run the boundary checks from the repository root:

# 1. No deep or escaping imports (expect deep=1 escape=1)
grep -rnE 'from "@vsuite/[a-z-]+/' packages tests ; echo "deep=$?"
# Resolves every relative import specifier (`from "./…"`, `import("./…")`) in
# packages/*/{src,tests,e2e} and fails if it lands outside its own packages/<pkg>/.
# Non-import reads such as new URL("../../../package.json", import.meta.url)
# (root package.json version reads) are allowed and not scanned.
node -e '
const fs=require("fs"),p=require("path");let bad=0;
const walk=d=>fs.existsSync(d)?fs.readdirSync(d,{withFileTypes:true}).flatMap(e=>e.name==="node_modules"?[]:e.isDirectory()?walk(p.join(d,e.name)):/\.[cm]?tsx?$|\.[cm]?js$/.test(e.name)?[p.join(d,e.name)]:[]):[];
for(const pkg of fs.readdirSync("packages")){const root=p.resolve("packages",pkg);
for(const f of ["src","tests","e2e"].flatMap(s=>walk(p.join(root,s)))){const t=fs.readFileSync(f,"utf8");
for(const m of t.matchAll(/(?:\bfrom\s*|\bimport\s*\(\s*)["\x27](\.{1,2}\/[^"\x27]*)["\x27]/g)){
const r=p.resolve(p.dirname(f),m[1]);if(r!==root&&!r.startsWith(root+p.sep)){bad=1;console.log(f+": "+m[1])}}}}
console.log("escape="+(bad?0:1))'
 
# 2. Actual edges per package (must be a subset of the graph above)
for p in json migrations catalog templates config packs detection targets core workflows cli; do
  echo "$p: $(grep -rhoE 'from "@vsuite/[a-z-]+"' packages/$p/src | sort -u | sed 's/from //' | tr '\n' ' ')"
done
echo "ui(main): $(grep -rhoE --exclude='*.test.ts' --exclude='test-context.ts' 'from "@vsuite/[a-z-]+"' packages/ui/src/main | sort -u | sed 's/from //' | tr '\n' ' ')"
 
# 3. No runtime @vsuite/* imports outside the UI main process (expect runtime-vsuite-in-renderer=1)
grep -rnE --exclude='*.test.ts' --exclude='*.test.tsx' '^import (\{|\*|[A-Za-z_$][A-Za-z0-9_$]* (,|from))[^;]* from "@vsuite/' packages/ui/src/renderer packages/ui/src/preload packages/ui/src/shared ; echo "runtime-vsuite-in-renderer=$?"

Check 3 matches value, namespace, and default imports but not import type …. Test files are excluded from checks 2 (UI) and 3: they are never bundled and may import @vsuite/workflows or @vsuite/test-utils at runtime.

Generator output is guarded by golden tests: packages/templates/tests/golden/ (every catalog agent and skill) and packages/core/tests/golden.test.ts (a migrated 0.2.1 config generates equivalent output).

End-to-end data flow

flowchart LR
  answers["vsuite init answers"] --> json["vsuite.json"]
  json --> migrate["core: loadConfig → migrations: migrateConfig<br/>(atomic write if changed)"]
  migrate --> config["config: parseConfig / readConfig + configSchema"]
  config --> registry["packs: loadRegistry<br/>(fetch, verify vsuite.lock.json, merge with catalog)"]
  registry --> resolve["catalog: resolveSelection"]
  resolve --> ctx["templates: TemplateContext v1"]
  ctx --> skillsNode["core: loadProjectSkills + Liquid skill render<br/>(catalog or packs: renderSkillEntry)"]
  ctx --> bodies["Liquid agent render + auto-appended required sections<br/>(catalog or packs: renderAgentEntry)"]
  skillsNode --> bodies
  bodies --> targets["targets: adapters[target].render"]
  skillsNode --> skillOut["core: SKILL.md outputs"]
  resolve --> scripts["templates: vsuite MCP server + job-metrics tooling"]
  config --> native["targets: mergeOpenCodeConfig + MCP config merge"]
  targets --> plan["core: writePlan"]
  skillOut --> plan
  scripts --> plan
  native --> plan
  plan --> disk["files on disk"]

Before generating, the CLI calls loadConfig (packages/core/src/load-config.ts), which migrates the raw JSON to the CLI version with migrateConfig, validates it with parseConfig, and writes it atomically only if the migration changed it; migration therefore always precedes validation. generate(root) in packages/core/src/generate.ts then reads and validates the config, loads configured packs with loadRegistry (verifying vsuite.lock.json: path: mismatches warn and relock, npm:/git: mismatches fail with PACK_INTEGRITY unless --update-packs) and merges them with the catalog, resolves the selection (expanding required skills and capabilities), checks that every selected artifact supports every configured target, loads project skills from projectSkillsDirectory, renders one agent output per selected agent and target and one SKILL.md per skill and target through the restricted LiquidJS engine in @vsuite/templates (catalog and pack templates share one engine, the template context, and the auto-append of required sections), writes the vsuite MCP server and job-metrics tooling under .vsuite/, and merged native config (.opencode/opencode.json, .mcp.json, .vscode/mcp.json). writePlan then rejects unsafe or duplicate paths and paths beneath symbolic links, stages every file to a temporary path, and renames them into place. Afterwards generate ensures .vsuite/ is git-ignored, removes stale catalog skills and pack outputs, and writes vsuite.lock.json when it changed.

Build and bundling

flowchart LR
  src["@vsuite/* packages<br/>exports → ./src/index.ts"] --> tsup["tsup in packages/cli<br/>noExternal: /^@vsuite\//"]
  cliSrc["packages/cli/src/main.ts"] --> tsup
  tsup --> dist["packages/cli/dist/main.js"]
  thirdParty["@clack/prompts, diff, jsonc-parser, liquidjs, yaml, zod"] -. "external, CLI dependencies" .-> dist
  rootPkg["root package.json version"] -. "define __VSUITE_VERSION__" .-> tsup
  schemaScript["pnpm run schema"] --> schema["packages/cli/schema/vsuite.schema.json"]
  • Internal packages have no build step; their exports point at ./src/index.ts, and TypeScript, Vitest, and tsx consume source directly.
  • tsup inlines every @vsuite/* import into dist/main.js. Third-party runtime packages stay external and are declared as CLI dependencies.
  • @vsuite/* packages are CLI devDependencies and private, so the published manifest never references unpublished packages. Keeping them private avoids versioning and publishing packages whose only consumers are the CLI and the desktop app.
  • The vsuite version lives in the root package.json. packages/cli/tsup.config.ts reads it and injects it with define as __VSUITE_VERSION__; packages/cli/src/version.ts uses that constant in the bundle and falls back to reading the root package.json in source mode (tsx, Vitest). pnpm run version:sync (scripts/sync-version.mjs) copies the version into packages/cli/package.json; a test enforces equality.
  • The published package contains only dist and schema; its exports are the vsuite binary and @krizic/vsuite/schema/vsuite.schema.json. There is no programmatic API.

Desktop app

packages/ui is an Electron app built with electron-vite (see its README). Like tsup for the CLI, electron-vite inlines every @vsuite/* import into the main-process bundle in packages/ui/out/, so the packaged app has no runtime node_modules dependency on workspace packages. Preload and renderer bundles contain no @vsuite/* code (renderer isolation rule above). App data lives in <userData>/app.db (node:sqlite, versioned migrations); VSUITE_USER_DATA overrides userData for tests.

pnpm run ui:dist runs electron-builder (packages/ui/electron-builder.yml) and writes installers to packages/ui/release/: dmg and zip (macOS), nsis (Windows), AppImage and deb (Linux). The packaging path runs packages/ui/scripts/obfuscate.mjs between electron-vite build and electron-builder to obfuscate the main bundle and renderer app-owned chunks, and the app ships with Electron fuses + asar integrity enabled. Release installers are built by the Gitea deploy and release workflows (below); .github/workflows/ui.yml additionally runs typecheck, Biome, tests, Playwright e2e, and ui:dist on a macOS, Windows, and Ubuntu matrix. The macOS build is ad-hoc signed (identity: "-") and not notarized, so it is not distributable to other machines without a Gatekeeper override; Windows and Linux installers are unsigned. There is no auto-update.

Release flow

Releases run on Gitea Actions.

.gitea/workflows/deploy.yml runs on every push to main or master and on manual dispatch:

flowchart LR
  quality["quality<br/>typecheck"] --> test["test<br/>build + pnpm test"] --> version["version<br/>bump patch, tag vX, push"]
  version --> npm["npm<br/>prepublishOnly + publish @krizic/vsuite"]
  version --> linux & windows & macos
  linux & windows & macos --> dl["publish-dl<br/>dl/vsuite/vX/ then latest/"]
  docsBuild["docs-build<br/>scripts/build-docs.sh"] --> docsPublish["docs-publish"]
  • version bumps the patch version in the root package.json, runs scripts/sync-version.mjs, commits chore: release <version> [skip ci] (root, CLI, and UI package.json), creates an annotated tag v<version>, and pushes both atomically. The [skip ci] marker prevents a release loop.

  • linux and windows build installers on Ubuntu with scripts/build-desktop.sh. macos needs a self-hosted runner and runs only when the repository variable MACOS_RUNNER is true; see the macOS runner guide.

  • publish-dl uploads the installers with SHA256SUMS to https://dl.vedrankrizic.com/vsuite/<tag>/ (for example v0.7.1/) and then mirrors them to https://dl.vedrankrizic.com/vsuite/latest/. It runs only when every enabled build succeeded, then finishes latest/ in separate steps, in this order: the installers are copied into latest/ without deleting anything, then the permanent aliases vsuite-mac.dmg, vsuite-win.exe and vsuite-linux.AppImage are uploaded, then latest/manifest.json, and last a prune that deletes the previous release’s installers. Nothing is deleted from latest/ until the new manifest is live, so neither the old nor the new manifest ever names a missing file. If a step fails, latest/ holds both old and new files, with no broken links, until a re-run. scripts/write-manifest.mjs picks one installer per platform from the downloaded artifacts and uses that same selection for the aliases and the manifest. An alias keeps the selected installer’s extension when it differs (for example vsuite-mac.zip). The prune never deletes manifest.json or vsuite-{mac,win,linux}.*. The alias step replaces the aliases of every built platform and removes their other-extension variants. The manifest has the shape { "latestVersion": "0.7.3", "mac": "vsuite-0.7.3.dmg", "win": "vsuite Setup 0.7.3.exe", "lin": "vsuite-0.7.3.AppImage" }, with values that are file names inside latest/. It is published only to latest/.

    Stale macOS: when the macos job is skipped, the previous latest/vsuite-mac.* alias is kept, so it is an older build than latestVersion. The write step checks latest/ over HTTP for that alias, and the manifest’s mac names it ("vsuite-mac.dmg"), or is null when there is none or the check fails. The next release with a macOS build replaces it. After the prune, the job ends with a smoke check that requires HTTP 200 for SHA256SUMS (versioned and latest/), latest/manifest.json, every alias written in this run, and every file the manifest names. rsync cannot set cache headers, so a short cache for manifest.json and the aliases has to be configured on the web server.

  • The docs site is built and published independently of the release chain.

.gitea/workflows/release.yml rebuilds and republishes an existing tag without bumping the version. It runs when a tag v* is pushed by a person (tags pushed by the Actions token do not trigger workflows) or on manual dispatch with a tag; npm publishing is optional on dispatch. Dispatching it with the previous good tag is the rollback path for latest/.

The secrets and variables each workflow needs are listed in the header comment of deploy.yml.

Development

pnpm install
pnpm test                                  # all Vitest projects
pnpm vitest run --project <name>           # one project, e.g. @vsuite/core, @krizic/vsuite, root
pnpm run typecheck
pnpm run check                             # Biome lint and format check
pnpm run build                             # bundle packages/cli/dist/main.js
pnpm vsuite <command>                      # run the CLI from source
pnpm run schema                            # regenerate the published JSON Schema and the template context schema
pnpm run version:sync                      # copy the root version into packages/cli/package.json
pnpm run ui:dev                            # run the desktop app in development
pnpm run ui:dist                           # package installers into packages/ui/release/

To try the CLI from source in another project, run pnpm vsuite <command> from this repository or install a packed tarball (see the CLI README).

The root package.json version is the single source of the vsuite version. pnpm run prepublishOnly runs schema generation, tests, type check, and build.

Documentation site

The docs site is built from the repository’s Markdown with @krizic/static-docs (static-docs.config.json; docs/superpowers/ is excluded).

pnpm docs:build          # write ./docs-build
pnpm docs:serve          # serve it locally
pnpm docs:screenshots    # rebuild the UI and recapture docs/images/*.png

pnpm docs:screenshots runs scripts/capture-screenshots.mjs: it launches the built Electron app on a throwaway profile with sample projects and captures the screenshots used in the root README at 1440x900.

Historical design records

docs/superpowers/ holds dated specs, plans, research, and reviews written while features were designed. They record intent at the time and are not maintained; where they disagree with the code or the live docs, the code and the live docs win.

Extending

Most additions are data in @vsuite/catalog plus a Liquid template in packages/templates/templates/ (see @vsuite/templates). @vsuite/config derives its enums from the catalog id lists, so the schema follows automatically; regenerate the published schema with pnpm run schema.

Add a target

  1. packages/catalog/src/types.ts: add the id to targetIds.
  2. packages/catalog/src/artifacts.ts: add its directory to targetDirectories, and declare support on the artifacts that support it.
  3. packages/targets/src/<target>.ts: implement TargetAdapter (id, render(input) returning { files, warnings } from an AgentProjection), then register it in adapters in packages/targets/src/adapters.ts and export it from src/index.ts.
  4. packages/core/src/generate.ts: add target-specific skill metadata in skillOutput and native/MCP config handling if the target needs them.
  5. packages/templates/src/scripts.ts: support the target in renderMetricsScript if it gets the metrics script.
  6. Tests: a new packages/targets/tests/<target>.test.ts, plus packages/core/tests/generate.test.ts and packages/config/tests/* for the new id.

Add an agent

  1. packages/catalog/src/types.ts: add the id to agentIds.
  2. packages/catalog/src/artifacts.ts: add its AgentDefinition to agents (identity, visibility, handoffs, and the defaults the wizard writes into vsuite.json).
  3. packages/templates/templates/agents/<id>.md.liquid: add its body template. Required sections (telemetry, stack, skills, metrics, memory) are appended automatically unless the template places them with {% render "vsuite:<name>" %}; the variables available are listed in the context reference.
  4. Recapture goldens with pnpm --filter @vsuite/templates golden:capture (the golden matrix picks the new id up from the catalog) and review the new files under packages/templates/tests/golden/.
  5. Tests: packages/catalog/tests/resolve.test.ts, packages/templates/tests/render.test.ts, and packages/core/tests/generate.test.ts.

Add a skill

  1. packages/catalog/src/types.ts: add the id to skillIds.
  2. packages/catalog/src/artifacts.ts: add its SkillDefinition to skills and reference it from agents’ requiredSkills where needed.
  3. packages/templates/templates/skills/<id>.md.liquid: add its body template (skills get no appended sections).
  4. Recapture goldens with pnpm --filter @vsuite/templates golden:capture and review the diff.
  5. Tests: packages/catalog/tests/resolve.test.ts, packages/templates/tests/render.test.ts, and packages/core/tests/generate.test.ts.

Project-specific skills need no code change; see project skills in the CLI reference.

Add an MCP server

  1. packages/catalog/src/mcp-servers.ts: add the id to mcpServerIds and a McpServerDefinition (stdio launch or remote url, short tool names without dots, configKey, Copilot prefix, requiredEnv, optional optIn and prerequisite). Check the package name and command against the vendor docs first.
  2. Targets translate it automatically. Add a named warning in packages/targets/src/warnings.ts only if a target cannot express it.
  3. Tests: packages/catalog/tests/vocabulary.test.ts, packages/config/tests/schema.test.ts, and packages/core/tests/graph-projection.test.ts.

Add a stack technology

  1. packages/catalog/src/stack-registry.ts: add a StackTechnology with id, category, label, detect (dependency, module, artifact, or SDK names; order within a category is detection precedence), skill (<id>-conventions), and mcps bindings (ownerTools, readerTools or null, optional readerServer for a read-only launch variant).
  2. Add the skill id to packages/catalog/src/convention-skills.ts, and its body with the eight required sections as packages/templates/templates/skills/conventions/<id>-conventions.md.liquid; then recapture goldens with pnpm --filter @vsuite/templates golden:capture.
  3. If the technology needs a new manifest format, extend packages/detection/src/ecosystems.ts.
  4. Ownership lives in stackOwnership (packages/catalog/src/resolve-stack.ts). The wizard and vsuite stack set both apply resolveStack through packages/workflows/src/stack.ts.
  5. Tests: packages/catalog/tests/stack-registry.test.ts, packages/templates/tests/convention-skills.test.ts, and packages/detection/tests/stack.test.ts.

Add a config migration

Needed when a release changes the vsuite.json format; see @vsuite/migrations.

  1. Root package.json: the migration version is the vsuite release from here. Bump it if needed and run pnpm run version:sync.
  2. packages/migrations/src/migrations/<version>.ts: export a Migration whose version is that release.
  3. packages/migrations/src/registry.ts: add it to migrations in version order.
  4. packages/config/src/schema.ts: update the schema for the new format and regenerate with pnpm run schema.
  5. Tests: a per-migration test and a legacy-fixture test in packages/migrations/tests/; the registry guard test fails if a migration is newer than the root version.
  6. Docs: the migrations README and any vsuite.json examples.