Maintained by: designer role
Last updated: 2026-05-14
This document is the concrete design baseline for vstack — a platform artifact: a
standalone CLI tool and SDK that installs structured role artifacts into a project's
.github/ directory for use with GitHub Copilot Agent Mode.
It translates the architecture blueprint in docs/architecture/overview.md into
implementable interfaces, data schemas, state models, validation rules, and error
contracts. Implementation follows this document; architecture decisions are recorded in
docs/architecture/adr/.
Coordinated runs may also use a disposable handoff cache under
.vstack/memories/session/<RUN_ID>/. This cache exists only to reduce duplicated
prompt context between delegated stages; it never replaces role-owned artifacts
or .vstack/vstack.json.
An artifact (skill, agent, instruction, prompt, hook) exists in one of these states relative
to the target .github/ directory:
absent — not in manifest AND not on disk
untracked — not in manifest, but a file exists at the expected path
clean — in manifest; on-disk checksum matches manifest checksum
modified — in manifest; on-disk checksum differs from manifest checksum
missing — in manifest; expected file does not exist on disk
managed-legacy — in manifest; checksum absent; file exists but drift cannot be determined
unknown — state cannot be determined (I/O error)
State machine:
stateDiagram-v2
[*] --> absent : not installed
[*] --> untracked : file exists, not in manifest
absent --> clean : install writes file + records checksum
absent --> managed-legacy : legacy entry promoted (no checksum recorded)
untracked --> clean : install --force or --adopt-name
clean --> modified : user edits file
clean --> missing : user deletes file
clean --> clean : install --update rewrites file (same or newer version)
modified --> clean : install --force rewrites file
missing --> clean : install (re)writes file
clean --> absent : uninstall removes file
modified --> modified : uninstall skips (checksum drift; no --force)
managed-legacy --> clean : manifest upgrade --backfill (VSTACK-META footer present)
managed-legacy --> managed-legacy : manifest upgrade --backfill (no footer; entry unchanged)
managed-legacy --> clean : install --force or --force-name rewrites file + records checksum
.vstack/vstack.json — written by install, read by all other commands.
{
"manifest_version": 2,
"hash_algorithm": "sha256",
"vstack_version": "1.3.0",
"installed_at": "2026-04-26T12:34:56.000000+00:00",
"artifacts": {
"skills": [
{
"name": "architecture",
"file": "skills/architecture/SKILL.md",
"version": "1.0.1",
"checksum": "a1b2c3...",
"checksum_algorithm": "sha256"
}
],
"agents": [],
"hooks": [],
"instructions": [],
"prompts": []
}
}Top-level field contracts:
| Field | Type | Required | Notes |
|---|---|---|---|
manifest_version |
integer | yes | Must equal CURRENT_MANIFEST_VERSION (2) for most operations |
hash_algorithm |
string | yes | Current default: sha256 |
vstack_version |
string | yes | vstack version that last wrote the manifest |
installed_at |
string | yes | ISO-8601 timestamp of last write |
artifacts |
object | yes | Dict keyed by manifest type key (skills, agents, …) |
Per-artifact entry (ArtifactEntry) field contracts:
| Field | Type | Required | Notes |
|---|---|---|---|
name |
string | yes | Canonical artifact name |
file |
string | yes | Relative file path under install root (for example skills/x/SKILL.md) |
version |
string | no | Template revision token (current policy: YYYYMMDDNNN); may be absent on legacy entries |
checksum |
string | no | May be absent on legacy entries |
checksum_algorithm |
string | no | May be absent on legacy entries |
manifest_version is read before any operation that consumes manifest state. If the
value differs from CURRENT_MANIFEST_VERSION, the operation fails with:
ERROR: Legacy manifest schema detected in .vstack/vstack.json.
Run: vstack manifest upgrade --target <project-root>.
ManifestFile.read(allow_legacy=True) bypasses this gate — it is called only by
ManifestCommand when executing manifest upgrade.
CURRENT_MANIFEST_VERSION: int = 2
@dataclass
class ArtifactEntry:
name: str
file: str
version: str | None = None
checksum: str | None = None
checksum_algorithm: str | None = None
@dataclass
class Manifest:
vstack_version: str
installed_at: str
manifest_version: int = CURRENT_MANIFEST_VERSION
hash_algorithm: str = CURRENT_HASH_ALGORITHM
artifacts: dict[str, list[ArtifactEntry]] = field(default_factory=dict)
def entries_for(self, type_name: str) -> list[ArtifactEntry]: ...
def names_for(self, type_name: str) -> list[str]: ...
def files_for(self, type_name: str) -> list[str]: ...
def to_dict(self) -> dict: ...
def needs_upgrade(self) -> bool: ...
@classmethod
def from_dict(cls, data: dict) -> "Manifest": ...
def upgraded(self) -> "Manifest": ...
# Returns a new Manifest at CURRENT_MANIFEST_VERSION.
# Infers missing checksum_algorithm via _infer_algorithm_for_legacy_entry.
def with_backfilled_checksums(
self,
install_dir: Path,
) -> tuple["Manifest", list[str], list[str]]: ...
# Returns (updated_manifest, backfilled_names, skipped_names).
# For each entry with checksum=None:
# - Reads the on-disk file at install_dir / entry.file.
# - Computes SHA-256 and stores it only when the file contains a VSTACK-META footer.
# - If the file exists but lacks the footer: entry is unchanged; name added to skipped_names.
# - If the file does not exist: silently skipped (entry is already missing).
# The returned manifest is a new instance; self is not mutated.
@staticmethod
def _infer_algorithm(*, entry: dict, fallback_algorithm: str, manifest_version: int) -> str | None: ...
@staticmethod
def _infer_algorithm_for_legacy_entry(checksum: str, fallback_algorithm: str) -> str | None: ...
class ManifestFile:
def __init__(self, parent_dir: Path) -> None: ...
read_error: str | None
def read(self, *, allow_legacy: bool = False) -> Manifest | None: ...
# Returns None for missing/invalid/legacy (unless allow_legacy=True).
# Stores message in read_error for user-facing diagnostics.
def write(self, manifest: Manifest) -> None: ...
# Atomic write: stages to <path>.tmp, then os.replace -> <path>.
# Never leaves .vstack/vstack.json in a partially-written state on POSIX.
def exists(self) -> bool: ...@dataclass
class ArtifactTypeConfig:
type_name: str
templates_dir: str
output_subdir: str
output_pattern: str
add_frontmatter: bool
artifact_is_dir: bool = False
partials_subdir: str | None = "_partials"
template_filename: str = "template.md"
config_filename: str = "config.yaml"
auto_gen_footer: bool = False
placeholders: dict[str, str] = field(default_factory=dict)
fail_on_unresolved: bool = False
frontmatter_schema: FrontmatterSchema | None = None
preserve_multiline_frontmatter: bool = False
manifest_key: str = ""
@dataclass
class RenderedArtifact:
name: str
content: str
source_path: Path
frontmatter: dict | None = None
unresolved: list[str] = field(default_factory=list)
@dataclass
class ArtifactResult:
artifacts: list[RenderedArtifact]
unresolved_warnings: list[str]
verification: ValidationResult
@property
def ok(self) -> bool: ...
class GenericArtifactGenerator:
def __init__(self, type_config: ArtifactTypeConfig, templates_root: Path) -> None: ...
# Static helpers
@staticmethod
def resolve_placeholders(text: str, resolvers: dict[str, str]) -> str: ...
@staticmethod
def find_unresolved(text: str) -> list[str]: ...
@staticmethod
def parse_generation_metadata(text: str) -> dict[str, str] | None: ...
# Discovery + loading
def load_partials(self) -> dict[str, str]: ...
def find_templates(self) -> list[Path]: ...
def find_extra_files(self, tmpl_dir: Path) -> list[Path]: ...
def load_artifact_config(self, tmpl_dir: Path) -> dict: ...
# Rendering + output
def render(self, tmpl_dir: Path) -> RenderedArtifact: ...
def render_all(self) -> list[RenderedArtifact]: ...
def output_path(self, name: str) -> str: ...
def install_relative_path(self, name: str) -> str: ...
def generate(self, output_dir: Path) -> ArtifactResult: ...
# Validation
def verify_input(self, expected_names: list[str] | None = None) -> ValidationResult: ...
def verify_output(self, output_dir: Path, expected_names: list[str] | None = None) -> ValidationResult: ...class CommandLineInterface:
def __init__(
self,
*,
parser_cls: type[CommandLineParser] = CommandLineParser,
service_cls: type[CommandService] = CommandService,
templates_root,
) -> None: ...
@classmethod
def resolve_only_for_scope(cls, args: object) -> list[str] | None: ...
def run(self) -> int: ...
# Parses argv via argparse and dispatches selected command.
# Top-level ValueError handling is in src/vstack/main.py.
class CommandService:
def __init__(
self,
templates_root: Path,
*,
items_root: str = ARTIFACTS_DOCS_ROOT,
workflow_stages: list[dict[str, str]] | None = None,
workflow_mode: str = "agentic",
hook_default_mode: str = "audit",
hook_default_log_level: str = "minimal",
hook_log_retention_days: int = 7,
hook_log_dir: str = ".vstack/logs",
...
) -> None: ...
generators: list[GenericArtifactGenerator]
def label(self, path: Path) -> str: ...
# Returns path relative to template root when possible.
def manifest_for(self, install_dir: Path) -> ManifestFile: ...
# Returns ManifestFile handle for .vstack/vstack.json in project installs.
def artifact_control_state(
self,
*,
out_file: Path,
existing_entry,
) -> tuple[str, str]: ...
# Returns (state, detail_message).
# state: "managed" | "managed-legacy" | "modified" | "missing"
# | "untracked" | "absent" | "unknown"
# On OSError: returns ("unknown", str(error)) — never raises.
def gen_for(self, artifact_type: str) -> GenericArtifactGenerator | None: ...
class BaseCommand(ABC):
@abstractmethod
def run(
self,
*,
context: CommandContext,
) -> int: ...
# Returns 0 on success, non-zero on errors.
# CommandContext carries args, install_dir, and only.
COMMAND_CATALOG: dict[str, CommandConfig]
# Declares parser registration and command factories for install/verify/status/...The tables below are the authoritative quick-reference for implementers. For full authoring guidance, field semantics, examples, and template structure, see the dedicated docs:
- Agents → agents.md
- Hooks → hooks.md
- Skills → skills.md
- Instructions → instructions.md
| Field | Type | Required | Constraints |
|---|---|---|---|
name |
string | yes | Lowercase kebab-case; max 64 chars; must match directory name |
version |
string | yes | Template revision token (YYYYMMDDNNN); used for manifest tracking only — not emitted to generated SKILL.md |
description |
string | yes | Max 1024 chars; what the skill does and when to invoke it |
license |
string | no | SPDX identifier |
compatibility |
string | no | Free text compatibility note |
metadata.owner |
string | no | — |
metadata.maturity |
string | no | "stable" | "beta" | "experimental" |
argument-hint |
string | no | Shown after /skill-name in chat input |
user-invocable |
bool | no | Default true; false hides skill from slash-command menu |
disable-model-invocation |
bool | no | Default false; true prevents Copilot from auto-loading this skill |
| Field | Type | Required | Notes |
|---|---|---|---|
name |
string | no | Overrides filename as picker label |
description |
string | no | Placeholder text in chat input |
argument-hint |
string | no | Hint text shown after @agent in chat |
tools |
list | no | read, search, edit, execute, web, vscode, todo, agent |
agents |
list | no | Subagents this agent may invoke; ["*"] = all |
model |
string or list | no | Force a specific model or list of models; omit to allow user selection |
user-invocable |
bool | no | Default true |
disable-model-invocation |
bool | no | Default false; true prevents other agents calling this one |
target |
string | no | "vscode" (default) or "github-copilot" |
handoffs |
object-list | no | Sequential workflow handoff steps |
mcp-servers |
raw | no | MCP server config — github-copilot target only |
hooks |
raw | no | Chat hooks (preview feature) |
metadata |
raw | no | String key/value annotations — github-copilot target only |
version |
string | no | Internal only — never emitted. vstack change-tracking only |
| Field | Type | Required | Notes |
|---|---|---|---|
name |
string | yes | Matches directory name; kebab-case |
description |
string | yes | When this instruction applies |
applyTo |
string | no | Glob pattern e.g. "**/*.py" or "**/*" |
| Field | Type | Required | Notes |
|---|---|---|---|
name |
string | yes | Matches directory name; kebab-case |
description |
string | yes | What this prompt does |
mode |
string | no | "ask" | "edit" | "generate" |
Placeholders use {{TOKEN}} syntax. Resolution is literal string substitution — no
logic, no loops. Unresolved tokens are errors in validate and in output verification
for types with fail_on_unresolved=true; install logs unresolved tokens as warnings.
Partials live in src/vstack/_templates/skills/_partials/*.md. File stem is converted
from lowercase-kebab to UPPER_SNAKE to form the token:
skill-context.md → {{SKILL_CONTEXT}}.
| Token | Source file | Used by |
|---|---|---|
{{SKILL_CONTEXT}} |
skill-context.md |
All skills — shared opening block |
{{BASE_BRANCH}} |
base-branch.md |
Skills that reference git diff |
{{RUN_TESTS}} |
run-tests.md |
Skills that run tests |
{{OBSERVABILITY_CHECKLIST}} |
observability-checklist.md |
verify, architecture |
Resolution rules:
- Load all partial files for the artifact type (skills only).
- For each partial, substitute all occurrences of
{{STEM_AS_UPPER_SNAKE}}in the template body. - After all substitutions, scan for any remaining
{{...}}patterns. - Remaining unresolved tokens are attached to
RenderedArtifact.unresolvedand are reported byvalidate,install, andverifyflows.
Every generated artifact ends with this block when auto_gen_footer=True:
<!-- AUTO-GENERATED — maintained by vstack, do not edit directly -->
<!-- VSTACK-META: {"artifact_name":"…","artifact_type":"…","artifact_version":"…","generator":"vstack","vstack_version":"…"} -->
- Human-readable line: constant in
src/vstack/artifacts/constants.py - Machine-readable
VSTACK-METAline: emitted byGenericArtifactGenerator._build_footer - Markdown artifact types (
skill,agent,instruction,prompt) useauto_gen_footer=True; JSON hooks useauto_gen_footer=False
vstack [--version] [--help] <command> [options]Exit codes across all commands:
| Code | Meaning |
|---|---|
0 |
Success; all requested operations completed without conflict |
1 |
Operational failure: validation error, conflict, schema mismatch, I/O error |
2 |
Invalid argument syntax (raised by argparse, not vstack) |
Error and warning output is command-specific:
stdout: status summaries, install/uninstall progress, verify checks
stderr: parser errors and selected command failures (for example legacy manifest read errors)
--no-color suppresses ANSI codes.
Render all templates in memory; report unresolved tokens. No files written.
vstack validate [--only <type>...]| Flag | Description |
|---|---|
--only <type>... |
Restrict to specific artifact types, e.g. --only skill |
Output: one line per error. Exit 0 = all valid, exit 1 = one or more errors.
Validate source templates and/or installed output (schema, tokens, presence, checksum drift).
vstack verify [--target <dir>] [--global] [--no-source] [--no-output]Checks in order:
- Source template schema validation (unless
--no-source) - Source template token resolution (unless
--no-source) - Output file presence against manifest (unless
--no-output) - Checksum drift against manifest (unless
--no-output)
Read-only artifact state report. No files written.
vstack status [--target <dir>] [--global] [--only <type>]
[--format text|json|yaml] [--verbose] [--no-color]text output (default) contains per-type summary lines and optional entry lines:
skill:architecture clean
skill:debug modified (checksum drift)
agent:engineer missing
json output shape:
{
"ok": true,
"install_dir": "/path/to/.github",
"types": [
{
"type": "skill",
"counts": {
"managed": 27,
"managed_legacy": 0,
"modified": 0,
"missing": 0,
"unknown": 0,
"untracked": 0,
"absent": 0
},
"issues": 0,
"warnings": 0,
"entries": []
}
],
"summary": {
"issues": 0,
"warnings": 0,
"types_checked": 1
}
}Exit 0 = all artifacts clean; exit 1 = any artifact not clean.
Write artifact files to the target directory and record checksums in .vstack/vstack.json.
vstack install [--target <dir>] [--global] [--only <type>...]
[--force] [--force-name <name>] [--adopt-name <name>]
[--update] [--dry-run]Install decision matrix per artifact:
| File state | default | --force |
--update |
--force-name <n> |
--adopt-name <n> |
|---|---|---|---|---|---|
| absent | write ✓ | write ✓ | write ✓ | write ✓ | write ✓ |
| untracked | skip | overwrite | skip | overwrite (named) | adopt (named) |
| clean | write ✓ | write ✓ | write ✓ | overwrite (named) | — |
| modified | skip | overwrite | skip | overwrite (named) | — |
| missing | write ✓ | write ✓ | write ✓ | write ✓ | — |
--force and --update are mutually exclusive.
--dry-run prints what would change without writing any files; exit code reflects what
would have happened.
Remove artifacts recorded in .vstack/vstack.json. Modified files are preserved by default.
vstack uninstall [--target <dir>] [--global] [--only <type>...]
[--force] [--force-name <name>]Removal rules:
clean→ deletedmodified→ skipped (unless--forceor--force-name)missing→ entry removed from manifest, no file operation
Same contracts as top-level status and verify, scoped to manifest-tracked artifacts.
Migrate legacy .vstack/vstack.json (version < 2) to manifest_version: 2.
vstack manifest upgrade [--target <dir>] [--backfill]This is the only command that calls ManifestFile.read(allow_legacy=True).
| Flag | Description |
|---|---|
--backfill |
Compute and store SHA-256 checksums for tracked entries with no checksum, gated by VSTACK-META identity check. Opt-in; no default. |
Without --backfill, manifest upgrade performs a schema-only migration: manifest_version is promoted to 2, hash_algorithm is set, and missing checksum_algorithm values are inferred for entries that already carry a checksum. No checksums are computed or written.
With --backfill, the call sequence after schema migration is:
manifest.upgraded()— schema migration (always).manifest.with_backfilled_checksums(install_dir)— checksum backfill (only when--backfillis set).manifest_file.write(result)— atomic write of the final manifest (ADR-016).
--backfill is a one-way operation: current on-disk content becomes the canonical checksum baseline for backfilled entries. Entries whose files contain the VSTACK-META footer are promoted from managed-legacy to clean. Entries whose files exist but lack the footer remain managed-legacy; a warning is emitted for each. Entries whose files are absent are silently skipped.
| Exit code | Meaning |
|---|---|
0 |
Manifest upgraded (and backfilled if --backfill; partial backfill is OK) |
1 |
Manifest missing, unreadable, or parse error |
| Condition | Runtime behavior |
|---|---|
Missing .vstack/vstack.json |
ManifestFile.read() returns None; read_error is None |
Invalid .vstack/vstack.json JSON |
ManifestFile.read() returns None; read_error = invalid format message |
| Legacy schema on read | ManifestFile.read() returns None; read_error = upgrade guidance |
| Unknown artifact type lookup | CommandService.gen_for() returns None |
| File I/O during state checks | artifact_control_state() returns ("unknown", message) |
| CLI scope validation failures | ValueError raised in scope resolver; handled in src/vstack/main.py |
Top-level error mapping for CLI process exit lives in src/vstack/main.py.
- Validation results are typically collected in
ValidationResult/CheckMessageand rendered at command level. - Operational failures are reported either through return codes and stderr output or
via
read_errordiagnostics on manifest reads.
flowchart TD
A[Discover template directories<br>skip underscore-prefixed dirs] --> B[Load per-template source]
B --> C[Validate frontmatter against schema]
C --> D[Load partials when configured]
D --> E[Resolve placeholder tokens]
E --> F{Unresolved tokens?}
F -->|yes| Z[Marked on RenderedArtifact.unresolved]
F -->|no| G[Inject frontmatter and auto-gen footer]
G --> H{Command mode}
H -->|install| I[Write output via ManifestFile.write — atomic]
H -->|validate or verify| J[Report errors; no files written]
flowchart TD
A[sys.argv] --> B[CommandLineParser.build]
B --> C[args = parser.parse_args]
C --> D[CommandService created with templates_root]
D --> E[COMMAND_CATALOG → name→BaseCommand map]
C --> F[resolve install_dir and only scope]
E --> G[command.run(*, context=CommandContext(args, install_dir, only))]
F --> G
G --> H[BaseCommand subclass executes]
H --> I[service.generators / service.label / service.manifest_for / service.artifact_control_state]
| Module | Class / function | Responsibility |
|---|---|---|
interface.py |
CommandLineInterface |
Facade: parser construction, service creation, target/scope resolution, dispatch |
catalog.py |
COMMAND_CATALOG |
Maps command names to parser metadata and BaseCommand factories |
service.py |
CommandService |
Shared coordinator: generators, path labelling, manifest access, artifact state |
base.py |
BaseCommand |
ABC: all handlers implement run(*, context: CommandContext) → int |
install.py |
InstallCommand |
Install flow: per-artifact write, checksum recording, dry-run, force/adopt/update modes |
verify.py |
VerifyCommand |
Source + output verification: schema, tokens, presence, checksum drift |
status.py |
StatusCommand |
Read-only report across text, JSON, and YAML output formats |
uninstall.py |
UninstallCommand |
Remove tracked artifacts; preserve modified files unless explicitly forced |
validate.py |
ValidateCommand |
In-memory template render and token resolution check; no files written |
manifest.py |
ManifestCommand |
Dispatch manifest status / verify / upgrade subcommands |
parser.py |
CommandLineParser |
Build and configure the argparse argument tree |
helpers.py |
Module-level functions | Shared install/uninstall helpers: name normalization, manifest preservation utilities |
constants.py |
Colors, KNOWN_TYPES, … |
ANSI codes, artifact-type registry, canonical name lists, expected-name guards |
- Minimal runtime dependencies. Runtime uses Python >= 3.11 stdlib plus one external dependency:
pyyaml>=6.0. - Templates are source of truth. No generated files live in
src/vstack/_templates/. - Flat resolver. No template inheritance, no conditionals — literal substitution only.
- Install-time output. Generated artifacts belong under
.github/; never committed to the vstack source repo. - Idempotent. Running the generator twice with the same
--targetproduces identical output. - Conservative by default. Untracked and modified files are never overwritten without an explicit escape-hatch flag.
- Atomic manifest writes.
.vstack/vstack.jsonis never left in a partially-written state (staged via.tmp+os.replace). - Explicit upgrade gate. Legacy manifest schemas are rejected with an actionable hint; never silently migrated.