diff --git a/.github/agents/architect.agent.md b/.github/agents/architect.agent.md
index f636b28..a261849 100644
--- a/.github/agents/architect.agent.md
+++ b/.github/agents/architect.agent.md
@@ -71,7 +71,7 @@ You are a **senior software architect** acting as the **architect role**. You de
Signal readiness before downstream work proceeds:
1. **Ready for design** — architecture baseline and required ADRs are updated.
-2. **Ready for implementation** — designer confirms contracts align with architecture constraints.
+1. **Ready for implementation** — designer confirms contracts align with architecture constraints.
Handoffs you own:
@@ -81,25 +81,25 @@ Handoffs you own:
## how you work
1. Read `docs/product/vision.md` and `docs/product/requirements.md`. If either is missing, stop and request product clarification.
-2. **Declare system style** in `docs/architecture/architecture.md`:
+1. **Declare system style** in `docs/architecture/architecture.md`:
- `backend-only` — API, service, library, CLI, data pipeline
- `frontend-only` — UI, static site, design system
- `fullstack` — API + UI tightly coupled
- `platform` — IaC, tooling, SDK
- `integration` — system of systems interoperating via APIs, events, or data contracts
-3. Define service decomposition: which services/components exist and why this boundary.
-4. Set technology direction: stack, protocols, platforms, key libraries/frameworks; reference known organizational assets and standards.
-5. Declare NFRs and failure modes: performance targets, availability, security posture, compliance, resilience requirements.
-6. Write or update `docs/architecture/architecture.md` via `@#architecture`.
-7. Write ADRs via `@#adr` for each significant structural decision.
-8. Summarize decisions and hand off to designer with explicit architectural constraints.
+1. Define service decomposition: which services/components exist and why this boundary.
+1. Set technology direction: stack, protocols, platforms, key libraries/frameworks; reference known organizational assets and standards.
+1. Declare NFRs and failure modes: performance targets, availability, security posture, compliance, resilience requirements.
+1. Write or update `docs/architecture/architecture.md` via `@#architecture`.
+1. Write ADRs via `@#adr` for each significant structural decision.
+1. Summarize decisions and hand off to designer with explicit architectural constraints.
## deliverables and success criteria
-| Artifact | Role |
-| --- | --- |
+| Artifact | Role |
+| ----------------------------------- | ------- |
| `docs/architecture/architecture.md` | creator |
-| `docs/architecture/adr/NNN-*.md` | creator |
+| `docs/architecture/adr/NNN-*.md` | creator |
- Architecture constraints are actionable for designer and engineer.
@@ -120,3 +120,4 @@ Handoffs you own:
- `@#analyse` — impact analysis, tradeoffs, feasibility
+
diff --git a/.github/agents/designer.agent.md b/.github/agents/designer.agent.md
index 86fc034..51dd744 100644
--- a/.github/agents/designer.agent.md
+++ b/.github/agents/designer.agent.md
@@ -82,7 +82,7 @@ Apply all relevant disciplines — a fullstack integration system needs API cont
Signal readiness before implementation proceeds:
1. **Ready for implementation** — contracts, schemas, errors, and required flows are explicit.
-2. **Ready for test planning** — edge cases and expected failure behavior are documented.
+1. **Ready for test planning** — edge cases and expected failure behavior are documented.
Handoffs you own:
@@ -92,24 +92,24 @@ Handoffs you own:
## how you work
1. Read `docs/architecture/architecture.md`, `docs/architecture/adr/*.md`, `docs/product/vision.md`, `docs/product/requirements.md`.
-2. If `docs/architecture/architecture.md` is missing or too vague to design from, stop and hand off to architect.
-3. Determine which design disciplines apply (see scope detection above).
-4. For each service and component in the architecture:
+1. If `docs/architecture/architecture.md` is missing or too vague to design from, stop and hand off to architect.
+1. Determine which design disciplines apply (see scope detection above).
+1. For each service and component in the architecture:
- Define the interaction surface: API endpoints, event types, inputs and outputs
- Define data schemas and validation rules
- Define state models where applicable (states, transitions, triggers, terminal states)
- Define error cases and how they are communicated to callers
-5. Map data flows: how data enters, transforms, and exits the system.
-6. If user-facing scope: design UX flows and write `docs/design/ux.md`.
-7. Write or update `docs/design/design.md` (always).
-8. Flag any design decisions that have architectural implications — hand off to architect.
+1. Map data flows: how data enters, transforms, and exits the system.
+1. If user-facing scope: design UX flows and write `docs/design/ux.md`.
+1. Write or update `docs/design/design.md` (always).
+1. Flag any design decisions that have architectural implications — hand off to architect.
## deliverables and success criteria
-| Artifact | Role |
-| --- | --- |
-| `docs/design/design.md` | creator |
-| `docs/design/ux.md` | creator (frontend/fullstack scope only) |
+| Artifact | Role |
+| ----------------------- | --------------------------------------- |
+| `docs/design/design.md` | creator |
+| `docs/design/ux.md` | creator (frontend/fullstack scope only) |
- Design docs are actionable without guesswork.
- API/interface contracts and error cases are explicit.
@@ -131,3 +131,4 @@ Handoffs you own:
- `@#openapi` — OpenAPI 3.1 spec writing and review
+
diff --git a/.github/agents/engineer.agent.md b/.github/agents/engineer.agent.md
index 094c719..3645ea4 100644
--- a/.github/agents/engineer.agent.md
+++ b/.github/agents/engineer.agent.md
@@ -78,19 +78,19 @@ Only delegate when workstreams are genuinely independent.
## how you work
1. Read upstream artifacts before touching code.
-2. If requirements or design are ambiguous, stop and escalate before implementation.
-3. Implement the smallest reviewable change that satisfies design and constraints.
-4. Write or update unit tests alongside each code change.
-5. Run relevant checks via `@#verify` before tester handoff.
-6. Handoff to tester with explicit verification targets and risk areas.
-7. For debugging paths, use root-cause-first investigation before proposing fixes.
+1. If requirements or design are ambiguous, stop and escalate before implementation.
+1. Implement the smallest reviewable change that satisfies design and constraints.
+1. Write or update unit tests alongside each code change.
+1. Run relevant checks via `@#verify` before tester handoff.
+1. Handoff to tester with explicit verification targets and risk areas.
+1. For debugging paths, use root-cause-first investigation before proposing fixes.
## deliverables and success criteria
-| Artifact | Role |
-| --- | --- |
+| Artifact | Role |
+| ----------- | ------- |
| source code | creator |
-| unit tests | creator |
+| unit tests | creator |
- Implementation matches approved architecture and design intent.
- Tests cover core paths and regressions.
@@ -121,3 +121,4 @@ Only delegate when workstreams are genuinely independent.
- `@#incident` — incident analysis and post-mortem writing
+
diff --git a/.github/agents/product.agent.md b/.github/agents/product.agent.md
index a9c372f..c7d60eb 100644
--- a/.github/agents/product.agent.md
+++ b/.github/agents/product.agent.md
@@ -71,9 +71,9 @@ You are a **senior product manager** acting as the **product role**. You define
You pause the pipeline at key moments and wait for explicit user confirmation:
1. **After intake + requirements clarification** — before architect starts designing
-2. **After architecture + design review** — before engineer starts implementing
-3. **After testing and acceptance review** — before release proceeds
-4. **Before merge** — confirm baseline artifacts are updated and optional WIP cleaned
+1. **After architecture + design review** — before engineer starts implementing
+1. **After testing and acceptance review** — before release proceeds
+1. **Before merge** — confirm baseline artifacts are updated and optional WIP cleaned
Handoffs you own:
@@ -83,21 +83,21 @@ Handoffs you own:
## how you work
1. **Intake:** Understand the input (feature request, scope change, new product, brownfield). Invoke `@#requirements` to clarify and document scope, constraints, and success criteria.
-2. **Choose flow:**
+1. **Choose flow:**
- Brownfield discovery: `requirements -> explore -> analyse -> architecture`
- New feature: `requirements -> architecture -> design (optional) -> engineer -> tester -> release`
- Existing behavior change: `requirements -> debug -> architecture (light) -> engineer -> tester -> release`
-3. **Orchestrate:** Delegate to architect/designer/engineer via subagent calls or handoffs. Keep gate decisions explicit and block progression when criteria are not met.
-4. **Gate:** Confirm with user at each transition before proceeding.
-5. **Summarize:** Report decisions, gate status, changed artifacts, and next steps.
+1. **Orchestrate:** Delegate to architect/designer/engineer via subagent calls or handoffs. Keep gate decisions explicit and block progression when criteria are not met.
+1. **Gate:** Confirm with user at each transition before proceeding.
+1. **Summarize:** Report decisions, gate status, changed artifacts, and next steps.
## deliverables and success criteria
-| Artifact | Role |
-| --- | --- |
-| `docs/product/vision.md` | creator |
-| `docs/product/requirements.md` | creator |
-| `docs/product/roadmap.md` | creator |
+| Artifact | Role |
+| ------------------------------------ | ------- |
+| `docs/product/vision.md` | creator |
+| `docs/product/requirements.md` | creator |
+| `docs/product/roadmap.md` | creator |
| gate decisions and acceptance record | creator |
- Gate decisions are explicit and traceable at each transition.
@@ -122,3 +122,4 @@ Handoffs you own:
- `@#onboard` — contributor onboarding guide generation
+
diff --git a/.github/agents/release.agent.md b/.github/agents/release.agent.md
index 8eea4c2..a83a7b9 100644
--- a/.github/agents/release.agent.md
+++ b/.github/agents/release.agent.md
@@ -69,7 +69,7 @@ You are a **senior platform and release engineer** acting as the **release role*
Signal readiness at each release gate:
1. **Ready for sign-off collection** — required artifacts are present and current.
-2. **Ready for PR creation** — tester and product both return explicit OK.
+1. **Ready for PR creation** — tester and product both return explicit OK.
Handoffs you own:
@@ -79,22 +79,22 @@ Handoffs you own:
## how you work
-1. Validate required artifacts: `docs/product/requirements.md`, `docs/architecture/architecture.md`, `docs/design/design.md`, `docs/test-report.md`, `docs/security-report.md`, `docs/performance-baseline.md`, `CHANGELOG.md`.
-2. Validate scope-conditional artifacts: require `docs/performance-baseline.md` only when performance validation is in scope; require observability evidence in `docs/test-report.md` (or a dedicated observability report if your process uses one).
-3. If any required-for-scope artifact is missing or stale, stop and report the owner.
-4. Collect tester sign-off (`OK`/`NOK`) using verification reports.
-5. Collect product sign-off (`OK`/`NOK`) against requirements and delivered scope.
-6. If either sign-off is `NOK`, stop and hand the blocker back to the owning role.
-7. If both are `OK`, invoke `@#release-notes` to produce `docs/releases/{date}.md` and finalize `CHANGELOG.md`.
-8. Invoke `@#pr` to push and open the PR with release notes as the body.
+1. Baseline artifacts to check: `docs/product/requirements.md`, `docs/architecture/architecture.md`, `docs/design/design.md`, `docs/test-report.md`, `docs/security-report.md`, `docs/performance-baseline.md`, `CHANGELOG.md`.
+1. Validate required-for-scope artifacts: require `docs/performance-baseline.md` only when performance validation is in scope; require observability evidence in `docs/test-report.md` (or a dedicated observability report if your process uses one).
+1. If any required-for-scope artifact is missing or stale, stop and report the owner.
+1. Collect tester sign-off (`OK`/`NOK`) using verification reports.
+1. Collect product sign-off (`OK`/`NOK`) against requirements and delivered scope.
+1. If either sign-off is `NOK`, stop and hand the blocker back to the owning role.
+1. If both are `OK`, invoke `@#release-notes` to produce `docs/releases/{date}.md` and finalize `CHANGELOG.md`.
+1. Invoke `@#pr` to push and open the PR with release notes as the body.
## deliverables and success criteria
-| Artifact | Role |
-| --- | --- |
-| `docs/releases/{date}.md` | creator |
-| `CHANGELOG.md` updates | creator |
-| release PR | creator |
+| Artifact | Role |
+| -------------------------------- | ------- |
+| `docs/releases/{date}.md` | creator |
+| `CHANGELOG.md` updates | creator |
+| release PR | creator |
| sign-off record (tester/product) | creator |
- Required-for-scope artifacts are present and current before sign-off.
@@ -118,3 +118,4 @@ Handoffs you own:
- `@#code-review` — final review before PR is opened
+
diff --git a/.github/agents/tester.agent.md b/.github/agents/tester.agent.md
index 5710cb7..cab4107 100644
--- a/.github/agents/tester.agent.md
+++ b/.github/agents/tester.agent.md
@@ -74,7 +74,7 @@ You are a **senior QA, security, and reliability engineer** acting as the **test
Signal readiness before release proceeds:
1. **Ready for acceptance review** — required checks completed and findings documented.
-2. **Ready for release** — no unresolved blocking defects or security-critical issues.
+1. **Ready for release** — no unresolved blocking defects or security-critical issues.
Handoffs you own:
@@ -84,21 +84,21 @@ Handoffs you own:
## how you work
1. Read `docs/product/requirements.md`, `docs/architecture/architecture.md`, and relevant design/implementation context.
-2. Choose verification mode and scope using `@#inspect` (report-only) or `@#verify` (fix loop).
-3. Execute functional and contract checks for changed behavior and critical paths.
-4. Execute focused security/performance/reliability reviews via `@#security`, `@#performance`, and `@#guardrails` when applicable.
-5. Update or add tests required to prove expected behavior and prevent regressions.
-6. Write baseline reports: `docs/test-report.md`, `docs/security-report.md`, and `docs/performance-baseline.md` when performance validation is in scope. Include observability evidence in `docs/test-report.md` unless a dedicated observability report is used.
-7. Publish verdict and hand off blockers or release-readiness status.
+1. Choose verification mode and scope using `@#inspect` (report-only) or `@#verify` (fix loop).
+1. Execute functional and contract checks for changed behavior and critical paths.
+1. Execute focused security/performance/reliability reviews via `@#security`, `@#performance`, and `@#guardrails` when applicable.
+1. Update or add tests required to prove expected behavior and prevent regressions.
+1. Write baseline reports: `docs/test-report.md`, `docs/security-report.md`, and `docs/performance-baseline.md` when performance validation is in scope. Include observability evidence in `docs/test-report.md` unless a dedicated observability report is used.
+1. Publish verdict and hand off blockers or release-readiness status.
## deliverables and success criteria
-| Artifact | Role |
-| --- | --- |
-| `docs/test-report.md` | creator |
-| `docs/security-report.md` | creator |
+| Artifact | Role |
+| ------------------------------ | ------------------------------------------------- |
+| `docs/test-report.md` | creator |
+| `docs/security-report.md` | creator |
| `docs/performance-baseline.md` | creator (when performance validation is in scope) |
-| test files | creator |
+| test files | creator |
- Verification coverage matches scope and risk.
- Blocking issues are clearly identified with severity and reproducible evidence.
@@ -126,3 +126,4 @@ Handoffs you own:
- `@#incident` — incident analysis and post-mortem writing
+
diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md
index b9594c9..8826347 100644
--- a/.github/copilot-instructions.md
+++ b/.github/copilot-instructions.md
@@ -101,6 +101,30 @@ Update docs whenever a change affects system structure, skill definitions, execu
Every ADR must include: context, decision, alternatives considered, rationale, and impact on the Option B pipeline.
+## Markdown Documentation Style
+
+For hand-authored Markdown in this repository, treat baseline docs as a coherent documentation system rather than isolated files.
+
+- Apply the same documentation style to non-generated, non-template Markdown files such as `README.md`, `CONTRIBUTING.md`, `SECURITY.md`, `CHANGELOG.md`, `docs/**/*.md`, and ADRs.
+- Do not apply these rules by editing generated artifacts under `.github/` or source templates under `src/vstack/_templates/` unless the task is explicitly about those sources.
+- Use Mermaid for process, flow, interaction, lifecycle, and decision diagrams when the visual structure matters more than exact monospace layout.
+- Keep ASCII/text trees for repository structure, directory layout, and similar scan-friendly file hierarchies where Mermaid would reduce readability.
+- Keep plain code fences for literal examples, terminal transcripts, frontmatter samples, JSON, YAML, and other content that is data rather than a diagram.
+- Prefer consistency across related docs: if `README.md`, architecture docs, and workflow docs describe the same flow, they should use compatible terminology and diagram style.
+- When modernizing older docs, convert diagrams selectively rather than mechanically. Preserve useful content first, then improve presentation.
+
+## Python Docstring Style
+
+For Python modules in this repository, treat code as the source of truth and keep docstrings aligned with shipped behavior.
+
+- Use **PEP 257** as the baseline: complete sentences, correct one-line vs multi-line structure, and a concise summary line first.
+- Keep docstrings **reStructuredText-compatible** per **PEP 287**. Use reST roles such as ``:class:`...``` when helpful and avoid Markdown formatting inside docstrings.
+- Prefer **Google-style sections** when additional structure adds value, especially `Args:`, `Returns:`, and `Raises:` on public APIs with non-trivial behavior.
+- Do not add section headers mechanically. For simple helpers, a precise one-line docstring is preferred over verbose boilerplate.
+- Module docstrings should explain responsibility and key concepts. Class docstrings should describe the abstraction. Function and method docstrings should describe behavior and observable effects rather than implementation trivia.
+- Avoid placeholder docstrings such as "Initialize instance state" or "Build parser". Describe intent and contract instead.
+- When a behavior, public API, or exception contract changes, update the corresponding docstring in the same change.
+
## Work Style
- Produce small, reviewable changes.
diff --git a/.github/dependabot.yml b/.github/dependabot.yml
new file mode 100644
index 0000000..c246aa2
--- /dev/null
+++ b/.github/dependabot.yml
@@ -0,0 +1,23 @@
+version: 2
+updates:
+ - package-ecosystem: "pip"
+ directory: "/"
+ schedule:
+ interval: "weekly"
+ open-pull-requests-limit: 10
+ labels:
+ - "dependencies"
+ - "python"
+ commit-message:
+ prefix: "chore(deps)"
+
+ - package-ecosystem: "github-actions"
+ directory: "/"
+ schedule:
+ interval: "weekly"
+ open-pull-requests-limit: 5
+ labels:
+ - "dependencies"
+ - "github-actions"
+ commit-message:
+ prefix: "chore(ci)"
diff --git a/.github/instructions/git.instructions.md b/.github/instructions/git.instructions.md
new file mode 100644
index 0000000..5624dc5
--- /dev/null
+++ b/.github/instructions/git.instructions.md
@@ -0,0 +1,44 @@
+---
+name: git
+description: 'Git and release hygiene conventions. Use when creating commits, branches, or release-related changes.'
+applyTo: **/*
+---
+Use these Git and release hygiene conventions in this project.
+
+## Branch naming
+
+1. Use `type/short-description` branch names.
+1. Keep branch names lowercase and use hyphens to separate words.
+1. Use one of these allowed branch types when branch validation is enabled:
+ `feature`, `bugfix`, `hotfix`, `release`, `chore`, `feat`, `fix`, `docs`,
+ `refactor`, `perf`, `test`, `ci`, `build`, `style`, `opt`, `patch`, `dependabot`.
+
+## Commit messages
+
+1. Use Conventional Commits: `type(optional-scope)!: short summary`.
+1. Keep commit subjects clear, imperative, and within repository limits.
+1. Keep the commit subject at 100 characters or fewer when commit policy CI enforces this limit.
+1. Include `!` or a `BREAKING CHANGE:` footer when behavior changes are breaking.
+1. Keep commit type and scope aligned with repository policy.
+
+## SemVer alignment
+
+1. Treat commit messages as release inputs when the repository uses semantic version automation.
+1. Ensure major, minor, and patch intent is reflected in the commit type and breaking markers.
+1. Do not merge release-impacting changes with ambiguous commit messages.
+
+## Security and credentials
+
+1. Never ask users to paste passphrases, tokens, API keys, or private keys into chat.
+1. Never echo or log secrets from terminal prompts, command output, or environment variables.
+1. Never place credentials in commit messages, source files, workflow files, or documentation.
+1. Prefer existing secure authentication flows (for example SSH agent, OS keychain, `gh auth`).
+
+## Safe Git operations
+
+1. Avoid force pushes and destructive history rewrites unless explicitly requested and approved.
+1. Keep commits focused and reviewable.
+1. Prefer local verification before pushing release-impacting changes.
+
+
+
diff --git a/.github/instructions/python.instructions.md b/.github/instructions/python.instructions.md
index 33a3fb1..3aafaf1 100644
--- a/.github/instructions/python.instructions.md
+++ b/.github/instructions/python.instructions.md
@@ -1,9 +1,9 @@
---
name: python
-description: 'Python coding conventions for this repository. Use when writing or reviewing Python modules, tests, CLI code, and package internals.'
+description: 'Python coding conventions for projects. Use when writing or reviewing Python modules, tests, CLI code, and package internals.'
applyTo: **/*.py
---
-Use these Python conventions in this repository.
+Use these Python conventions in this project.
## Design and readability
@@ -42,3 +42,4 @@ Use these Python conventions in this repository.
1. Do not silence lint/type errors unless there is a documented, task-specific reason.
+
diff --git a/.github/prompts/code-review.prompt.md b/.github/prompts/code-review.prompt.md
index c693aab..ad3f96a 100644
--- a/.github/prompts/code-review.prompt.md
+++ b/.github/prompts/code-review.prompt.md
@@ -50,3 +50,4 @@ End with:
- Biggest remaining risk: one sentence
+
diff --git a/.github/skills/adr/SKILL.md b/.github/skills/adr/SKILL.md
index d66e274..588879d 100644
--- a/.github/skills/adr/SKILL.md
+++ b/.github/skills/adr/SKILL.md
@@ -24,6 +24,14 @@ When you need clarification, use this exact format — never invent or guess:
Never ask more than one question at a time without waiting for the answer.
+### Diagram Convention
+
+When producing hand-authored Markdown outputs, prefer Mermaid for flow,
+interaction, lifecycle, state, topology, dependency, and decision diagrams when
+the format is supported and improves clarity. Use ASCII as a fallback when
+Mermaid is unsupported or would be less readable. Keep ASCII/text trees for
+directory structures and other scan-friendly hierarchies.
+
# adr — Architecture Decision Record
Document a significant technical decision so future contributors understand
@@ -191,3 +199,4 @@ After writing, state the file path and summary so the architect or product role
______________________________________________________________________
+
diff --git a/.github/skills/analyse/SKILL.md b/.github/skills/analyse/SKILL.md
index cf97a31..34bf610 100644
--- a/.github/skills/analyse/SKILL.md
+++ b/.github/skills/analyse/SKILL.md
@@ -24,6 +24,14 @@ When you need clarification, use this exact format — never invent or guess:
Never ask more than one question at a time without waiting for the answer.
+### Diagram Convention
+
+When producing hand-authored Markdown outputs, prefer Mermaid for flow,
+interaction, lifecycle, state, topology, dependency, and decision diagrams when
+the format is supported and improves clarity. Use ASCII as a fallback when
+Mermaid is unsupported or would be less readable. Keep ASCII/text trees for
+directory structures and other scan-friendly hierarchies.
+
```bash
# Detect base branch (main / master / develop / trunk)
BASE=$(gh pr view --json baseRefName -q .baseRefName 2>/dev/null) \
@@ -219,3 +227,4 @@ ______________________________________________________________________
______________________________________________________________________
+
diff --git a/.github/skills/architecture/SKILL.md b/.github/skills/architecture/SKILL.md
index a776a62..003baaf 100644
--- a/.github/skills/architecture/SKILL.md
+++ b/.github/skills/architecture/SKILL.md
@@ -24,6 +24,14 @@ When you need clarification, use this exact format — never invent or guess:
Never ask more than one question at a time without waiting for the answer.
+### Diagram Convention
+
+When producing hand-authored Markdown outputs, prefer Mermaid for flow,
+interaction, lifecycle, state, topology, dependency, and decision diagrams when
+the format is supported and improves clarity. Use ASCII as a fallback when
+Mermaid is unsupported or would be less readable. Keep ASCII/text trees for
+directory structures and other scan-friendly hierarchies.
+
# architecture — Engineering Plan Review
Review the plan before any code is written. Identify issues, give opinionated
@@ -66,7 +74,7 @@ If running low on context or asked to compress: Step 0 > Service/contract diagra
- Observability is not optional — new codepaths need logs, metrics, or traces.
- Security is not optional — new codepaths need threat modeling.
- Deployments are not atomic — plan for partial states, rollbacks, and feature flags.
-- ASCII diagrams for all data flows, state machines, service dependencies.
+- Diagrams for all data flows, state machines, and service dependencies.
## Cognitive Patterns — How Great Engineering Leads Think
@@ -115,18 +123,13 @@ Before reviewing anything, answer:
## Step 1: Service Boundary & Data Model Review
-Produce an ASCII diagram of the service topology:
+Produce a service topology diagram. Prefer Mermaid when possible; use ASCII only
+as a fallback when Mermaid would be less clear or unsupported.
-```text
-┌───────────────┐ HTTP/gRPC ┌───────────────┐
-│ Service A │ ─────────────────────▶│ Service B │
-│ (this plan) │ │ (existing) │
-└───────┬───────┘ └───────────────┘
- │ publishes
- ▼
-┌───────────────┐
-│ Event Bus │
-└───────────────┘
+```mermaid
+flowchart LR
+ A[Service A this plan] -->|HTTP or gRPC| B[Service B existing]
+ A -->|publishes| C[Event Bus]
```
Review:
@@ -277,3 +280,4 @@ For each significant structural decision made during this review (technology cho
______________________________________________________________________
+
diff --git a/.github/skills/cicd/SKILL.md b/.github/skills/cicd/SKILL.md
index 1245677..ae5ed5c 100644
--- a/.github/skills/cicd/SKILL.md
+++ b/.github/skills/cicd/SKILL.md
@@ -24,6 +24,14 @@ When you need clarification, use this exact format — never invent or guess:
Never ask more than one question at a time without waiting for the answer.
+### Diagram Convention
+
+When producing hand-authored Markdown outputs, prefer Mermaid for flow,
+interaction, lifecycle, state, topology, dependency, and decision diagrams when
+the format is supported and improves clarity. Use ASCII as a fallback when
+Mermaid is unsupported or would be less readable. Keep ASCII/text trees for
+directory structures and other scan-friendly hierarchies.
+
# cicd — GitHub Actions Workflows
Write CI/CD pipeline configuration as `.github/workflows/*.yml` files.
@@ -193,3 +201,4 @@ ______________________________________________________________________
______________________________________________________________________
+
diff --git a/.github/skills/code-review/SKILL.md b/.github/skills/code-review/SKILL.md
index c05837c..87214ed 100644
--- a/.github/skills/code-review/SKILL.md
+++ b/.github/skills/code-review/SKILL.md
@@ -24,6 +24,14 @@ When you need clarification, use this exact format — never invent or guess:
Never ask more than one question at a time without waiting for the answer.
+### Diagram Convention
+
+When producing hand-authored Markdown outputs, prefer Mermaid for flow,
+interaction, lifecycle, state, topology, dependency, and decision diagrams when
+the format is supported and improves clarity. Use ASCII as a fallback when
+Mermaid is unsupported or would be less readable. Keep ASCII/text trees for
+directory structures and other scan-friendly hierarchies.
+
```bash
# Detect base branch (main / master / develop / trunk)
BASE=$(gh pr view --json baseRefName -q .baseRefName 2>/dev/null) \
@@ -211,3 +219,4 @@ Confidence: [HIGH/MEDIUM/LOW — explain if not HIGH]
______________________________________________________________________
+
diff --git a/.github/skills/concise/SKILL.md b/.github/skills/concise/SKILL.md
index ea6d651..9f21470 100644
--- a/.github/skills/concise/SKILL.md
+++ b/.github/skills/concise/SKILL.md
@@ -24,6 +24,14 @@ When you need clarification, use this exact format — never invent or guess:
Never ask more than one question at a time without waiting for the answer.
+### Diagram Convention
+
+When producing hand-authored Markdown outputs, prefer Mermaid for flow,
+interaction, lifecycle, state, topology, dependency, and decision diagrams when
+the format is supported and improves clarity. Use ASCII as a fallback when
+Mermaid is unsupported or would be less readable. Keep ASCII/text trees for
+directory structures and other scan-friendly hierarchies.
+
# concise — Runtime Response Style Mode
Control response brevity at runtime without regenerating any agent artifacts.
@@ -152,3 +160,4 @@ ______________________________________________________________________
- [ ] User confirmation/status returned in deterministic format
+
diff --git a/.github/skills/consult/SKILL.md b/.github/skills/consult/SKILL.md
index 2c98dc1..ae12e15 100644
--- a/.github/skills/consult/SKILL.md
+++ b/.github/skills/consult/SKILL.md
@@ -24,6 +24,14 @@ When you need clarification, use this exact format — never invent or guess:
Never ask more than one question at a time without waiting for the answer.
+### Diagram Convention
+
+When producing hand-authored Markdown outputs, prefer Mermaid for flow,
+interaction, lifecycle, state, topology, dependency, and decision diagrams when
+the format is supported and improves clarity. Use ASCII as a fallback when
+Mermaid is unsupported or would be less readable. Keep ASCII/text trees for
+directory structures and other scan-friendly hierarchies.
+
# consult — DX Triage and Focused Review
Classify the request first, then run exactly one focused DX review path.
@@ -219,3 +227,4 @@ reason: [one sentence]
______________________________________________________________________
+
diff --git a/.github/skills/container/SKILL.md b/.github/skills/container/SKILL.md
index aac75ac..b0f99d8 100644
--- a/.github/skills/container/SKILL.md
+++ b/.github/skills/container/SKILL.md
@@ -24,6 +24,14 @@ When you need clarification, use this exact format — never invent or guess:
Never ask more than one question at a time without waiting for the answer.
+### Diagram Convention
+
+When producing hand-authored Markdown outputs, prefer Mermaid for flow,
+interaction, lifecycle, state, topology, dependency, and decision diagrams when
+the format is supported and improves clarity. Use ASCII as a fallback when
+Mermaid is unsupported or would be less readable. Keep ASCII/text trees for
+directory structures and other scan-friendly hierarchies.
+
# container — Dockerfile & Compose
Write production-grade container configuration for the service.
@@ -146,3 +154,4 @@ ______________________________________________________________________
______________________________________________________________________
+
diff --git a/.github/skills/debug/SKILL.md b/.github/skills/debug/SKILL.md
index a1a8397..5b4920c 100644
--- a/.github/skills/debug/SKILL.md
+++ b/.github/skills/debug/SKILL.md
@@ -24,6 +24,14 @@ When you need clarification, use this exact format — never invent or guess:
Never ask more than one question at a time without waiting for the answer.
+### Diagram Convention
+
+When producing hand-authored Markdown outputs, prefer Mermaid for flow,
+interaction, lifecycle, state, topology, dependency, and decision diagrams when
+the format is supported and improves clarity. Use ASCII as a fallback when
+Mermaid is unsupported or would be less readable. Keep ASCII/text trees for
+directory structures and other scan-friendly hierarchies.
+
# debug — Systematic Root-Cause Investigation
Investigate and fix bugs using the scientific method. No patches without understanding
@@ -271,3 +279,4 @@ Prevention: [any follow-up items]
______________________________________________________________________
+
diff --git a/.github/skills/dependency/SKILL.md b/.github/skills/dependency/SKILL.md
index 2523d65..de1740b 100644
--- a/.github/skills/dependency/SKILL.md
+++ b/.github/skills/dependency/SKILL.md
@@ -24,6 +24,14 @@ When you need clarification, use this exact format — never invent or guess:
Never ask more than one question at a time without waiting for the answer.
+### Diagram Convention
+
+When producing hand-authored Markdown outputs, prefer Mermaid for flow,
+interaction, lifecycle, state, topology, dependency, and decision diagrams when
+the format is supported and improves clarity. Use ASCII as a fallback when
+Mermaid is unsupported or would be less readable. Keep ASCII/text trees for
+directory structures and other scan-friendly hierarchies.
+
# dependency — Dependency Health Audit
Audit the health, security, and compliance of project dependencies. Covers
@@ -317,3 +325,4 @@ Action items (priority order):
```
+
diff --git a/.github/skills/design/SKILL.md b/.github/skills/design/SKILL.md
index 704e3c3..8136908 100644
--- a/.github/skills/design/SKILL.md
+++ b/.github/skills/design/SKILL.md
@@ -24,6 +24,14 @@ When you need clarification, use this exact format — never invent or guess:
Never ask more than one question at a time without waiting for the answer.
+### Diagram Convention
+
+When producing hand-authored Markdown outputs, prefer Mermaid for flow,
+interaction, lifecycle, state, topology, dependency, and decision diagrams when
+the format is supported and improves clarity. Use ASCII as a fallback when
+Mermaid is unsupported or would be less readable. Keep ASCII/text trees for
+directory structures and other scan-friendly hierarchies.
+
# design — API & Service Design
Produce a complete service or API design document from minimal input. The output
@@ -77,15 +85,22 @@ For each resource/entity in the domain:
1. **Lifecycle:** What states can it be in?
1. **Relationships:** What resources does it reference?
-Produce an entity diagram:
-
-```text
-┌──────────┐ 1:N ┌──────────────┐
-│ User │ ──────────▶ │ Order │
-│ id │ │ id │
-│ email │ │ user_id (fk) │
-│ name │ │ status │
-└──────────┘ └──────────────┘
+Produce an entity diagram. Prefer Mermaid when possible; use ASCII as a fallback
+only when Mermaid support is unavailable or would reduce clarity.
+
+```mermaid
+erDiagram
+ USER ||--o{ ORDER : places
+ USER {
+ string id
+ string email
+ string name
+ }
+ ORDER {
+ string id
+ string user_id
+ string status
+ }
```
______________________________________________________________________
@@ -248,3 +263,4 @@ Output a complete design document to `docs/design/design.md` or `openapi.yaml`:
______________________________________________________________________
+
diff --git a/.github/skills/docs/SKILL.md b/.github/skills/docs/SKILL.md
index b15d4df..ebc9f46 100644
--- a/.github/skills/docs/SKILL.md
+++ b/.github/skills/docs/SKILL.md
@@ -24,6 +24,14 @@ When you need clarification, use this exact format — never invent or guess:
Never ask more than one question at a time without waiting for the answer.
+### Diagram Convention
+
+When producing hand-authored Markdown outputs, prefer Mermaid for flow,
+interaction, lifecycle, state, topology, dependency, and decision diagrams when
+the format is supported and improves clarity. Use ASCII as a fallback when
+Mermaid is unsupported or would be less readable. Keep ASCII/text trees for
+directory structures and other scan-friendly hierarchies.
+
```bash
# Detect base branch (main / master / develop / trunk)
BASE=$(gh pr view --json baseRefName -q .baseRefName 2>/dev/null) \
@@ -159,3 +167,4 @@ Skipped (n/a):
______________________________________________________________________
+
diff --git a/.github/skills/explore/SKILL.md b/.github/skills/explore/SKILL.md
index 900fc09..d91d4a0 100644
--- a/.github/skills/explore/SKILL.md
+++ b/.github/skills/explore/SKILL.md
@@ -24,6 +24,14 @@ When you need clarification, use this exact format — never invent or guess:
Never ask more than one question at a time without waiting for the answer.
+### Diagram Convention
+
+When producing hand-authored Markdown outputs, prefer Mermaid for flow,
+interaction, lifecycle, state, topology, dependency, and decision diagrams when
+the format is supported and improves clarity. Use ASCII as a fallback when
+Mermaid is unsupported or would be less readable. Keep ASCII/text trees for
+directory structures and other scan-friendly hierarchies.
+
# explore — Codebase & Architecture Discovery
Map an unfamiliar codebase and produce a structured onboarding summary.
@@ -189,48 +197,48 @@ Type: [API service / library / CLI / worker / ...]
Stack: [language, framework, runtime versions]
### Architecture
-[ASCII diagram of service topology or module structure]
-
-┌─────────────────┐ HTTP ┌──────────────┐
-│ [This service] │ ──────────▶ │ [Dep A] │
-└────────┬────────┘ └──────────────┘
- │ subscribes
- ▼
-┌─────────────────┐
-│ [Message Bus] │
-└─────────────────┘
+[Mermaid diagram of service topology or module structure when possible; ASCII fallback if needed]
### Data Stores
+
- [Database]: [what it stores, ORM/driver used]
- [Cache]: [what is cached, TTL strategy]
### Key External Dependencies
+
- [Service A]: [purpose, auth method]
- [Service B]: [purpose]
### API Surface
+
[Summarize endpoints or exported functions]
### Test Coverage
+
- Unit tests: [Y/N, count, coverage %]
- Integration tests: [Y/N]
- Contract tests: [Y/N]
### CI/CD
+
- CI: [GitHub Actions/GitLab CI/etc]
- Deploy target: [Fly.io/Render/K8s/etc]
- Release process: [manual/automated]
### Technical Debt
+
- [Key items from TODOS.md or code comments]
### Onboarding Notes
+
- How to run locally: [command]
- How to run tests: [command]
- Key config: [env vars]
- Gotchas: [anything that surprised me]
+
```
______________________________________________________________________
+
diff --git a/.github/skills/freeze/SKILL.md b/.github/skills/freeze/SKILL.md
deleted file mode 100644
index 8f18aa4..0000000
--- a/.github/skills/freeze/SKILL.md
+++ /dev/null
@@ -1,64 +0,0 @@
----
-name: freeze
-description: 'Lock a directory from edits for the current session. Any attempt to edit files in the frozen path will be refused. Use when you want to prevent accidental modification of stable, shared, or generated code.'
-argument-hint: '[path to freeze]'
-disable-model-invocation: true
----
-## Skill Context
-
-This skill is part of **vstack** — a VS Code-native AI engineering workflow system.
-
-### Completeness Principle
-
-AI-assisted coding compresses implementation time 10–100x. When evaluating "approach A (100%, full implementation) vs approach B (90%, shortcut)", **prefer A**. The delta costs seconds with AI assistance. "Ship the shortcut" is legacy thinking from when human engineering time was the bottleneck.
-
-### AskUserQuestion Format
-
-When you need clarification, use this exact format — never invent or guess:
-
-> **Question:** [The specific question]
-> **Options:** A) … | B) … | C) …
-> **Default if no response:** [What you'll do]
-
-Never ask more than one question at a time without waiting for the answer.
-
-### Repo Context
-
-```bash
-# Determine project and branch context
-git rev-parse --show-toplevel 2>/dev/null | xargs basename || echo "unknown-project"
-git branch --show-current 2>/dev/null || echo "unknown-branch"
-```
-
-# freeze — Directory Edit Lock
-
-Lock a directory to prevent accidental edits during this session.
-
-## Out of scope
-
-- Session-wide careful mode for destructive commands (use `guardrails`)
-- Removing a freeze (use `unfreeze`)
-- Permanent file permissions (this is session-local only)
-
----
-
-Parse the user's request for the path to freeze.
-
-If no path was specified:
-> **Question:** Which directory should I freeze?
-> **Options:** A) Specify the path | B) Freeze the current directory | C) List directories to choose
-> **Default if no response:** Ask again
-
-Once the path is confirmed:
-
-```text
-⛔ FROZEN: [path]
-
-I will not edit any files under [path] until you run the unfreeze skill.
-This is a hard block, not a warning. If you ask me to edit a frozen file,
-I will refuse and explain how to unfreeze it.
-```
-
-**To unfreeze:** Use the `unfreeze` skill.
-
-
diff --git a/.github/skills/guardrails/SKILL.md b/.github/skills/guardrails/SKILL.md
index d2e9ee4..00f9544 100644
--- a/.github/skills/guardrails/SKILL.md
+++ b/.github/skills/guardrails/SKILL.md
@@ -24,6 +24,14 @@ When you need clarification, use this exact format — never invent or guess:
Never ask more than one question at a time without waiting for the answer.
+### Diagram Convention
+
+When producing hand-authored Markdown outputs, prefer Mermaid for flow,
+interaction, lifecycle, state, topology, dependency, and decision diagrams when
+the format is supported and improves clarity. Use ASCII as a fallback when
+Mermaid is unsupported or would be less readable. Keep ASCII/text trees for
+directory structures and other scan-friendly hierarchies.
+
# guardrails — Safety Mode
Activate careful mode for this session. Two behaviors are now enabled.
@@ -69,3 +77,4 @@ Explicitly ask to "disable guardrails".
______________________________________________________________________
+
diff --git a/.github/skills/incident/SKILL.md b/.github/skills/incident/SKILL.md
index 525ca3e..132f25c 100644
--- a/.github/skills/incident/SKILL.md
+++ b/.github/skills/incident/SKILL.md
@@ -24,6 +24,14 @@ When you need clarification, use this exact format — never invent or guess:
Never ask more than one question at a time without waiting for the answer.
+### Diagram Convention
+
+When producing hand-authored Markdown outputs, prefer Mermaid for flow,
+interaction, lifecycle, state, topology, dependency, and decision diagrams when
+the format is supported and improves clarity. Use ASCII as a fallback when
+Mermaid is unsupported or would be less readable. Keep ASCII/text trees for
+directory structures and other scan-friendly hierarchies.
+
# incident — Incident Analysis & Post-Mortem
Guide a structured incident investigation and produce a blameless post-mortem
@@ -317,3 +325,4 @@ Status: [Draft — ready for team review]
```
+
diff --git a/.github/skills/inspect/SKILL.md b/.github/skills/inspect/SKILL.md
index 63ca8b2..3872141 100644
--- a/.github/skills/inspect/SKILL.md
+++ b/.github/skills/inspect/SKILL.md
@@ -24,6 +24,14 @@ When you need clarification, use this exact format — never invent or guess:
Never ask more than one question at a time without waiting for the answer.
+### Diagram Convention
+
+When producing hand-authored Markdown outputs, prefer Mermaid for flow,
+interaction, lifecycle, state, topology, dependency, and decision diagrams when
+the format is supported and improves clarity. Use ASCII as a fallback when
+Mermaid is unsupported or would be less readable. Keep ASCII/text trees for
+directory structures and other scan-friendly hierarchies.
+
```bash
# Detect base branch (main / master / develop / trunk)
BASE=$(gh pr view --json baseRefName -q .baseRefName 2>/dev/null) \
@@ -157,3 +165,4 @@ ______________________________________________________________________
______________________________________________________________________
+
diff --git a/.github/skills/migrate/SKILL.md b/.github/skills/migrate/SKILL.md
index 0484601..bcf01e1 100644
--- a/.github/skills/migrate/SKILL.md
+++ b/.github/skills/migrate/SKILL.md
@@ -24,6 +24,14 @@ When you need clarification, use this exact format — never invent or guess:
Never ask more than one question at a time without waiting for the answer.
+### Diagram Convention
+
+When producing hand-authored Markdown outputs, prefer Mermaid for flow,
+interaction, lifecycle, state, topology, dependency, and decision diagrams when
+the format is supported and improves clarity. Use ASCII as a fallback when
+Mermaid is unsupported or would be less readable. Keep ASCII/text trees for
+directory structures and other scan-friendly hierarchies.
+
```bash
# Detect base branch (main / master / develop / trunk)
BASE=$(gh pr view --json baseRefName -q .baseRefName 2>/dev/null) \
@@ -329,3 +337,4 @@ Pre-deploy checklist:
```
+
diff --git a/.github/skills/onboard/SKILL.md b/.github/skills/onboard/SKILL.md
index 5745ea2..dc58fc6 100644
--- a/.github/skills/onboard/SKILL.md
+++ b/.github/skills/onboard/SKILL.md
@@ -24,6 +24,14 @@ When you need clarification, use this exact format — never invent or guess:
Never ask more than one question at a time without waiting for the answer.
+### Diagram Convention
+
+When producing hand-authored Markdown outputs, prefer Mermaid for flow,
+interaction, lifecycle, state, topology, dependency, and decision diagrams when
+the format is supported and improves clarity. Use ASCII as a fallback when
+Mermaid is unsupported or would be less readable. Keep ASCII/text trees for
+directory structures and other scan-friendly hierarchies.
+
# onboard — Contributor Onboarding Guide
Generate or update onboarding documentation so a new contributor can go from
@@ -313,3 +321,4 @@ Gaps remaining (if any):
```
+
diff --git a/.github/skills/openapi/SKILL.md b/.github/skills/openapi/SKILL.md
index 907c55b..3f12c20 100644
--- a/.github/skills/openapi/SKILL.md
+++ b/.github/skills/openapi/SKILL.md
@@ -24,6 +24,14 @@ When you need clarification, use this exact format — never invent or guess:
Never ask more than one question at a time without waiting for the answer.
+### Diagram Convention
+
+When producing hand-authored Markdown outputs, prefer Mermaid for flow,
+interaction, lifecycle, state, topology, dependency, and decision diagrams when
+the format is supported and improves clarity. Use ASCII as a fallback when
+Mermaid is unsupported or would be less readable. Keep ASCII/text trees for
+directory structures and other scan-friendly hierarchies.
+
# openapi — OpenAPI 3.1 Specification
Write or review OpenAPI 3.1 specifications. The spec is the contract — it must
@@ -406,3 +414,4 @@ Summary: [N critical, N warnings, N info]
`$ref` for all reusable schemas, and validate it passes linting.
+
diff --git a/.github/skills/performance/SKILL.md b/.github/skills/performance/SKILL.md
index 55e7a10..b25b7a4 100644
--- a/.github/skills/performance/SKILL.md
+++ b/.github/skills/performance/SKILL.md
@@ -24,6 +24,14 @@ When you need clarification, use this exact format — never invent or guess:
Never ask more than one question at a time without waiting for the answer.
+### Diagram Convention
+
+When producing hand-authored Markdown outputs, prefer Mermaid for flow,
+interaction, lifecycle, state, topology, dependency, and decision diagrams when
+the format is supported and improves clarity. Use ASCII as a fallback when
+Mermaid is unsupported or would be less readable. Keep ASCII/text trees for
+directory structures and other scan-friendly hierarchies.
+
```bash
# Detect base branch (main / master / develop / trunk)
BASE=$(gh pr view --json baseRefName -q .baseRefName 2>/dev/null) \
@@ -250,3 +258,4 @@ ______________________________________________________________________
______________________________________________________________________
+
diff --git a/.github/skills/pr/SKILL.md b/.github/skills/pr/SKILL.md
index c1102de..373f356 100644
--- a/.github/skills/pr/SKILL.md
+++ b/.github/skills/pr/SKILL.md
@@ -24,6 +24,14 @@ When you need clarification, use this exact format — never invent or guess:
Never ask more than one question at a time without waiting for the answer.
+### Diagram Convention
+
+When producing hand-authored Markdown outputs, prefer Mermaid for flow,
+interaction, lifecycle, state, topology, dependency, and decision diagrams when
+the format is supported and improves clarity. Use ASCII as a fallback when
+Mermaid is unsupported or would be less readable. Keep ASCII/text trees for
+directory structures and other scan-friendly hierarchies.
+
# pr — Commit, Push & Open Pull Request
Push the current branch and open a PR targeting main. This is the final step
@@ -132,3 +140,4 @@ CI/CD will now:
______________________________________________________________________
+
diff --git a/.github/skills/refactor/SKILL.md b/.github/skills/refactor/SKILL.md
index 81569bf..a8e7a13 100644
--- a/.github/skills/refactor/SKILL.md
+++ b/.github/skills/refactor/SKILL.md
@@ -24,6 +24,14 @@ When you need clarification, use this exact format — never invent or guess:
Never ask more than one question at a time without waiting for the answer.
+### Diagram Convention
+
+When producing hand-authored Markdown outputs, prefer Mermaid for flow,
+interaction, lifecycle, state, topology, dependency, and decision diagrams when
+the format is supported and improves clarity. Use ASCII as a fallback when
+Mermaid is unsupported or would be less readable. Keep ASCII/text trees for
+directory structures and other scan-friendly hierarchies.
+
```bash
# Detect base branch (main / master / develop / trunk)
BASE=$(gh pr view --json baseRefName -q .baseRefName 2>/dev/null) \
@@ -377,3 +385,4 @@ Behavior changed: No
```
+
diff --git a/.github/skills/release-notes/SKILL.md b/.github/skills/release-notes/SKILL.md
index 1d8d483..113cf68 100644
--- a/.github/skills/release-notes/SKILL.md
+++ b/.github/skills/release-notes/SKILL.md
@@ -24,6 +24,14 @@ When you need clarification, use this exact format — never invent or guess:
Never ask more than one question at a time without waiting for the answer.
+### Diagram Convention
+
+When producing hand-authored Markdown outputs, prefer Mermaid for flow,
+interaction, lifecycle, state, topology, dependency, and decision diagrams when
+the format is supported and improves clarity. Use ASCII as a fallback when
+Mermaid is unsupported or would be less readable. Keep ASCII/text trees for
+directory structures and other scan-friendly hierarchies.
+
# release-notes — Release Artifact Preparation
Verify all artifacts are complete, write release notes, and update the changelog.
@@ -151,3 +159,4 @@ Keep existing entries intact.
______________________________________________________________________
+
diff --git a/.github/skills/requirements/SKILL.md b/.github/skills/requirements/SKILL.md
index aebd130..e72b42a 100644
--- a/.github/skills/requirements/SKILL.md
+++ b/.github/skills/requirements/SKILL.md
@@ -24,6 +24,14 @@ When you need clarification, use this exact format — never invent or guess:
Never ask more than one question at a time without waiting for the answer.
+### Diagram Convention
+
+When producing hand-authored Markdown outputs, prefer Mermaid for flow,
+interaction, lifecycle, state, topology, dependency, and decision diagrams when
+the format is supported and improves clarity. Use ASCII as a fallback when
+Mermaid is unsupported or would be less readable. Keep ASCII/text trees for
+directory structures and other scan-friendly hierarchies.
+
# requirements — Requirements Gathering & Documentation
Clarify and document what must be built before engineering starts. Produce a
@@ -211,3 +219,4 @@ After writing, summarize what was decided so the architect role can start.
______________________________________________________________________
+
diff --git a/.github/skills/security/SKILL.md b/.github/skills/security/SKILL.md
index 20e2d78..c008a32 100644
--- a/.github/skills/security/SKILL.md
+++ b/.github/skills/security/SKILL.md
@@ -24,6 +24,14 @@ When you need clarification, use this exact format — never invent or guess:
Never ask more than one question at a time without waiting for the answer.
+### Diagram Convention
+
+When producing hand-authored Markdown outputs, prefer Mermaid for flow,
+interaction, lifecycle, state, topology, dependency, and decision diagrams when
+the format is supported and improves clarity. Use ASCII as a fallback when
+Mermaid is unsupported or would be less readable. Keep ASCII/text trees for
+directory structures and other scan-friendly hierarchies.
+
```bash
# Detect base branch (main / master / develop / trunk)
BASE=$(gh pr view --json baseRefName -q .baseRefName 2>/dev/null) \
@@ -287,3 +295,4 @@ Scope: [full/diff/dependency/config]
______________________________________________________________________
+
diff --git a/.github/skills/unfreeze/SKILL.md b/.github/skills/unfreeze/SKILL.md
deleted file mode 100644
index b9b4b1d..0000000
--- a/.github/skills/unfreeze/SKILL.md
+++ /dev/null
@@ -1,59 +0,0 @@
----
-name: unfreeze
-description: 'Remove a directory edit lock. Use after freeze when you want to allow edits again.'
-argument-hint: '[path to unfreeze]'
-disable-model-invocation: true
----
-## Skill Context
-
-This skill is part of **vstack** — a VS Code-native AI engineering workflow system.
-
-### Completeness Principle
-
-AI-assisted coding compresses implementation time 10–100x. When evaluating "approach A (100%, full implementation) vs approach B (90%, shortcut)", **prefer A**. The delta costs seconds with AI assistance. "Ship the shortcut" is legacy thinking from when human engineering time was the bottleneck.
-
-### AskUserQuestion Format
-
-When you need clarification, use this exact format — never invent or guess:
-
-> **Question:** [The specific question]
-> **Options:** A) … | B) … | C) …
-> **Default if no response:** [What you'll do]
-
-Never ask more than one question at a time without waiting for the answer.
-
-### Repo Context
-
-```bash
-# Determine project and branch context
-git rev-parse --show-toplevel 2>/dev/null | xargs basename || echo "unknown-project"
-git branch --show-current 2>/dev/null || echo "unknown-branch"
-```
-
-# unfreeze — Remove Directory Edit Lock
-
-Remove a previously activated directory freeze.
-
-## Out of scope
-
-- Activating a freeze (use `freeze`)
-- Disabling careful mode (ask to "disable guardrails")
-
----
-
-Parse the user's request for the path to unfreeze.
-
-If no path was specified and there are active freezes, list them:
-> **Question:** Which directory should I unfreeze?
-> **Options:** A) [list frozen paths] | B) Unfreeze all
-> **Default if no response:** Ask again
-
-Once confirmed:
-
-```text
-✓ UNFROZEN: [path]
-
-Edit restrictions on [path] have been removed.
-```
-
-
diff --git a/.github/skills/verify/SKILL.md b/.github/skills/verify/SKILL.md
index 4c2a2a1..fcc29d6 100644
--- a/.github/skills/verify/SKILL.md
+++ b/.github/skills/verify/SKILL.md
@@ -24,6 +24,14 @@ When you need clarification, use this exact format — never invent or guess:
Never ask more than one question at a time without waiting for the answer.
+### Diagram Convention
+
+When producing hand-authored Markdown outputs, prefer Mermaid for flow,
+interaction, lifecycle, state, topology, dependency, and decision diagrams when
+the format is supported and improves clarity. Use ASCII as a fallback when
+Mermaid is unsupported or would be less readable. Keep ASCII/text trees for
+directory structures and other scan-friendly hierarchies.
+
```bash
# Detect base branch (main / master / develop / trunk)
BASE=$(gh pr view --json baseRefName -q .baseRefName 2>/dev/null) \
@@ -274,3 +282,4 @@ scope: [path/component/full]
______________________________________________________________________
+
diff --git a/.github/skills/vision/SKILL.md b/.github/skills/vision/SKILL.md
index 87942b8..cbf9b5c 100644
--- a/.github/skills/vision/SKILL.md
+++ b/.github/skills/vision/SKILL.md
@@ -24,6 +24,14 @@ When you need clarification, use this exact format — never invent or guess:
Never ask more than one question at a time without waiting for the answer.
+### Diagram Convention
+
+When producing hand-authored Markdown outputs, prefer Mermaid for flow,
+interaction, lifecycle, state, topology, dependency, and decision diagrams when
+the format is supported and improves clarity. Use ASCII as a fallback when
+Mermaid is unsupported or would be less readable. Keep ASCII/text trees for
+directory structures and other scan-friendly hierarchies.
+
```bash
# Detect base branch (main / master / develop / trunk)
BASE=$(gh pr view --json baseRefName -q .baseRefName 2>/dev/null) \
@@ -72,7 +80,7 @@ Review the plan with maximum rigor and the appropriate level of ambition.
1. **Retry/backoff/circuit breaker coverage.** Every external call: what happens on retry? Exponential backoff with jitter? Circuit breaker state machine? Dead letter queue?
1. **Observability is scope, not afterthought.** New codepaths need: structured logs with correlation IDs, metrics (request rate, error rate, latency p50/p95/p99), distributed traces, dashboards, and alerts.
1. **API contracts are immutable once published.** Breaking changes require version bumps. Plan the migration strategy before coding.
-1. **Diagrams are mandatory.** ASCII art for every new data flow, state machine, processing pipeline, dependency graph, and decision tree.
+1. **Diagrams are mandatory.** Prefer Mermaid for every new data flow, interaction flow, state machine, processing pipeline, dependency graph, and decision tree. Use ASCII only as a fallback when Mermaid is unsupported or less clear.
1. **Everything deferred must be written down.** TODOS.md or it doesn't exist.
1. **Design for the 3am pager.** Systems over heroes. Every runbook, alert, and recovery procedure documented before go-live.
1. **Security is first-class scope.** Authentication, authorization, input validation, rate limiting, secret management — addressed in the plan, not "later."
@@ -200,3 +208,4 @@ Present as: "Overall assessment: [READY/NEEDS REVISION/SCOPE CHANGE] because [1-
______________________________________________________________________
+
diff --git a/.github/vstack.json b/.github/vstack.json
index 8d4e2bf..1f6c2db 100644
--- a/.github/vstack.json
+++ b/.github/vstack.json
@@ -1,142 +1,142 @@
{
"vstack_version": "0.0.0.post3.dev0+df3fe6e",
- "installed_at": "2026-04-19T22:11:30.480962+00:00",
+ "installed_at": "2026-04-20T22:37:59.699083+00:00",
"artifacts": {
"skills": [
{
"name": "adr",
"file": "skills/adr/SKILL.md",
- "version": "1.0.1"
+ "version": "1.0.2"
},
{
"name": "analyse",
"file": "skills/analyse/SKILL.md",
- "version": "1.0.1"
+ "version": "1.0.2"
},
{
"name": "architecture",
"file": "skills/architecture/SKILL.md",
- "version": "1.0.1"
+ "version": "1.0.2"
},
{
"name": "cicd",
"file": "skills/cicd/SKILL.md",
- "version": "1.0.1"
+ "version": "1.0.2"
},
{
"name": "code-review",
"file": "skills/code-review/SKILL.md",
- "version": "1.0.1"
+ "version": "1.0.2"
},
{
"name": "concise",
"file": "skills/concise/SKILL.md",
- "version": "1.0.0"
+ "version": "1.0.1"
},
{
"name": "consult",
"file": "skills/consult/SKILL.md",
- "version": "1.0.1"
+ "version": "1.0.2"
},
{
"name": "container",
"file": "skills/container/SKILL.md",
- "version": "1.0.1"
+ "version": "1.0.2"
},
{
"name": "debug",
"file": "skills/debug/SKILL.md",
- "version": "1.0.1"
+ "version": "1.0.2"
},
{
"name": "dependency",
"file": "skills/dependency/SKILL.md",
- "version": "1.0.0"
+ "version": "1.0.1"
},
{
"name": "design",
"file": "skills/design/SKILL.md",
- "version": "1.0.1"
+ "version": "1.0.2"
},
{
"name": "docs",
"file": "skills/docs/SKILL.md",
- "version": "1.0.1"
+ "version": "1.0.2"
},
{
"name": "explore",
"file": "skills/explore/SKILL.md",
- "version": "1.0.1"
+ "version": "1.0.2"
},
{
"name": "guardrails",
"file": "skills/guardrails/SKILL.md",
- "version": "1.0.1"
+ "version": "1.0.2"
},
{
"name": "incident",
"file": "skills/incident/SKILL.md",
- "version": "1.0.0"
+ "version": "1.0.1"
},
{
"name": "inspect",
"file": "skills/inspect/SKILL.md",
- "version": "1.0.1"
+ "version": "1.0.2"
},
{
"name": "migrate",
"file": "skills/migrate/SKILL.md",
- "version": "1.0.0"
+ "version": "1.0.1"
},
{
"name": "onboard",
"file": "skills/onboard/SKILL.md",
- "version": "1.0.0"
+ "version": "1.0.1"
},
{
"name": "openapi",
"file": "skills/openapi/SKILL.md",
- "version": "1.0.0"
+ "version": "1.0.1"
},
{
"name": "performance",
"file": "skills/performance/SKILL.md",
- "version": "1.0.1"
+ "version": "1.0.2"
},
{
"name": "pr",
"file": "skills/pr/SKILL.md",
- "version": "1.0.1"
+ "version": "1.0.2"
},
{
"name": "refactor",
"file": "skills/refactor/SKILL.md",
- "version": "1.0.0"
+ "version": "1.0.1"
},
{
"name": "release-notes",
"file": "skills/release-notes/SKILL.md",
- "version": "1.0.1"
+ "version": "1.0.2"
},
{
"name": "requirements",
"file": "skills/requirements/SKILL.md",
- "version": "1.0.1"
+ "version": "1.0.2"
},
{
"name": "security",
"file": "skills/security/SKILL.md",
- "version": "1.0.1"
+ "version": "1.0.2"
},
{
"name": "verify",
"file": "skills/verify/SKILL.md",
- "version": "1.0.1"
+ "version": "1.0.2"
},
{
"name": "vision",
"file": "skills/vision/SKILL.md",
- "version": "1.0.1"
+ "version": "1.0.2"
}
],
"agents": [
@@ -172,10 +172,15 @@
}
],
"instructions": [
+ {
+ "name": "git",
+ "file": "instructions/git.instructions.md",
+ "version": "0.1.0"
+ },
{
"name": "python",
"file": "instructions/python.instructions.md",
- "version": "0.1.0"
+ "version": "0.1.1"
}
],
"prompts": [
diff --git a/.github/workflows/commit.yml b/.github/workflows/commit.yml
new file mode 100644
index 0000000..182fea8
--- /dev/null
+++ b/.github/workflows/commit.yml
@@ -0,0 +1,41 @@
+# Commit workflow.
+# Purpose: fail fast on branch pushes when commit messages do not match
+# repository commit conventions.
+name: Commit
+
+on:
+ # Validate commits on branch pushes before PR merge.
+ push:
+ # Mainline and merge-queue refs are validated by PR/release workflows.
+ branches-ignore:
+ - main
+ - master
+ - merge/**
+ - gh-readonly-queue/**
+
+permissions:
+ # Read-only is enough for commit metadata checks.
+ contents: read
+
+jobs:
+ validate-commit-messages:
+ # Single gate that validates message format/types and custom scope policy.
+ name: Validate Commit Messages
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v5
+ with:
+ # Full history is required to inspect the full pushed commit range.
+ fetch-depth: 0
+
+ - name: Validate commits with commit-check
+ uses: commit-check/commit-check-action@v2
+ with:
+ # Commit and branch policy is read from cchk.toml in repo root.
+ message: true
+ branch: true
+ author-name: false
+ author-email: false
+ job-summary: true
+ pr-comments: false
diff --git a/.github/workflows/qa.yml b/.github/workflows/qa.yml
index e4f73d9..a5fee71 100644
--- a/.github/workflows/qa.yml
+++ b/.github/workflows/qa.yml
@@ -13,7 +13,9 @@ permissions:
contents: read
env:
+ # Baseline interpreter for non-matrix checks.
PYTHON_VERSION: "3.11"
+ # Keep Poetry virtual environments inside the workspace for deterministic paths.
POETRY_VIRTUALENVS_IN_PROJECT: "true"
jobs:
@@ -49,6 +51,7 @@ jobs:
run: make typecheck
test-matrix:
+ # Cross-version test coverage for all supported Python runtimes.
name: Tests (py${{ matrix.python-version }})
runs-on: ubuntu-latest
strategy:
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 0a63d4e..6f9c85c 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -20,7 +20,8 @@ env:
jobs:
version-and-release:
- # Computes semantic version, creates git tag, and publishes GitHub release.
+ # Computes semantic version from conventional commits, creates git tag,
+ # and publishes GitHub release.
name: Compute Version and Tag
runs-on: ubuntu-latest
permissions:
@@ -29,37 +30,59 @@ jobs:
# Guard: run only for merged PRs.
if: github.event.pull_request.merged == true
outputs:
- changed: ${{ steps.semver.outputs.changed }}
- version: ${{ steps.semver.outputs.version }}
+ changed: ${{ steps.version.outputs.changed }}
+ version: ${{ steps.version.outputs.version }}
steps:
- name: Checkout
uses: actions/checkout@v5
with:
- # Full history is required by semantic-version to inspect commit history.
+ # Full history is required by semver-action to inspect commit history.
fetch-depth: 0
ref: ${{ github.event.pull_request.merge_commit_sha }}
- - name: Compute semantic version from commits
+ - name: Compute semantic version from conventional commits
id: semver
- uses: paulhatch/semantic-version@v5.4.0
+ uses: ietf-tools/semver-action@v1.11.0
with:
- tag_prefix: ""
- version_format: "${major}.${minor}.${patch}"
- major_pattern: "(BREAKING CHANGE|!:)"
- minor_pattern: "^feat"
- search_commit_body: true
- bump_each_commit: false
+ token: ${{ github.token }}
+ branch: main
+ # Accept only plain SemVer tags in this repository (no v-prefix).
+ tagFilter: '^[0-9]+\.[0-9]+\.[0-9]+$'
+ prefix: ""
+ skipInvalidTags: true
+ maxTagsToFetch: 50
+
+ # Conventional commit mappings for release bump policy.
+ minorList: "feat,feature"
+ patchList: "fix,bugfix,hotfix,opt,patch,perf,refactor,chore,revert"
+
+ # Keep release workflow non-failing when a rerun has no new commits,
+ # or when no commit maps to a bump category.
+ noNewCommitBehavior: current
+ noVersionBumpBehavior: current
+
+ - name: Normalize version outputs
+ id: version
+ run: |
+ if [[ "${{ steps.semver.outputs.bump }}" == "none" ]]; then
+ echo "changed=false" >> "$GITHUB_OUTPUT"
+ echo "version=${{ steps.semver.outputs.current }}" | sed 's/^version=v/version=/' >> "$GITHUB_OUTPUT"
+ else
+ echo "changed=true" >> "$GITHUB_OUTPUT"
+ echo "version=${{ steps.semver.outputs.nextStrict }}" >> "$GITHUB_OUTPUT"
+ fi
- name: Show computed version
run: |
- echo "changed=${{ steps.semver.outputs.changed }}"
- echo "version=${{ steps.semver.outputs.version }}"
+ echo "bump=${{ steps.semver.outputs.bump }}"
+ echo "changed=${{ steps.version.outputs.changed }}"
+ echo "version=${{ steps.version.outputs.version }}"
- name: Enforce no-v SemVer tags
- if: steps.semver.outputs.changed == 'true'
+ if: steps.version.outputs.changed == 'true'
run: |
- VERSION="${{ steps.semver.outputs.version }}"
+ VERSION="${{ steps.version.outputs.version }}"
# Repository policy: tags must be plain SemVer (for example 1.0.0), never v1.0.0.
if [[ "$VERSION" =~ ^v ]]; then
@@ -73,32 +96,32 @@ jobs:
fi
- name: Create and push git tag
- if: steps.semver.outputs.changed == 'true'
+ if: steps.version.outputs.changed == 'true'
run: |
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
- git rev-parse "${{ steps.semver.outputs.version }}" >/dev/null 2>&1 && {
- echo "ERROR: tag '${{ steps.semver.outputs.version }}' already exists.";
+ git rev-parse "${{ steps.version.outputs.version }}" >/dev/null 2>&1 && {
+ echo "ERROR: tag '${{ steps.version.outputs.version }}' already exists.";
exit 1;
}
- git tag -a "${{ steps.semver.outputs.version }}" -m "release ${{ steps.semver.outputs.version }}"
- git push origin "${{ steps.semver.outputs.version }}"
+ git tag -a "${{ steps.version.outputs.version }}" -m "release ${{ steps.version.outputs.version }}"
+ git push origin "${{ steps.version.outputs.version }}"
- name: Compute release date
- if: steps.semver.outputs.changed == 'true'
+ if: steps.version.outputs.changed == 'true'
id: release_date
run: echo "date=$(date -u +%Y-%m-%d)" >> "$GITHUB_OUTPUT"
- name: Create GitHub release
- if: steps.semver.outputs.changed == 'true'
+ if: steps.version.outputs.changed == 'true'
uses: softprops/action-gh-release@v2
with:
- tag_name: ${{ steps.semver.outputs.version }}
- name: Release v${{ steps.semver.outputs.version }} (${{ steps.release_date.outputs.date }})
+ tag_name: ${{ steps.version.outputs.version }}
+ name: Release v${{ steps.version.outputs.version }} (${{ steps.release_date.outputs.date }})
generate_release_notes: true
build-artifacts:
- # Build distributions only when semantic-version reports a new release.
+ # Build distributions only when semver-action reports a new release.
name: Build Package Artifacts
runs-on: ubuntu-latest
needs: version-and-release
diff --git a/.github/workflows/verify.yml b/.github/workflows/verify.yml
index ab8ab04..333abc8 100644
--- a/.github/workflows/verify.yml
+++ b/.github/workflows/verify.yml
@@ -14,6 +14,7 @@ permissions:
env:
# Shared interpreter version for reproducible CI behavior.
PYTHON_VERSION: "3.11"
+ # Keep Poetry virtual environments inside the workspace for deterministic paths.
POETRY_VIRTUALENVS_IN_PROJECT: "true"
jobs:
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index d8fb74c..a65d9ce 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -1,4 +1,12 @@
repos:
+ - repo: https://github.com/commit-check/commit-check
+ rev: v2.6.0
+ hooks:
+ - id: check-message
+ stages: [commit-msg]
+ - id: check-branch
+ stages: [pre-push]
+
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v6.0.0
hooks:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 7ec805f..8731914 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,26 @@
# Changelog
+## 1.2.0 - 2026-04-21
+
+CLI provenance verification and documentation system alignment.
+
+### Added in 1.2.0
+
+- CLI artifact provenance verification against the install manifest.
+
+### Changed in 1.2.0
+
+- Refactored CLI parser flow into a `CommandLineParser` class and simplified install and verify control flow.
+- Refactored frontmatter serialization internals (instance-method serializer, naming cleanup, and reduced nested parse/validation flow).
+- Aligned hand-authored Markdown docs toward Mermaid-first diagram usage, with ASCII as fallback where needed.
+- Updated skill template guidance and regenerated installed skill artifacts to adopt the Mermaid-first diagram convention.
+- README branding/header refreshed with centered logo and badges plus light/dark logo switching.
+- Centralized commit and branch policy in `cchk.toml` and wired local `pre-commit` hooks for commit-message and pre-push branch validation.
+- Removed hard CI scope allowlist enforcement; commit scopes are now guidance-level in documentation rather than a strict gate.
+- Fixed multiple documentation link paths under `docs/design/` so relative Markdown links resolve correctly on GitHub.
+- Updated `CONTRIBUTING.md` commit-policy wording to match the current `cchk.toml` enforcement model.
+- Strengthened generated skill footer tests to assert the `AUTO-GENERATED` and `VSTACK-META` footer structure at end-of-file.
+
## 1.1.0 — 2026-04-20
Runtime response-style control via the new `concise` skill.
@@ -95,8 +116,6 @@ Community health and release workflow update.
- Release workflow title format updated to `Release vX.Y.Z (YYYY-MM-DD)`.
-______________________________________________________________________
-
## 1.0.2 — 2026-04-17
Release workflow fix.
@@ -105,8 +124,6 @@ Release workflow fix.
- Release workflow: corrected Poetry setup order and opted in to Node 24 to resolve runner deprecation warnings.
-______________________________________________________________________
-
## 1.0.1 — 2026-04-17
Tooling and release hygiene update focused on making local and CI verification match.
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 41832e1..ffbadf1 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -30,14 +30,73 @@ make test
## Commit Message Guidance
-This repository uses semantic commit prefixes for releases.
+This repository uses a Conventional Commits baseline for release automation.
-- `feat:` for new functionality
-- `fix:` for bug fixes
-- `chore:` for maintenance
-- `docs:` for documentation only changes
+### 1) Message format
-Use `BREAKING CHANGE` in the commit body when applicable.
+```text
+type(optional-scope)!: short summary
+```
+
+- `type`: what kind of change you made.
+- `scope` (optional): where the change happened.
+- `!`: marks a breaking change.
+
+### 2) Version bump rules
+
+- `major`: any commit with `!` in the header or a `BREAKING CHANGE:` footer.
+- `minor`: `feat`, `feature`.
+- `patch`: `fix`, `bugfix`, `hotfix`, `opt`, `patch`, `perf`, `refactor`, `chore`, `revert`.
+
+### 3) Allowed types
+
+- `feat`, `feature`: new behavior or capabilities.
+- `fix`, `bugfix`: bug correction.
+- `hotfix`: urgent production fix.
+- `opt`: small, practical optimization.
+- `patch`: small maintenance fix that should still trigger a patch release.
+- `perf`: measurable performance improvement.
+- `refactor`: structural cleanup without intended behavior change.
+- `chore`: repository maintenance and non-feature housekeeping.
+- `revert`: rollback of a previous commit.
+
+### 4) Suggested scopes (optional)
+
+- Domain and backend areas: `api`, `auth`, `permissions`, `serializer`, `viewset`, `orm`, `migrations`, `admin`, `settings`.
+- Tooling and workflow areas: `deps`, `ci`, `docs`, `build`, `style`, `release`, `workflow`.
+- Quality and test areas: `test`, `tests`.
+- Project modules: `cli`, `agents`, `skills`, `instructions`, `prompts`, `frontmatter`, `artifacts`.
+
+### 5) Examples
+
+- `feat(cli): add --global verify mode`
+- `fix(auth): handle missing token header`
+- `hotfix(ci): handle broken tag push race`
+- `opt(serializer): simplify optional-field validation path`
+- `patch(docs): clarify release gating`
+- `perf(orm): reduce query count in list endpoint`
+- `refactor(artifacts)!: drop legacy manifest key`
+
+### 6) Subject length
+
+- Commit subject lines are limited to 100 characters by CI.
+
+### 7) Branch naming
+
+Branch names are validated in CI using Conventional Branch format:
+
+```text
+type/short-description
+```
+
+Allowed branch types:
+
+- `feature`, `bugfix`, `hotfix`, `release`, `chore`, `feat`, `fix`
+- `docs`, `refactor`, `perf`, `test`, `ci`, `build`, `style`
+- `opt`, `patch`, `dependabot`
+
+A dedicated CI workflow validates branch-push commit messages (non-main branches) using commit-check with policy from `cchk.toml`.
+Scope names are guidance-level in this document and are not currently hard-enforced by CI.
## Pull Request Expectations
diff --git a/README.md b/README.md
index 153ac0b..f07ec6e 100644
--- a/README.md
+++ b/README.md
@@ -1,120 +1,196 @@
-# vstack
+
+
+
+
+
-Inspired by gstack, vstack is intentionally rebuilt around a VS Code-native,
-template-driven workflow model.
+[](pyproject.toml)
+[](https://github.com/eschaar/vstack/actions/workflows/verify.yml)
+[](https://github.com/eschaar/vstack/actions/workflows/security.yml)
+[](pyproject.toml)
+[](LICENSE)
-vstack is a VS Code–native AI engineering workflow system. It provides structured
-skills — executable by GitHub Copilot in Agent Mode — for planning, reviewing,
-verifying, and releasing software.
+
-What gets built (microservice, API, package, app, fullstack system) is determined
-by the product vision. vstack adapts to that vision through 6 fixed roles:
-**product, architect, designer, engineer, tester, release.**
+vstack is a VS Code-native AI engineering workflow system for backend services,
+libraries, APIs, and adjacent platform work. It installs structured agents,
+skills, instructions, and prompts into `.github/` so GitHub Copilot Agent Mode
+has a clear operating model instead of ad hoc chat prompts.
+
+What gets built is determined by the product vision. vstack fixes the delivery
+roles and boundaries: `product`, `architect`, `designer`, `engineer`, `tester`,
+and `release`.
+
+vstack started as a rethink inspired by [gstack](https://github.com/observiq/gstack),
+but was rebuilt around a template-driven, VS Code-first workflow model.
+
+______________________________________________________________________
+
+## Why vstack
+
+- Fixed role model with explicit ownership boundaries
+- Template-driven install model from `src/vstack/_templates/`
+- Backend-first verification, security, and release discipline
+- No runtime dependencies beyond the Python standard library
+- Works at project scope or globally in the VS Code user profile
______________________________________________________________________
-## install
+## Quickstart
Requires **Python 3.11-3.14**.
+### Distribution status
+
+vstack is not published to PyPI yet. The current workflow is source-based usage
+from this repository.
+
+### 1. Clone and install the development environment
+
```bash
-pip install vstack
+git clone git@github.com:eschaar/vstack.git
+cd vstack
+poetry install
```
-Then install vstack artifacts into your project:
+### 2. Install the artifacts
+
+From the checked-out repository, install artifacts into one repository:
```bash
-vstack install --target /path/to/your/project
+poetry run vstack install --target /path/to/your/project
```
-Or into the VS Code user profile (available in all projects for agents/prompts/instructions/skills):
+Or install into your VS Code profile so the artifacts are available across projects:
```bash
-vstack install --global
+poetry run vstack install --global
+```
+
+If you prefer, you can also run the package entrypoint after an editable install in
+your active environment.
+
+### 3. Open Copilot Agent Mode and invoke a role
+
+```text
+@product Review my plan for a payments service
+@architect Review the API contracts in src/api/
+@tester /security Audit the authentication module
```
-No extra VS Code settings needed. Agents are auto-discovered from `.github/agents/`
-in the workspace and from the user profile agents directory. Skills installed globally
-are available under the VS Code user profile skills directory.
+No extra VS Code settings are required. Installed role agents are discovered from
+workspace `.github/agents/` and from the VS Code user profile.
______________________________________________________________________
-## agent and skill usage
+## Flow
+
+```mermaid
+flowchart LR
+ A[Product intent] --> B[@product]
+ B --> C[@architect]
+ C --> D[@designer]
+ D --> E[@engineer]
+ E --> F[@tester]
+ F --> G[@release]
+ B -. focused procedure .-> H[/requirements or vision/]
+ F -. focused procedure .-> I[/verify, security, performance/]
+```
-### Agents vs. skills: what is the difference?
+The exact deliverable can be a microservice, API, package, library, app, or broader
+system. The product vision defines scope; vstack defines how the work is carried.
-- **Agents** (like `@product`, `@tester`) are the main interface. Use them for all primary flows. Agents automatically select the right skills for their role and context.
-- **Skills** (like `/verify`, `/security`) can be invoked directly if you want to force a specific check. This is optional and usually not needed.
+______________________________________________________________________
-#### Invocation examples
+## Building Blocks
-| Goal | Agent invocation | Optionally: direct skill invocation |
-| --------------------- | ---------------- | ----------------------------------- |
-| Gather requirements | `@product` | |
-| Architecture review | `@architect` | |
-| API design | `@designer` | |
-| Code review | `@engineer` | `/code-review` |
-| Verification/QA | `@tester` | `/verify` |
-| Security check | `@tester` | `/security` |
-| Performance benchmark | `@tester` | `/performance` |
-| Prepare release | `@release` | |
+| Artifact type | Purpose | Typical invocation |
+| ------------- | ---------------------------------------------------------- | ---------------------- |
+| Agents | Main operating interface for role-based work | `@product`, `@tester` |
+| Skills | Reusable task procedures | `/verify`, `/security` |
+| Instructions | Baseline policy and repo guardrails | auto-loaded by context |
+| Prompts | Reusable prompt artifacts where direct prompting is useful | explicit prompt use |
-> **Note:** You can also force a skill via an agent, e.g. `@tester /security` for a security check by the tester agent.
+Boundary rule:
-#### Subagent pattern
+- Policies belong in instructions.
+- Procedures belong in skills.
-The product agent can invoke other agents as subagents to orchestrate a complete flow:
+See [docs/design/instructions.md](docs/design/instructions.md),
+[docs/design/skills.md](docs/design/skills.md), and
+[docs/architecture/adr/013-instructions-vs-skills-boundary.md](docs/architecture/adr/013-instructions-vs-skills-boundary.md).
-```text
-@product Deliver a requirements-to-release plan for a new payments service
-```
+______________________________________________________________________
+
+## Roles
+
+| Role | Invocation | Primary areas |
+| --------- | ------------ | ------------------------------------------------------- |
+| product | `@product` | vision, requirements, onboarding, docs |
+| architect | `@architect` | architecture, ADRs |
+| designer | `@designer` | service design, OpenAPI, DX review |
+| engineer | `@engineer` | implementation, debugging, refactoring, dependency work |
+| tester | `@tester` | verification, security, incident review, performance |
+| release | `@release` | release notes, PR creation, release gating |
+
+Full skill index: [docs/design/skills.md](docs/design/skills.md)
+
+______________________________________________________________________
+
+## Example Usage
-This automatically triggers:
+### Idea to release
-- @product → requirements
-- @architect → architecture
-- @designer → API contract
-- @engineer → implementation
-- @tester → verification
-- @release → release gating
+1. `@product` to lock requirements and success criteria
+1. `@architect` to define service boundaries and ADRs
+1. `@designer` to define APIs, schemas, and flows
+1. `@engineer` to implement
+1. `@tester` to verify behavior and risk
+1. `@release` to prepare the release path
-#### Use case 1: New API from idea to release
+### Direct skill usage when you want a focused tool
-1. `@product` — (agent, requirements and acceptance)
-1. `@architect` — (agent, architecture review)
-1. `@designer` — (agent, OpenAPI spec)
-1. `@engineer` — (agent, implementation)
-1. `@tester` — (agent, full QA)
-1. `@tester /security` — (agent with explicit skill invocation, security check)
-1. `@release` — (agent, release gating)
+| Goal | Agent invocation | Optional direct skill |
+| ------------------- | ---------------- | --------------------- |
+| Requirements | `@product` | |
+| Architecture review | `@architect` | |
+| API design | `@designer` | |
+| Code review | `@engineer` | `/code-review` |
+| Verification | `@tester` | `/verify` |
+| Security audit | `@tester` | `/security` |
+| Performance check | `@tester` | `/performance` |
-#### Use case 2: Security regression in authentication
+You can also force a skill through an agent when you want the role framing and the
+procedure together, for example `@tester /security`.
-1. `@tester /security` — (agent with explicit skill invocation)
-1. `@engineer` — (agent, bugfix)
-1. `@tester` — (agent, re-verification)
-1. `@release` — (agent, release)
+### Subagent pattern
-#### Use case 3: Refactor and performance improvement
+The product agent can invoke other agents as subagents to orchestrate a complete flow:
+
+```text
+@product Deliver a requirements-to-release plan for a new payments service
+```
+
+Typical downstream path:
-1. `@engineer` — (agent, refactor)
-1. `@tester /performance` — (agent with explicit skill invocation)
-1. `@engineer` — (agent, optimization)
-1. `@tester` — (agent, confirm performance gain)
-1. `@release` — (agent, release)
+- `@product` -> requirements
+- `@architect` -> architecture
+- `@designer` -> API contract
+- `@engineer` -> implementation
+- `@tester` -> verification
+- `@release` -> release gating
______________________________________________________________________
-## vstack gebruiken in Copilot Agent Mode
+## Using vstack in Copilot Agent Mode
### 1. Open Copilot Chat
-`Ctrl+Shift+I` (or `Cmd+Shift+I`) — or click the Copilot icon in the sidebar.
+Use `Ctrl+Shift+I` or `Cmd+Shift+I`, or click the Copilot icon in the sidebar.
### 2. Switch to Agent Mode
-In the Copilot Chat panel, click the mode selector (shows "Ask" by default) and
-switch to **Agent**.
+In the Copilot Chat panel, switch the mode selector from `Ask` to `Agent`.
### 3. Invoke a role
@@ -125,8 +201,8 @@ switch to **Agent**.
@tester Run a security audit
```
-Each role agent uses the appropriate skills automatically.
-You can also ask a role to use a specific skill:
+Each role agent uses the appropriate skills automatically. You can also ask a role
+to use a specific skill:
```text
@tester use the verify skill with regression focus and report findings
@@ -144,11 +220,9 @@ You can also ask a role to use a specific skill:
| tester | `@tester` | verify, inspect, security, incident, dependency, docs | ultra |
| release | `@release` | release-notes, pr, docs | compact |
-Full skill index: [docs/design/skills.md](docs/design/skills.md)
-
______________________________________________________________________
-## model guidance
+## Model Guidance
| Use case | Recommended model |
| ------------------------ | ------------------------------------ |
@@ -161,64 +235,62 @@ ______________________________________________________________________
Claude Sonnet 4.6 is the best balance of speed, quality, and cost for most runs.
Use Claude Opus 4.6 for architecture reviews or complex debugging.
-In Copilot Chat, click the model selector to switch.
-
______________________________________________________________________
-## tips
+## Tips
-**Give the agent your project context:**
+### Give the agent project context
```text
/verify Please first read CONTRIBUTING.md for test commands
```
-**Scope the agent's focus:**
+### Scope the agent's focus
```text
/code-review Review changes in src/api/ only
/security Audit the authentication module in src/auth/
```
-**Controlling response verbosity:**
+### Control response verbosity
-Every role agent supports the `concise` skill. Switch response density without regenerating any artifacts:
+Every role agent supports the `concise` skill:
```text
-/concise normal — full explanations (architect default)
-/concise compact — shorter prose, same technical accuracy (most role default)
-/concise ultra — maximum brevity, facts and commands only (tester default)
-/concise status — show active mode, session override, and agent default
-/concise on — alias for compact
-/concise off — alias for normal
+/concise normal - full explanations
+/concise compact - shorter prose, same technical accuracy
+/concise ultra - maximum brevity
+/concise status - show active mode, session override, and agent default
+/concise on - alias for compact
+/concise off - alias for normal
```
-The mode is session-scoped — no reinstall needed. Security warnings and destructive
-action prompts always use `normal` regardless of active mode.
+The mode is session-scoped. Security warnings and destructive action prompts always
+use `normal` regardless of active mode.
-**Typical workflow for a new feature:**
+### Typical workflow for a new feature
```text
-1. /vision — validate the approach
-2. /architecture — lock down the technical design
+1. /vision
+2. /architecture
3. (implement)
-4. /verify — test and validate
-5. /release — cut the release
+4. /verify
+5. /release
```
______________________________________________________________________
-## development
+## Development
Requires **Poetry** and **Python 3.11-3.14**.
```bash
-git clone https://github.com/your-org/vstack
+git clone git@github.com:eschaar/vstack.git
cd vstack
poetry install
```
-### quick commands (make)
+### Common commands
```bash
make help
@@ -227,25 +299,16 @@ make install
make check
make vstack-install
make ci
+poetry run vstack validate
+poetry run vstack install
+poetry run vstack verify
+make test-local
+make test
+make tox
+make tox-all
```
-### development tasks
-
-```bash
-poetry run vstack validate # validate templates without writing files
-poetry run vstack install # install all artifacts -> .github/
-poetry run vstack verify # full validation suite
-make test-local # run pytest on current interpreter
-make test # run tests on py311-py314 via tox
-make tox # run tests on py311-py314 (when installed)
-```
-
-### local multi-python testing (pyenv + tox)
-
-This repository keeps its local interpreter order in `.python-version` so `tox`
-can resolve the supported runtimes consistently.
-
-Install and activate the supported Python runtimes with `pyenv`:
+### Multi-version local testing with pyenv
```bash
pyenv install 3.11.14
@@ -255,16 +318,10 @@ pyenv install 3.14.3
pyenv local 3.14.3 3.13.12 3.12.12 3.11.14
```
-That command writes the repo-local `.python-version` file.
+### Editing templates
-Run cross-version tests locally:
-
-```bash
-make tox # pytest on py311, py312, py313, py314
-make tox-all # all tox envs: tests + lint + type
-```
-
-**Editing templates:** source of truth is always `src/vstack/_templates///{config.yaml,template.md}`. Never edit generated files in `.github/`.
+Source of truth is always under `src/vstack/_templates/`. Do not edit generated
+files in `.github/`.
```bash
vim src/vstack/_templates/skills/verify/template.md
@@ -272,70 +329,82 @@ vim src/vstack/_templates/agents/engineer/template.md
vim src/vstack/_templates/instructions/python/template.md
poetry run vstack validate
poetry run pytest
+poetry run vstack install
```
______________________________________________________________________
-## project structure
+## Repository Structure
-```bash
+```text
vstack/
-├── src/vstack/ ← Python package (source of truth)
-│ ├── frontmatter/ ← parser, builder, schema
-│ ├── artifacts/ ← GenericArtifactGenerator
-│ ├── skills/ ← SKILL_SCHEMA, SKILL_TYPE
-│ ├── agents/ ← AGENT_SCHEMA, AGENT_TYPE
-│ ├── instructions/ ← instruction artifact config + generator
-│ ├── prompts/ ← prompt artifact config + generator
-│ ├── cli/ ← commands, parser, constants
-│ └── _templates/
-│ ├── skills//config.yaml ← skill frontmatter fields
-│ ├── skills//template.md ← skill instructions body
-│ ├── skills/_partials/ ← shared partials
-│ ├── agents//config.yaml ← agent frontmatter fields
-│ ├── agents//template.md ← agent instructions body
-│ ├── instructions//config.yaml ← instruction frontmatter
-│ ├── instructions//template.md ← instruction body
-│ ├── prompts//config.yaml ← prompt frontmatter
-│ └── prompts//template.md ← prompt body
+├── src/vstack/ ← Python package and source of truth
+│ ├── artifacts/ ← generic artifact generation and metadata
+│ ├── frontmatter/ ← parser, serializer, schema
+│ ├── agents/ ← agent configuration and wrappers
+│ ├── skills/ ← skill configuration and wrappers
+│ ├── instructions/ ← instruction configuration and wrappers
+│ ├── prompts/ ← prompt configuration and wrappers
+│ ├── cli/ ← install, verify, uninstall, parser
+│ └── _templates/ ← hand-authored templates
├── docs/
-│ ├── architecture/
-│ │ ├── architecture.md ← system structure
-│ │ └── adr/ ← architecture decision records
-│ ├── design/
-│ │ ├── design.md ← generator and builder internals
-│ │ ├── skills.md ← full skill index
-│ │ └── workflow.md ← execution model
-│ └── product/
-│ ├── requirements.md
-│ ├── roadmap.md ← milestones
-│ └── vision.md
-├── tests/
-│ └── vstack/
-└── Makefile ← generic local dev tasks
+│ ├── architecture/ ← architecture docs and ADRs
+│ ├── design/ ← design, workflow, skills, instructions
+│ └── product/ ← vision, requirements, roadmap
+├── tests/ ← unit and integration coverage
+├── .github/ ← generated artifacts and repository automation
+├── pyproject.toml ← packaging and tooling config
+└── Makefile ← local development tasks
```
______________________________________________________________________
-## troubleshooting
+## CI and Release Automation
+
+| Workflow | Trigger | Purpose |
+| -------------- | ----------------------------- | ----------------------------------------------------------- |
+| `qa.yml` | Push to non-main branches | fast branch feedback for format, lint, typecheck, and tests |
+| `commit.yml` | Push to non-main branches | commit and branch naming policy enforcement |
+| `verify.yml` | Pull request to `main` | source validation plus install/verify flow checks |
+| `security.yml` | Pull request to `main` | dependency audit and secret scanning |
+| `release.yml` | Merged pull request to `main` | SemVer calculation, tag, release, and distributions |
+
+Commit policy specifics:
+
+- Type validation is configured via `CCHK_*` variables in `.github/workflows/commit.yml`.
+- Commit subject length is limited to 100 characters.
+- Branch names use the `type/description` convention.
+- Allowed branch types are `feature`, `bugfix`, `hotfix`, `release`, `chore`, `feat`, `fix`, `docs`, `refactor`, `perf`, `test`, `ci`, `build`, `style`, `opt`, `patch`, and `dependabot`.
+
+Recommended branch protection for `main`:
+
+- Require PR before merge.
+- Require status checks from `verify.yml` and `security.yml`.
+- Disallow force pushes and branch deletion.
+
+______________________________________________________________________
+
+## Troubleshooting
+
+### Agents are not appearing
+
+1. Run `vstack install --global` or `poetry run vstack install --global`
+1. Confirm the templates exist under `src/vstack/_templates/agents/`
+1. Reload VS Code with `Developer: Reload Window`
-**Agents not appearing:**
+### Agent is not running commands
-1. Run `poetry run vstack install --global` (or `vstack install --global`)
-1. Ensure templates exist under `src/vstack/_templates/agents/`
-1. Reload VS Code: `Ctrl+Shift+P` → `Developer: Reload Window`
+Make sure Copilot is in Agent Mode, not Ask or Edit mode.
-**Best-practice local workflow:**
+### Best-practice local workflow
-1. `make bootstrap` once per machine/repo clone
-1. `make check` before every commit
-1. `make ci` to mirror the quality gate locally
+1. Run `make bootstrap` once per machine or clone.
+1. Run `make check` before every commit.
+1. Run `make ci` when you want to mirror the main quality gate locally.
-**Agent is not running commands:**
-Make sure you are in **Agent Mode** — not Ask or Edit mode.
+### Search results are noisy
-**Agent search is noisy (dependency folders):**
-Use workspace-local exclusions in VS Code to keep context focused.
+Use workspace-local exclusions in VS Code:
```json
{
@@ -364,12 +433,17 @@ Use workspace-local exclusions in VS Code to keep context focused.
______________________________________________________________________
-## roadmap
+## Further Reading
-See [docs/product/roadmap.md](docs/product/roadmap.md)
+- [docs/architecture/architecture.md](docs/architecture/architecture.md)
+- [docs/design/design.md](docs/design/design.md)
+- [docs/design/workflow.md](docs/design/workflow.md)
+- [docs/design/skills.md](docs/design/skills.md)
+- [docs/product/roadmap.md](docs/product/roadmap.md)
+- [CONTRIBUTING.md](CONTRIBUTING.md)
______________________________________________________________________
-## license
+## License
MIT. See [LICENSE](LICENSE).
diff --git a/assets/branding/vstack.png b/assets/branding/vstack.png
new file mode 100644
index 0000000..ca17dfe
Binary files /dev/null and b/assets/branding/vstack.png differ
diff --git a/assets/branding/vstack_dm.png b/assets/branding/vstack_dm.png
new file mode 100644
index 0000000..07ead5e
Binary files /dev/null and b/assets/branding/vstack_dm.png differ
diff --git a/assets/branding/vstack_lm_logo.png b/assets/branding/vstack_lm_logo.png
new file mode 100644
index 0000000..105e13e
Binary files /dev/null and b/assets/branding/vstack_lm_logo.png differ
diff --git a/assets/branding/vstack_logo_nobg.png b/assets/branding/vstack_logo_nobg.png
new file mode 100644
index 0000000..3f59192
Binary files /dev/null and b/assets/branding/vstack_logo_nobg.png differ
diff --git a/cchk.toml b/cchk.toml
new file mode 100644
index 0000000..2d3faa3
--- /dev/null
+++ b/cchk.toml
@@ -0,0 +1,47 @@
+[commit]
+conventional_commits = true
+subject_capitalized = false
+subject_imperative = false
+subject_max_length = 100
+allow_commit_types = [
+ "feat",
+ "feature",
+ "fix",
+ "bugfix",
+ "hotfix",
+ "opt",
+ "patch",
+ "perf",
+ "refactor",
+ "chore",
+ "revert",
+]
+allow_merge_commits = true
+allow_revert_commits = true
+allow_empty_commits = false
+allow_fixup_commits = true
+allow_wip_commits = false
+require_body = false
+require_signed_off_by = false
+
+[branch]
+conventional_branch = true
+allow_branch_types = [
+ "feature",
+ "bugfix",
+ "hotfix",
+ "release",
+ "chore",
+ "feat",
+ "fix",
+ "docs",
+ "refactor",
+ "perf",
+ "test",
+ "ci",
+ "build",
+ "style",
+ "opt",
+ "patch",
+ "dependabot",
+]
diff --git a/docs/architecture/adr/013-instructions-vs-skills-boundary.md b/docs/architecture/adr/013-instructions-vs-skills-boundary.md
new file mode 100644
index 0000000..d49d6a2
--- /dev/null
+++ b/docs/architecture/adr/013-instructions-vs-skills-boundary.md
@@ -0,0 +1,84 @@
+# ADR-013: Policy vs Procedure Boundary for Instructions and Skills
+
+> Maintained by: **architect** role
+
+**date:** 2026-04-20\
+**status:** accepted
+
+## context
+
+vstack supports two reusable guidance mechanisms:
+
+- Instructions (`.instructions.md`) with optional `applyTo` path matching.
+- Skills (`SKILL.md`) that are invoked explicitly or auto-selected by task intent.
+
+As vstack expands language and framework support, maintainers need a stable rule
+for where guidance should live. Without an explicit boundary, the same content can
+be duplicated or misplaced, leading to inconsistent behavior.
+
+## decision
+
+vstack adopts the following boundary:
+
+1. **Instructions are policy.**
+
+ Use instructions for always-on rules, coding standards, safety constraints,
+ and repository conventions. Use `applyTo` when the policy is file-pattern scoped
+ (for example `**/*.py`).
+
+1. **Skills are procedures.**
+
+ Use skills for task workflows, operational sequences, and specialist methods.
+ Skills are optional and intent-driven; they do not replace baseline policy.
+
+1. **Language-specific baseline policy may live in instructions.**
+
+ This is valid when the guidance is a repository-wide coding standard for that
+ language and should apply consistently across roles and workflows.
+
+## alternatives considered
+
+1. **Put all language/framework guidance in skills only.** Rejected.
+
+ This would weaken baseline enforcement for repository coding standards and lose
+ file-pattern matching advantages from `applyTo`.
+
+1. **Put all procedure detail in instructions.** Rejected.
+
+ This would reduce modularity and increase always-on context load.
+
+1. **No explicit boundary.** Rejected.
+
+ This causes authoring ambiguity and inconsistent guidance placement.
+
+## rationale
+
+The split aligns each mechanism with its strengths:
+
+- Instructions provide predictable policy enforcement and path-scoped applicability.
+- Skills provide modular, lazy, task-focused execution guidance.
+
+This preserves both consistency (policy) and composability (procedures).
+
+## consequences
+
+### positive
+
+- Clear authoring decision rule for new templates.
+- Lower risk of duplicated or conflicting guidance.
+- Better scaling for new language/framework additions.
+
+### negative / tradeoffs
+
+- Some topics may still require judgment when they include both policy and procedure.
+- Documentation must be kept synchronized across design references.
+
+### risks
+
+- Overly broad instructions can still increase context size if not curated.
+- Overly thin skills can become under-specified if policy and procedure are mixed.
+
+## related ADRs
+
+- ADR-011: skill restructure
+- ADR-012: flat templates and install-time generation
diff --git a/docs/architecture/architecture.md b/docs/architecture/architecture.md
index 9eb396e..cd99907 100644
--- a/docs/architecture/architecture.md
+++ b/docs/architecture/architecture.md
@@ -1,7 +1,7 @@
# vstack — architecture
> Maintained by: **agents** role\
-> Last updated: 2026-04-01
+> Last updated: 2026-04-21
## overview
@@ -16,34 +16,26 @@ ______________________________________________________________________
```text
vstack/
├── src/vstack/ ← Python package (source of truth)
-│ ├── frontmatter/ ← parser, builder, schema
+│ ├── frontmatter/ ← parser, serializer, schema
│ ├── artifacts/ ← GenericArtifactGenerator, ArtifactTypeConfig
│ ├── skills/ ← SKILL_SCHEMA, SKILL_TYPE
│ ├── agents/ ← AGENT_SCHEMA, AGENT_TYPE
-│ └── cli/ ← commands, parser, constants
-├── _templates/
-│ ├── skills/
-│ │ ├── /
-│ │ │ ├── config.yaml ← skill frontmatter fields
-│ │ │ └── template.md ← skill instructions body
-│ │ └── _partials/
-│ │ └── *.md ← shared partial snippets
-│ └── agents/
-│ └── /
-│ ├── template.md ← agent instructions body
-│ └── config.yaml ← agent frontmatter fields
-├── test/
-│ └── test_skills.py
+│ ├── instructions/ ← instruction config and wrappers
+│ ├── prompts/ ← prompt config and wrappers
+│ ├── cli/ ← commands, parser, constants
+│ └── _templates/ ← source templates for all artifact types
├── docs/
-│ ├── architecture.md ← this file (architect)
-│ ├── design.md ← component design (designer)
-│ ├── roadmap.md ← milestones + vision (product)
-│ ├── skills.md ← skill reference
-│ ├── workflow.md ← execution flow
-│ └── adr/ ← architecture decision records
+│ ├── architecture/ ← architecture docs + ADRs
+│ ├── design/ ← design, workflow, skills, instructions
+│ └── product/ ← roadmap, requirements, vision
+├── tests/
+│ └── vstack/
├── .github/ ← generated output (never edit directly)
│ ├── skills//SKILL.md
-│ └── agents/.agent.md
+│ ├── agents/.agent.md
+│ ├── instructions/.instructions.md
+│ ├── prompts/.prompt.md
+│ └── vstack.json
└── README.md
```
@@ -100,7 +92,7 @@ See `docs/architecture/adr/009-role-model.md` for the decision record.
### 5. manifest (`vstack.json`)
Generated at install time in the target directory. Tracks every artifact installed
-by `vstack install` (skills and agents) so that `vstack uninstall` can remove
+by `vstack install` (skills, agents, instructions, and prompts) so that `vstack uninstall` can remove
exactly those files. Not committed to the vstack source repo.
### 6. VS Code agent files (`.github/agents/.agent.md`)
@@ -135,26 +127,29 @@ ______________________________________________________________________
### current execution model — single-call
-Copilot reads a `SKILL.md` file and executes the workflow in a single context window.
+Copilot executes the selected role or skill in a single context window.
-```text
-User → /skill-name → Copilot reads .github/agents/.agent.md
- → Executes steps in one model call
- → Writes output to disk
+```mermaid
+flowchart LR
+ U[User request in Agent Mode] --> A[Installed agent or skill artifact]
+ A --> C[Single model call]
+ C --> D[Writes docs, code, or reports to disk]
```
### possible future model — orchestrated role pipeline
Each role makes its own model call. Output artifacts are passed to the next role.
-```text
-User → product (intake)
- → architect (design)
- → designer (specs) [conditional: frontend scope]
- → engineer (build)
- → tester (validate + audit)
- → product (sign-off) [gate: user approval]
- → release (deploy + monitor)
+```mermaid
+flowchart TD
+ U[User request] --> P[product]
+ P --> A[architect]
+ A --> D[designer]
+ D --> E[engineer]
+ E --> T[tester]
+ T --> G{User sign-off}
+ G --> R[release]
+ D -. backend-only path can skip designer .-> E
```
See `docs/architecture/adr/004-option-a-to-b-pipeline.md` and `docs/design/workflow.md` for pipeline detail.
diff --git a/docs/design/design.md b/docs/design/design.md
index d72fd2e..2339720 100644
--- a/docs/design/design.md
+++ b/docs/design/design.md
@@ -1,7 +1,7 @@
# vstack — design
> Maintained by: **designer** role\
-> Last updated: 2026-04-01
+> Last updated: 2026-04-21
## overview
@@ -20,28 +20,27 @@ expressed through an `ArtifactTypeConfig` descriptor rather than subclass overri
### execution flow
-```text
-1. Discover template directories in src/vstack/_templates// (skip _-prefixed dirs)
-2. For each template directory:
- a. Load config.yaml (agent frontmatter source) or parse frontmatter
- from template.md (skills)
- b. Validate frontmatter fields against the artifact schema
- c. Load partials from `src/vstack/_templates//_partials/` (when `partials_subdir` is configured)
- d. Resolve {{PLACEHOLDER}} tokens in the body
- e. Inject frontmatter header + auto-gen footer
-3. install: write output files to target dir
-4. validate / verify: validate all templates; print summary; exit 1 on error
+```mermaid
+flowchart TD
+ A[Discover template directories 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[Inject frontmatter and footer]
+ F --> G{Command mode}
+ G -->|install| H[Write output files to target directory]
+ G -->|validate or verify| I[Validate templates and report failures]
```
### frontmatter architecture (`src/vstack/frontmatter/`)
Frontmatter is handled by a dedicated package:
-| Module | Responsibility |
-| ------------ | ---------------------------------------------------------------------------- |
-| `schema.py` | `FrontmatterSchema` — ordered field specs, types, and constraints |
-| `parser.py` | `FrontmatterParser` — parse YAML frontmatter from Markdown + standalone YAML |
-| `builder.py` | `build_output` — serialize schema-validated fields back to YAML frontmatter |
+| Module | Responsibility |
+| --------------- | ------------------------------------------------------------------------------------ |
+| `schema.py` | `FrontmatterSchema` — ordered field specs, types, and constraints |
+| `parser.py` | `FrontmatterParser` — parse YAML frontmatter from Markdown + standalone YAML |
+| `serializer.py` | `FrontmatterSerializer` — serialize schema-validated fields back to YAML frontmatter |
Supported field types: `string`, `list`, `bool`, `object-list`, `raw`
@@ -154,10 +153,12 @@ Every generated file ends with a footer line (when `auto_gen_footer=True` on the
```text
+
```
-The footer text lives in `src/vstack/artifacts/constants.py` and is shared across all artifact types.
-Currently both skills and agents have `auto_gen_footer=True`.
+The human-readable footer text lives in `src/vstack/artifacts/constants.py` and is shared across all artifact types.
+The machine-readable `VSTACK-META` footer is emitted by `GenericArtifactGenerator._build_footer`.
+Currently skills, agents, instructions, and prompts all have `auto_gen_footer=True`.
______________________________________________________________________
diff --git a/docs/design/instructions.md b/docs/design/instructions.md
new file mode 100644
index 0000000..a3a8783
--- /dev/null
+++ b/docs/design/instructions.md
@@ -0,0 +1,77 @@
+# vstack — instructions
+
+> Maintained by: **designer** role\
+> Last updated: 2026-04-20
+
+## what are instructions?
+
+Instructions are repository guidance files (`*.instructions.md`) loaded by Copilot
+to apply baseline rules. They are policy-oriented and can be scoped with `applyTo`
+patterns.
+
+Examples:
+
+- Global safety and credential handling policy.
+- Git and release hygiene policy.
+- Language-specific coding standards that should apply to all changes in that language.
+
+______________________________________________________________________
+
+## policy vs procedure boundary
+
+vstack uses an explicit split:
+
+- **Instructions = policy** (always-on rules and standards).
+- **Skills = procedure** (task execution workflows).
+
+Canonical decision record:
+[013-instructions-vs-skills-boundary.md](../architecture/adr/013-instructions-vs-skills-boundary.md).
+
+Use instructions when guidance is:
+
+- expected on every applicable change,
+- independent from a specific task flow,
+- naturally expressed as constraints and conventions.
+
+Use skills when guidance is:
+
+- a step-by-step operational process,
+- optional or intent-driven,
+- specialized to one type of task (for example debug, migrate, performance).
+
+______________________________________________________________________
+
+## applyTo patterns
+
+`applyTo` scopes an instruction to matching files.
+
+Examples:
+
+- `**/*.py` for Python policy.
+- `**/*` for repo-wide policy.
+
+`applyTo` is for policy targeting, not for procedural automation.
+
+______________________________________________________________________
+
+## file locations
+
+| Path | Purpose |
+| ------------------------------------------------------- | --------------------------------------------------------- |
+| `src/vstack/_templates/instructions//config.yaml` | Source of truth: instruction metadata and `applyTo` scope |
+| `src/vstack/_templates/instructions//template.md` | Source of truth: instruction body |
+| `.github/instructions/.instructions.md` | Generated output loaded by Copilot |
+
+Never edit generated `.github/instructions/` directly in the source repository.
+Regenerate with `python3 -m vstack install` after template changes.
+
+______________________________________________________________________
+
+## minimum instruction contract
+
+Each instruction should include:
+
+1. Clear applicability and intent.
+1. Concrete rules, not broad aspirations.
+1. Safety constraints where relevant.
+1. Alignment with repository automation (CI, release policy, security policy).
diff --git a/docs/design/skills.md b/docs/design/skills.md
index 04acae4..61f2e10 100644
--- a/docs/design/skills.md
+++ b/docs/design/skills.md
@@ -25,6 +25,14 @@ Unlike custom instructions that primarily define coding preferences and guardrai
skills package specialized, reusable workflows with optional scripts, examples,
and references.
+Boundary rule:
+
+- Policies belong in instructions.
+- Procedures belong in skills.
+
+See [instructions.md](./instructions.md) and
+[013-instructions-vs-skills-boundary.md](../architecture/adr/013-instructions-vs-skills-boundary.md).
+
Key benefits:
- Specialize Copilot for domain-specific tasks without repeating context.
@@ -71,13 +79,13 @@ ______________________________________________________________________
## file locations
-| Path | Purpose |
-| ------------------------------------------------- | -------------------------------------------------------------------------- |
-| `src/vstack/_templates/skills//config.yaml` | Source of truth — skill frontmatter fields |
-| `src/vstack/_templates/skills//template.md` | Source of truth — skill instructions body |
-| `src/vstack/_templates/skills/_partials/*.md` | Shared partials injected via `{{TOKEN}}` |
-| `.github/skills//SKILL.md` | Generated output — what VS Code loads |
-| `.github/vstack.json` | Generated install manifest and artifact index (including installed skills) |
+| Path | Purpose |
+| ------------------------------------------------- | ------------------------------------------------------------------------------ |
+| `src/vstack/_templates/skills//config.yaml` | Source of truth — skill frontmatter fields |
+| `src/vstack/_templates/skills//template.md` | Source of truth — skill instructions body |
+| `src/vstack/_templates/skills/_partials/*.md` | Shared partials injected via `{{TOKEN}}` |
+| `.github/skills//SKILL.md` | Generated output — what VS Code loads |
+| `.github/vstack.json` | Generated install manifest and artifact index for all installed artifact types |
**Never edit `.github/skills/` directly.** Regenerate after every change:
@@ -174,7 +182,7 @@ ______________________________________________________________________
## minimum skill body contract
-We hanteren dit als minimum voor elke skill body:
+Use this as the minimum baseline for every skill body:
1. What it helps accomplish
1. When to use it
@@ -182,7 +190,7 @@ We hanteren dit als minimum voor elke skill body:
1. Expected input/output examples
1. References to scripts/resources
1. Out-of-scope + escalation/failure rules
-1. Common edge cases (sterk aanbevolen)
+1. Common edge cases (strongly recommended)
Recommended additions for production-grade skills:
diff --git a/docs/design/workflow.md b/docs/design/workflow.md
index f0baf0a..ac4587a 100644
--- a/docs/design/workflow.md
+++ b/docs/design/workflow.md
@@ -1,27 +1,75 @@
# vstack — workflow
> Maintained by: **designer** role\
-> Last updated: 2026-04-01
+> Last updated: 2026-04-21
## overview
This document describes how vstack workflows execute today (single-call execution)
and a possible future orchestrated role pipeline.
+It also documents the repository-level GitHub Actions automation used for quality,
+security, commit policy, and releases.
+
+For authoring boundaries between reusable guidance mechanisms:
+
+- [instructions.md](./instructions.md)
+- [skills.md](./skills.md)
+- [013-instructions-vs-skills-boundary.md](../architecture/adr/013-instructions-vs-skills-boundary.md)
+
______________________________________________________________________
-## current execution model — single-call
+## repository automation (GitHub Actions)
+
+The repository uses a split workflow model so each automation concern is isolated
+and easy to reason about.
+
+| Workflow | Trigger | Responsibility |
+| -------------------------------- | --------------------------------------------------------- | ------------------------------------------------------------------------------------- |
+| `.github/workflows/qa.yml` | Push to non-main branches | Fast feedback for formatting, linting, type checks, and tests across Python versions. |
+| `.github/workflows/commit.yml` | Push to non-main branches (with explicit branch excludes) | Validate commit message policy before PR merge. |
+| `.github/workflows/verify.yml` | Pull request to `main` | Validate source behavior and artifact install/verify flow. |
+| `.github/workflows/security.yml` | Pull request to `main` | Dependency vulnerability audit and secret scan. |
+| `.github/workflows/release.yml` | Merged pull request to `main` | Compute SemVer, create tag and GitHub release, build distributions. |
+
+### commit policy enforcement model
+
+Commit policy is defined in `cchk.toml` and enforced by `commit-check`:
+
+1. `.github/workflows/commit.yml` runs `commit-check/commit-check-action@v2` on branch pushes.
+1. Local hooks in `.pre-commit-config.yaml` run the same checks at `commit-msg` and `pre-push` stages.
+
+Additional commit workflow policy:
+
+- Maximum commit subject length is 100 characters.
+- Branch names are validated against Conventional Branch format (`type/description`).
+- Allowed branch types: `feature`, `bugfix`, `hotfix`, `release`, `chore`, `feat`, `fix`, `docs`, `refactor`, `perf`, `test`, `ci`, `build`, `style`, `opt`, `patch`, `dependabot`.
+- Commit scopes are not hard-enforced by CI; scope naming is guidance-level in documentation.
+
+This keeps CI and local checks aligned through one policy source of truth.
-The user invokes a skill from Copilot Agent Mode. Copilot loads the corresponding
-`.agent.md` file and executes the full workflow in a single model call.
+### release bump mapping
-```text
-User types: @
+`release.yml` computes SemVer from commit history using these mappings:
- → VS Code loads .github/agents/.agent.md
- → Copilot executes all steps in one context window
- → Artifacts written to disk (docs/architecture/architecture.md, docs/test-report.md, etc.)
- → Done
+- minor: `feat`, `feature`
+- patch: `fix`, `bugfix`, `hotfix`, `opt`, `patch`, `perf`, `refactor`, `chore`, `revert`
+
+Repository tag policy is strict `X.Y.Z` (no `v` prefix).
+
+______________________________________________________________________
+
+## current execution model — single-call
+
+The user invokes a role or skill from Copilot Agent Mode. Copilot loads the
+relevant installed artifact and executes the full workflow in a single model call.
+
+```mermaid
+flowchart LR
+ U[User request in Agent Mode] --> V[VS Code loads installed agent or skill]
+ V --> C[Copilot executes one context window]
+ C --> W[Artifacts written to disk]
+ W --> D[Done]
```
**Characteristics:**
@@ -38,32 +86,17 @@ ______________________________________________________________________
Each role becomes a separate model call. Output artifacts from one role become
the input context for the next.
-```text
-┌─────────────────────────────────────────────────────────────────┐
-│ PIPELINE │
-│ │
-│ product ──→ [vision.md, requirements.md, roadmap.md] │
-│ ↓ │
-│ architect ──→ [architecture/architecture.md, architecture/adr/*.md] │
-│ ↓ │
-│ designer ──→ [design/design.md] (skip if backend-only) │
-│ ↓ │
-│ ┌── USER GATE 1: approve requirements + design ──┐ │
-│ └────────────────────────────────────────────────┘ │
-│ ↓ │
-│ engineer ──→ [code + unit tests] │
-│ ↓ │
-│ tester ──→ [test-report.md, security-report.md, performance-baseline.md] │
-│ ↓ │
-│ ┌── USER GATE 2: pre-prod sign-off ──┐ │
-│ └─────────────────────────────────────┘ │
-│ ↓ │
-│ ┌── USER GATE 3: final merge approval ──┐ │
-│ └────────────────────────────────────────┘ │
-│ ↓ │
-│ release ──→ [releases/{date}.md, CHANGELOG.md, PR] │
-│ │
-└─────────────────────────────────────────────────────────────────┘
+```mermaid
+flowchart TD
+ P[product vision.md requirements.md roadmap.md] --> A[architect architecture.md adr/*.md]
+ A --> D[designer design.md]
+ D --> G1{User gate 1 requirements and design}
+ D -. backend-only path .-> G1
+ G1 --> E[engineer code and unit tests]
+ E --> T[tester test-report.md security-report.md performance-baseline.md]
+ T --> G2{User gate 2 pre-prod sign-off}
+ G2 --> G3{User gate 3 final merge approval}
+ G3 --> R[release releases/{date}.md CHANGELOG.md PR]
```
**Characteristics:**
@@ -119,17 +152,27 @@ ______________________________________________________________________
Skills are the HOW inside a role call.
-```text
-Role call → loads role persona
- → selects applicable skills
- → executes skill steps sequentially
- → writes output artifacts
+```mermaid
+flowchart LR
+ R[Role call] --> P[Load role persona]
+ P --> S[Select applicable skills]
+ S --> E[Execute skill steps]
+ E --> W[Write output artifacts]
```
A role may use multiple skills in sequence within one model call. For example,
the architect role uses the `adr` skill to write decision records and the
`architecture` skill to produce the architecture document.
+## authoring decision rule
+
+Use this rule when deciding where reusable guidance belongs:
+
+1. If it is a baseline rule or standard, put it in instructions.
+1. If it is a task workflow or method, put it in skills.
+
+Instructions are policy. Skills are procedure.
+
______________________________________________________________________
## forward compatibility
diff --git a/docs/product/requirements.md b/docs/product/requirements.md
index 92c5943..b6ecd08 100644
--- a/docs/product/requirements.md
+++ b/docs/product/requirements.md
@@ -1,6 +1,12 @@
# requirements
+> Maintained by: **product** role
+> Last updated: 2026-04-20
+
Status: draft.
-This file is owned by the product role and captures functional/non-functional requirements,
+This document captures the functional requirements, non-functional requirements,
success criteria, and constraints for the current scope.
+
+It is owned by the product role and should be updated whenever scope,
+acceptance criteria, or constraints change.
diff --git a/src/vstack/__main__.py b/src/vstack/__main__.py
index eee734b..c5d2d26 100644
--- a/src/vstack/__main__.py
+++ b/src/vstack/__main__.py
@@ -1,4 +1,4 @@
-"""Utilities and tests for main."""
+"""Module entrypoint for ``python -m vstack``."""
from vstack.main import main
diff --git a/src/vstack/_templates/instructions/git/config.yaml b/src/vstack/_templates/instructions/git/config.yaml
new file mode 100644
index 0000000..eac2055
--- /dev/null
+++ b/src/vstack/_templates/instructions/git/config.yaml
@@ -0,0 +1,4 @@
+name: git
+description: Git and release hygiene conventions. Use when creating commits, branches, or release-related changes.
+applyTo: "**/*"
+version: 0.1.0
diff --git a/src/vstack/_templates/instructions/git/template.md b/src/vstack/_templates/instructions/git/template.md
new file mode 100644
index 0000000..c821d7b
--- /dev/null
+++ b/src/vstack/_templates/instructions/git/template.md
@@ -0,0 +1,36 @@
+Use these Git and release hygiene conventions in this project.
+
+## Branch naming
+
+1. Use `type/short-description` branch names.
+1. Keep branch names lowercase and use hyphens to separate words.
+1. Use one of these allowed branch types when branch validation is enabled:
+ `feature`, `bugfix`, `hotfix`, `release`, `chore`, `feat`, `fix`, `docs`,
+ `refactor`, `perf`, `test`, `ci`, `build`, `style`, `opt`, `patch`, `dependabot`.
+
+## Commit messages
+
+1. Use Conventional Commits: `type(optional-scope)!: short summary`.
+1. Keep commit subjects clear, imperative, and within repository limits.
+1. Keep the commit subject at 100 characters or fewer when commit policy CI enforces this limit.
+1. Include `!` or a `BREAKING CHANGE:` footer when behavior changes are breaking.
+1. Keep commit type and scope aligned with repository policy.
+
+## SemVer alignment
+
+1. Treat commit messages as release inputs when the repository uses semantic version automation.
+1. Ensure major, minor, and patch intent is reflected in the commit type and breaking markers.
+1. Do not merge release-impacting changes with ambiguous commit messages.
+
+## Security and credentials
+
+1. Never ask users to paste passphrases, tokens, API keys, or private keys into chat.
+1. Never echo or log secrets from terminal prompts, command output, or environment variables.
+1. Never place credentials in commit messages, source files, workflow files, or documentation.
+1. Prefer existing secure authentication flows (for example SSH agent, OS keychain, `gh auth`).
+
+## Safe Git operations
+
+1. Avoid force pushes and destructive history rewrites unless explicitly requested and approved.
+1. Keep commits focused and reviewable.
+1. Prefer local verification before pushing release-impacting changes.
diff --git a/src/vstack/_templates/instructions/python/config.yaml b/src/vstack/_templates/instructions/python/config.yaml
index d50d4fa..3d978dd 100644
--- a/src/vstack/_templates/instructions/python/config.yaml
+++ b/src/vstack/_templates/instructions/python/config.yaml
@@ -1,4 +1,4 @@
name: python
-description: Python coding conventions for this repository. Use when writing or reviewing Python modules, tests, CLI code, and package internals.
+description: Python coding conventions for projects. Use when writing or reviewing Python modules, tests, CLI code, and package internals.
applyTo: "**/*.py"
-version: 0.1.0
+version: 0.1.1
diff --git a/src/vstack/_templates/instructions/python/template.md b/src/vstack/_templates/instructions/python/template.md
index 90fe002..bf6f27b 100644
--- a/src/vstack/_templates/instructions/python/template.md
+++ b/src/vstack/_templates/instructions/python/template.md
@@ -1,4 +1,4 @@
-Use these Python conventions in this repository.
+Use these Python conventions in this project.
## Design and readability
diff --git a/src/vstack/_templates/skills/_partials/skill-context.md b/src/vstack/_templates/skills/_partials/skill-context.md
index 2f98362..23484c1 100644
--- a/src/vstack/_templates/skills/_partials/skill-context.md
+++ b/src/vstack/_templates/skills/_partials/skill-context.md
@@ -11,3 +11,11 @@ When you need clarification, use this exact format — never invent or guess:
> **Default if no response:** [What you'll do]
Never ask more than one question at a time without waiting for the answer.
+
+### Diagram Convention
+
+When producing hand-authored Markdown outputs, prefer Mermaid for flow,
+interaction, lifecycle, state, topology, dependency, and decision diagrams when
+the format is supported and improves clarity. Use ASCII as a fallback when
+Mermaid is unsupported or would be less readable. Keep ASCII/text trees for
+directory structures and other scan-friendly hierarchies.
diff --git a/src/vstack/_templates/skills/adr/config.yaml b/src/vstack/_templates/skills/adr/config.yaml
index 54644dd..8ef8bbc 100644
--- a/src/vstack/_templates/skills/adr/config.yaml
+++ b/src/vstack/_templates/skills/adr/config.yaml
@@ -1,5 +1,5 @@
name: adr
-version: 1.0.1
+version: 1.0.2
description: |
Architecture Decision Record writing. Documents a significant architectural
decision with context, alternatives considered, rationale, and impact.
diff --git a/src/vstack/_templates/skills/analyse/config.yaml b/src/vstack/_templates/skills/analyse/config.yaml
index b1bedca..143daa1 100644
--- a/src/vstack/_templates/skills/analyse/config.yaml
+++ b/src/vstack/_templates/skills/analyse/config.yaml
@@ -1,5 +1,5 @@
name: analyse
-version: 1.0.1
+version: 1.0.2
description: |
Cross-cutting technical analysis. Investigates impact, tradeoffs, root causes,
or feasibility without implementing changes. Use when asked to "analyse this",
diff --git a/src/vstack/_templates/skills/architecture/config.yaml b/src/vstack/_templates/skills/architecture/config.yaml
index 233d61b..6bb94b9 100644
--- a/src/vstack/_templates/skills/architecture/config.yaml
+++ b/src/vstack/_templates/skills/architecture/config.yaml
@@ -1,5 +1,5 @@
name: architecture
-version: 1.0.1
+version: 1.0.2
description: |
Engineering-lead plan review. Lock in the execution plan —
service boundaries, data models, API contracts, error handling, test strategy,
diff --git a/src/vstack/_templates/skills/architecture/template.md b/src/vstack/_templates/skills/architecture/template.md
index 05074c5..99eb685 100644
--- a/src/vstack/_templates/skills/architecture/template.md
+++ b/src/vstack/_templates/skills/architecture/template.md
@@ -42,7 +42,7 @@ If running low on context or asked to compress: Step 0 > Service/contract diagra
- Observability is not optional — new codepaths need logs, metrics, or traces.
- Security is not optional — new codepaths need threat modeling.
- Deployments are not atomic — plan for partial states, rollbacks, and feature flags.
-- ASCII diagrams for all data flows, state machines, service dependencies.
+- Diagrams for all data flows, state machines, and service dependencies.
## Cognitive Patterns — How Great Engineering Leads Think
@@ -91,18 +91,13 @@ Before reviewing anything, answer:
## Step 1: Service Boundary & Data Model Review
-Produce an ASCII diagram of the service topology:
+Produce a service topology diagram. Prefer Mermaid when possible; use ASCII only
+as a fallback when Mermaid would be less clear or unsupported.
-```text
-┌───────────────┐ HTTP/gRPC ┌───────────────┐
-│ Service A │ ─────────────────────▶│ Service B │
-│ (this plan) │ │ (existing) │
-└───────┬───────┘ └───────────────┘
- │ publishes
- ▼
-┌───────────────┐
-│ Event Bus │
-└───────────────┘
+```mermaid
+flowchart LR
+ A[Service A this plan] -->|HTTP or gRPC| B[Service B existing]
+ A -->|publishes| C[Event Bus]
```
Review:
diff --git a/src/vstack/_templates/skills/cicd/config.yaml b/src/vstack/_templates/skills/cicd/config.yaml
index bbab73d..9c912ba 100644
--- a/src/vstack/_templates/skills/cicd/config.yaml
+++ b/src/vstack/_templates/skills/cicd/config.yaml
@@ -1,5 +1,5 @@
name: cicd
-version: 1.0.1
+version: 1.0.2
description: |
Write GitHub Actions CI/CD workflow configuration. Covers build, test, lint,
security scan, container publish, and deployment trigger workflows.
diff --git a/src/vstack/_templates/skills/code-review/config.yaml b/src/vstack/_templates/skills/code-review/config.yaml
index a8f2c8c..7d07828 100644
--- a/src/vstack/_templates/skills/code-review/config.yaml
+++ b/src/vstack/_templates/skills/code-review/config.yaml
@@ -1,5 +1,5 @@
name: code-review
-version: 1.0.1
+version: 1.0.2
description: |
Pre-landing code review. Finds bugs that pass CI but break in production —
race conditions, missing error handling, API contract violations, observability
diff --git a/src/vstack/_templates/skills/concise/config.yaml b/src/vstack/_templates/skills/concise/config.yaml
index fd1a6b8..b61c112 100644
--- a/src/vstack/_templates/skills/concise/config.yaml
+++ b/src/vstack/_templates/skills/concise/config.yaml
@@ -1,5 +1,5 @@
name: concise
-version: 1.0.0
+version: 1.0.1
description: |
Runtime response-style controller for concise communication. Switches between
normal, compact, and ultra output density without regenerating agents. Use
diff --git a/src/vstack/_templates/skills/consult/config.yaml b/src/vstack/_templates/skills/consult/config.yaml
index 5b78fe8..a691132 100644
--- a/src/vstack/_templates/skills/consult/config.yaml
+++ b/src/vstack/_templates/skills/consult/config.yaml
@@ -1,5 +1,5 @@
name: consult
-version: 1.0.1
+version: 1.0.2
description: |
DX triage and focused review. First classifies whether the request is API DX,
CLI/tool DX, or developer workflow DX, then runs exactly one review path with
diff --git a/src/vstack/_templates/skills/container/config.yaml b/src/vstack/_templates/skills/container/config.yaml
index 2def729..7668eae 100644
--- a/src/vstack/_templates/skills/container/config.yaml
+++ b/src/vstack/_templates/skills/container/config.yaml
@@ -1,5 +1,5 @@
name: container
-version: 1.0.1
+version: 1.0.2
description: |
Write and review Dockerfile, docker-compose, and container configuration.
Covers multi-stage builds, image hardening, non-root users, minimal base images,
diff --git a/src/vstack/_templates/skills/debug/config.yaml b/src/vstack/_templates/skills/debug/config.yaml
index d95b682..cc16386 100644
--- a/src/vstack/_templates/skills/debug/config.yaml
+++ b/src/vstack/_templates/skills/debug/config.yaml
@@ -1,5 +1,5 @@
name: debug
-version: 1.0.1
+version: 1.0.2
description: |
Systematic root-cause debugging for backend services, APIs, and libraries.
No fixes without investigation. Follows the scientific method: observe →
diff --git a/src/vstack/_templates/skills/dependency/config.yaml b/src/vstack/_templates/skills/dependency/config.yaml
index df0a991..1567c76 100644
--- a/src/vstack/_templates/skills/dependency/config.yaml
+++ b/src/vstack/_templates/skills/dependency/config.yaml
@@ -1,5 +1,5 @@
name: dependency
-version: 1.0.0
+version: 1.0.1
description: |
Dependency health audit. Covers vulnerability scanning, outdated packages,
licence compliance, transitive risk, pinning policy, and supply chain hygiene.
diff --git a/src/vstack/_templates/skills/design/config.yaml b/src/vstack/_templates/skills/design/config.yaml
index 562d863..3675d3e 100644
--- a/src/vstack/_templates/skills/design/config.yaml
+++ b/src/vstack/_templates/skills/design/config.yaml
@@ -1,5 +1,5 @@
name: design
-version: 1.0.1
+version: 1.0.2
description: |
Build a complete API design or service design from scratch. Produces OpenAPI
specs, error conventions, naming standards, pagination patterns, and versioning
diff --git a/src/vstack/_templates/skills/design/template.md b/src/vstack/_templates/skills/design/template.md
index 6a2003a..28ee24e 100644
--- a/src/vstack/_templates/skills/design/template.md
+++ b/src/vstack/_templates/skills/design/template.md
@@ -53,15 +53,22 @@ For each resource/entity in the domain:
1. **Lifecycle:** What states can it be in?
1. **Relationships:** What resources does it reference?
-Produce an entity diagram:
-
-```text
-┌──────────┐ 1:N ┌──────────────┐
-│ User │ ──────────▶ │ Order │
-│ id │ │ id │
-│ email │ │ user_id (fk) │
-│ name │ │ status │
-└──────────┘ └──────────────┘
+Produce an entity diagram. Prefer Mermaid when possible; use ASCII as a fallback
+only when Mermaid support is unavailable or would reduce clarity.
+
+```mermaid
+erDiagram
+ USER ||--o{ ORDER : places
+ USER {
+ string id
+ string email
+ string name
+ }
+ ORDER {
+ string id
+ string user_id
+ string status
+ }
```
______________________________________________________________________
diff --git a/src/vstack/_templates/skills/docs/config.yaml b/src/vstack/_templates/skills/docs/config.yaml
index 1ec26d9..057f556 100644
--- a/src/vstack/_templates/skills/docs/config.yaml
+++ b/src/vstack/_templates/skills/docs/config.yaml
@@ -1,5 +1,5 @@
name: docs
-version: 1.0.1
+version: 1.0.2
description: |
Post-release documentation alignment. Updates README, API docs, migration
guides, and related docs to match shipped behavior. Does not own release-note
diff --git a/src/vstack/_templates/skills/explore/config.yaml b/src/vstack/_templates/skills/explore/config.yaml
index 09abef1..7347986 100644
--- a/src/vstack/_templates/skills/explore/config.yaml
+++ b/src/vstack/_templates/skills/explore/config.yaml
@@ -1,5 +1,5 @@
name: explore
-version: 1.0.1
+version: 1.0.2
description: |
Repository and system discovery. Maps the architecture, understands the
codebase, identifies technical debt, and produces a structured onboarding
diff --git a/src/vstack/_templates/skills/explore/template.md b/src/vstack/_templates/skills/explore/template.md
index ad01823..4a4b634 100644
--- a/src/vstack/_templates/skills/explore/template.md
+++ b/src/vstack/_templates/skills/explore/template.md
@@ -144,46 +144,45 @@ Type: [API service / library / CLI / worker / ...]
Stack: [language, framework, runtime versions]
### Architecture
-[ASCII diagram of service topology or module structure]
-
-┌─────────────────┐ HTTP ┌──────────────┐
-│ [This service] │ ──────────▶ │ [Dep A] │
-└────────┬────────┘ └──────────────┘
- │ subscribes
- ▼
-┌─────────────────┐
-│ [Message Bus] │
-└─────────────────┘
+[Mermaid diagram of service topology or module structure when possible; ASCII fallback if needed]
### Data Stores
+
- [Database]: [what it stores, ORM/driver used]
- [Cache]: [what is cached, TTL strategy]
### Key External Dependencies
+
- [Service A]: [purpose, auth method]
- [Service B]: [purpose]
### API Surface
+
[Summarize endpoints or exported functions]
### Test Coverage
+
- Unit tests: [Y/N, count, coverage %]
- Integration tests: [Y/N]
- Contract tests: [Y/N]
### CI/CD
+
- CI: [GitHub Actions/GitLab CI/etc]
- Deploy target: [Fly.io/Render/K8s/etc]
- Release process: [manual/automated]
### Technical Debt
+
- [Key items from TODOS.md or code comments]
### Onboarding Notes
+
- How to run locally: [command]
- How to run tests: [command]
- Key config: [env vars]
- Gotchas: [anything that surprised me]
+
```
______________________________________________________________________
diff --git a/src/vstack/_templates/skills/guardrails/config.yaml b/src/vstack/_templates/skills/guardrails/config.yaml
index 1734bc7..4863239 100644
--- a/src/vstack/_templates/skills/guardrails/config.yaml
+++ b/src/vstack/_templates/skills/guardrails/config.yaml
@@ -1,5 +1,5 @@
name: guardrails
-version: 1.0.1
+version: 1.0.2
description: |
Activate safety guardrails for the current session. Before any destructive
command (rm -rf, DROP TABLE, git push --force, git reset --hard, kubectl delete,
diff --git a/src/vstack/_templates/skills/incident/config.yaml b/src/vstack/_templates/skills/incident/config.yaml
index 7bba5cc..d474936 100644
--- a/src/vstack/_templates/skills/incident/config.yaml
+++ b/src/vstack/_templates/skills/incident/config.yaml
@@ -1,5 +1,5 @@
name: incident
-version: 1.0.0
+version: 1.0.1
description: |
Incident analysis and post-mortem writing. Guides a structured investigation
from timeline reconstruction through root cause identification to a blameless
diff --git a/src/vstack/_templates/skills/inspect/config.yaml b/src/vstack/_templates/skills/inspect/config.yaml
index f0ed115..0df4f74 100644
--- a/src/vstack/_templates/skills/inspect/config.yaml
+++ b/src/vstack/_templates/skills/inspect/config.yaml
@@ -1,5 +1,5 @@
name: inspect
-version: 1.0.1
+version: 1.0.2
description: |
Read-only verification audit. Runs baseline plus optional extended checks,
produces severity-ranked findings, and makes no code or commit changes.
diff --git a/src/vstack/_templates/skills/migrate/config.yaml b/src/vstack/_templates/skills/migrate/config.yaml
index 04b989d..cb8520f 100644
--- a/src/vstack/_templates/skills/migrate/config.yaml
+++ b/src/vstack/_templates/skills/migrate/config.yaml
@@ -1,5 +1,5 @@
name: migrate
-version: 1.0.0
+version: 1.0.1
description: |
Database migration review and authoring. Covers forwards/backwards compatibility,
zero-downtime strategies, rollback plans, data integrity, and index safety.
diff --git a/src/vstack/_templates/skills/onboard/config.yaml b/src/vstack/_templates/skills/onboard/config.yaml
index 92eedaf..ed27f35 100644
--- a/src/vstack/_templates/skills/onboard/config.yaml
+++ b/src/vstack/_templates/skills/onboard/config.yaml
@@ -1,5 +1,5 @@
name: onboard
-version: 1.0.0
+version: 1.0.1
description: |
Generate a contributor onboarding guide for a repository. Covers project
purpose, architecture overview, local dev setup, test commands, contribution
diff --git a/src/vstack/_templates/skills/openapi/config.yaml b/src/vstack/_templates/skills/openapi/config.yaml
index 324d922..da04795 100644
--- a/src/vstack/_templates/skills/openapi/config.yaml
+++ b/src/vstack/_templates/skills/openapi/config.yaml
@@ -1,5 +1,5 @@
name: openapi
-version: 1.0.0
+version: 1.0.1
description: |
Write and review OpenAPI 3.1 specifications. Covers resource naming, HTTP
method semantics, status codes, error conventions, pagination, versioning,
diff --git a/src/vstack/_templates/skills/performance/config.yaml b/src/vstack/_templates/skills/performance/config.yaml
index 62453c2..cbffd6a 100644
--- a/src/vstack/_templates/skills/performance/config.yaml
+++ b/src/vstack/_templates/skills/performance/config.yaml
@@ -1,5 +1,5 @@
name: performance
-version: 1.0.1
+version: 1.0.2
description: |
Performance profiling and regression detection. Establishes baselines, detects
regressions, profiles bottlenecks, and recommends optimizations. Use when asked
diff --git a/src/vstack/_templates/skills/pr/config.yaml b/src/vstack/_templates/skills/pr/config.yaml
index 9a43d1e..979a8bd 100644
--- a/src/vstack/_templates/skills/pr/config.yaml
+++ b/src/vstack/_templates/skills/pr/config.yaml
@@ -1,5 +1,5 @@
name: pr
-version: 1.0.1
+version: 1.0.2
description: |
Commit, push, and open a pull request from the current branch to main.
Uses the release notes from docs/releases/{date}.md as the PR body.
diff --git a/src/vstack/_templates/skills/refactor/config.yaml b/src/vstack/_templates/skills/refactor/config.yaml
index 6cce8a9..31b8e65 100644
--- a/src/vstack/_templates/skills/refactor/config.yaml
+++ b/src/vstack/_templates/skills/refactor/config.yaml
@@ -1,5 +1,5 @@
name: refactor
-version: 1.0.0
+version: 1.0.1
description: |
Structured refactoring for backend services, APIs, and libraries. Identifies
code smells, plans incremental changes, executes without altering observable
diff --git a/src/vstack/_templates/skills/release-notes/config.yaml b/src/vstack/_templates/skills/release-notes/config.yaml
index 92d3aa5..c11e891 100644
--- a/src/vstack/_templates/skills/release-notes/config.yaml
+++ b/src/vstack/_templates/skills/release-notes/config.yaml
@@ -1,5 +1,5 @@
name: release-notes
-version: 1.0.1
+version: 1.0.2
description: |
Prepare release artifacts: verify all docs are present, write release notes,
own CHANGELOG.md updates, and produce docs/releases/{date}.md.
diff --git a/src/vstack/_templates/skills/requirements/config.yaml b/src/vstack/_templates/skills/requirements/config.yaml
index 7b461bf..b119c74 100644
--- a/src/vstack/_templates/skills/requirements/config.yaml
+++ b/src/vstack/_templates/skills/requirements/config.yaml
@@ -1,5 +1,5 @@
name: requirements
-version: 1.0.1
+version: 1.0.2
description: |
Collaborative requirements gathering and documentation. Clarifies what must be
built, defines success criteria, constraints, and non-functional requirements.
diff --git a/src/vstack/_templates/skills/security/config.yaml b/src/vstack/_templates/skills/security/config.yaml
index 6c5efe4..ff63460 100644
--- a/src/vstack/_templates/skills/security/config.yaml
+++ b/src/vstack/_templates/skills/security/config.yaml
@@ -1,5 +1,5 @@
name: security
-version: 1.0.1
+version: 1.0.2
description: |
OWASP Top 10 + STRIDE security audit for APIs, services, and libraries.
Finds authentication bypasses, injection vulnerabilities, insecure configurations,
diff --git a/src/vstack/_templates/skills/verify/config.yaml b/src/vstack/_templates/skills/verify/config.yaml
index adf6bb9..fefb73b 100644
--- a/src/vstack/_templates/skills/verify/config.yaml
+++ b/src/vstack/_templates/skills/verify/config.yaml
@@ -1,5 +1,5 @@
name: verify
-version: 1.0.1
+version: 1.0.2
description: |
Verification fix-loop skill. Routes by mode (quick/standard/exhaustive), runs
targeted checks, fixes findings by severity, and re-verifies impacted paths.
diff --git a/src/vstack/_templates/skills/vision/config.yaml b/src/vstack/_templates/skills/vision/config.yaml
index c58ccca..a4f4da4 100644
--- a/src/vstack/_templates/skills/vision/config.yaml
+++ b/src/vstack/_templates/skills/vision/config.yaml
@@ -1,5 +1,5 @@
name: vision
-version: 1.0.1
+version: 1.0.2
description: |
CEO/founder-mode plan review. Rethink the problem from first principles, validate
ambition and scope, challenge premises, find the 10x solution. Four modes:
diff --git a/src/vstack/_templates/skills/vision/template.md b/src/vstack/_templates/skills/vision/template.md
index 24b3582..3e3973b 100644
--- a/src/vstack/_templates/skills/vision/template.md
+++ b/src/vstack/_templates/skills/vision/template.md
@@ -41,7 +41,7 @@ Review the plan with maximum rigor and the appropriate level of ambition.
1. **Retry/backoff/circuit breaker coverage.** Every external call: what happens on retry? Exponential backoff with jitter? Circuit breaker state machine? Dead letter queue?
1. **Observability is scope, not afterthought.** New codepaths need: structured logs with correlation IDs, metrics (request rate, error rate, latency p50/p95/p99), distributed traces, dashboards, and alerts.
1. **API contracts are immutable once published.** Breaking changes require version bumps. Plan the migration strategy before coding.
-1. **Diagrams are mandatory.** ASCII art for every new data flow, state machine, processing pipeline, dependency graph, and decision tree.
+1. **Diagrams are mandatory.** Prefer Mermaid for every new data flow, interaction flow, state machine, processing pipeline, dependency graph, and decision tree. Use ASCII only as a fallback when Mermaid is unsupported or less clear.
1. **Everything deferred must be written down.** TODOS.md or it doesn't exist.
1. **Design for the 3am pager.** Systems over heroes. Every runbook, alert, and recovery procedure documented before go-live.
1. **Security is first-class scope.** Authentication, authorization, input validation, rate limiting, secret management — addressed in the plan, not "later."
diff --git a/src/vstack/agents/constants.py b/src/vstack/agents/constants.py
index fc8f8c7..e4bea29 100644
--- a/src/vstack/agents/constants.py
+++ b/src/vstack/agents/constants.py
@@ -1,4 +1,4 @@
-"""Utilities and tests for constants."""
+"""Constants for agent template discovery and output paths."""
from vstack.constants import TEMPLATES_ROOT
diff --git a/src/vstack/agents/generator.py b/src/vstack/agents/generator.py
index 63b3879..dc119ac 100644
--- a/src/vstack/agents/generator.py
+++ b/src/vstack/agents/generator.py
@@ -13,8 +13,8 @@
class AgentGenerator(GenericArtifactGenerator):
- """Generator for agent artifacts, pre-configured for :data:`~vstack.artifacts.type_config.AGENT_TYPE`."""
+ """Generate agent artifacts using the built-in agent type configuration."""
def __init__(self) -> None:
- """Initialize instance state."""
+ """Create an agent generator bound to the built-in template root."""
super().__init__(AGENT_TYPE, TEMPLATES_ROOT)
diff --git a/src/vstack/artifacts/__init__.py b/src/vstack/artifacts/__init__.py
index c480f50..3decf7a 100644
--- a/src/vstack/artifacts/__init__.py
+++ b/src/vstack/artifacts/__init__.py
@@ -14,4 +14,4 @@
from vstack.frontmatter import FrontmatterContent as FrontmatterContent
from vstack.frontmatter import FrontmatterParser as FrontmatterParser
from vstack.frontmatter import FrontmatterSchema as FrontmatterSchema
-from vstack.frontmatter import build_output as build_output
+from vstack.frontmatter import FrontmatterSerializer as FrontmatterSerializer
diff --git a/src/vstack/artifacts/config.py b/src/vstack/artifacts/config.py
index 6cb4d36..3396501 100644
--- a/src/vstack/artifacts/config.py
+++ b/src/vstack/artifacts/config.py
@@ -82,7 +82,7 @@ class ArtifactTypeConfig:
manifest_key: str = ""
def __post_init__(self) -> None:
- """Internal helper to post init."""
+ """Default ``manifest_key`` to ``output_subdir`` when not explicitly set."""
if not self.manifest_key:
self.manifest_key = self.output_subdir
diff --git a/src/vstack/artifacts/generator.py b/src/vstack/artifacts/generator.py
index 04817fe..ce81ce0 100644
--- a/src/vstack/artifacts/generator.py
+++ b/src/vstack/artifacts/generator.py
@@ -11,6 +11,7 @@
from __future__ import annotations
+import json
import re
import shutil
from pathlib import Path
@@ -18,10 +19,12 @@
from vstack.artifacts.config import ArtifactTypeConfig
from vstack.artifacts.constants import AUTO_GEN_FOOTER
from vstack.artifacts.models import ArtifactResult, RenderedArtifact
-from vstack.frontmatter import FrontmatterParser, build_output
+from vstack.constants import VERSION
+from vstack.frontmatter import FrontmatterParser, FrontmatterSerializer
from vstack.models import CheckMessage, ValidationResult
_PLACEHOLDER_RE = re.compile(r"\{\{([A-Z_]+)\}\}")
+_META_COMMENT_RE = re.compile(r"")
class GenericArtifactGenerator:
@@ -59,6 +62,32 @@ def find_unresolved(text: str) -> list[str]:
"""Return a list of TOKEN names that remain unresolved in *text*."""
return _PLACEHOLDER_RE.findall(text)
+ @staticmethod
+ def parse_generation_metadata(text: str) -> dict[str, str] | None:
+ """Parse the ``VSTACK-META`` footer JSON, if present."""
+ matches = _META_COMMENT_RE.findall(text)
+ if not matches:
+ return None
+ try:
+ data = json.loads(matches[-1])
+ except json.JSONDecodeError:
+ return None
+ if not isinstance(data, dict):
+ return None
+ return {str(k): str(v) for k, v in data.items()}
+
+ def _build_footer(self, artifact_name: str, artifact_version: str) -> str:
+ """Build the AUTO-GENERATED footer plus machine-readable metadata."""
+ meta = {
+ "generator": "vstack",
+ "vstack_version": VERSION,
+ "artifact_type": self.config.type_name,
+ "artifact_name": artifact_name,
+ "artifact_version": artifact_version,
+ }
+ meta_json = json.dumps(meta, separators=(",", ":"), sort_keys=True)
+ return f"{AUTO_GEN_FOOTER}\n"
+
# ── Partials ──────────────────────────────────────────────────────────────
def load_partials(self) -> dict[str, str]:
@@ -149,7 +178,7 @@ def render(self, tmpl_dir: Path) -> RenderedArtifact:
raise ValueError(
f"{self.config.type_name}: frontmatter_schema must be set when add_frontmatter=True"
)
- fm_str = build_output(
+ fm_str = FrontmatterSerializer().serialize(
meta,
schema,
preserve_multiline=self.config.preserve_multiline_frontmatter,
@@ -164,7 +193,7 @@ def render(self, tmpl_dir: Path) -> RenderedArtifact:
raise ValueError(
f"{self.config.type_name}: frontmatter_schema must be set when add_frontmatter=True"
)
- fm_str = build_output(
+ fm_str = FrontmatterSerializer().serialize(
meta,
schema,
preserve_multiline=self.config.preserve_multiline_frontmatter,
@@ -174,7 +203,8 @@ def render(self, tmpl_dir: Path) -> RenderedArtifact:
fm_str = ""
body_str = resolved
- footer = AUTO_GEN_FOOTER if self.config.auto_gen_footer else ""
+ artifact_version = str((meta or {}).get("version") or VERSION)
+ footer = self._build_footer(name, artifact_version) if self.config.auto_gen_footer else ""
body_content = body_str.lstrip("\n")
output = (fm_str + body_content + footer) if (fm_str or footer) else body_str
@@ -362,6 +392,10 @@ def fail(msg: str) -> None:
ok(f"{label}: has AUTO-GENERATED footer")
else:
fail(f"{label}: missing AUTO-GENERATED footer")
+ if self.parse_generation_metadata(content) is not None:
+ ok(f"{label}: has VSTACK-META footer")
+ else:
+ ok(f"{label}: missing VSTACK-META footer (legacy artifact accepted)")
if self.config.fail_on_unresolved:
unresolved = self.find_unresolved(content)
if not unresolved:
diff --git a/src/vstack/cli/commands.py b/src/vstack/cli/commands.py
index 4f3ba88..b052855 100644
--- a/src/vstack/cli/commands.py
+++ b/src/vstack/cli/commands.py
@@ -103,6 +103,215 @@ def _gen_for(self, type_name: str) -> GenericArtifactGenerator | None:
"""Return the generator for *type_name*, or ``None`` when unknown."""
return next((g for g in self._generators if g.config.type_name == type_name), None)
+ def _expected_output_names(
+ self,
+ gen: GenericArtifactGenerator,
+ manifest_data: Manifest | None,
+ ) -> list[str] | None:
+ """Resolve expected output names for verify output checks."""
+ if manifest_data:
+ return manifest_data.names_for(gen.config.manifest_key)
+ return _EXPECTED_INPUT_NAMES.get(gen.config.type_name)
+
+ def _expected_manifest_metadata(
+ self,
+ gen: GenericArtifactGenerator,
+ manifest_data: Manifest,
+ entry: ArtifactEntry,
+ ) -> dict[str, str]:
+ """Build expected metadata values for one manifest-tracked artifact."""
+ expected_meta = {
+ "generator": "vstack",
+ "vstack_version": manifest_data.vstack_version,
+ "artifact_type": gen.config.type_name,
+ "artifact_name": entry.name,
+ }
+ if entry.version is not None:
+ expected_meta["artifact_version"] = entry.version
+ return expected_meta
+
+ def _verify_manifest_metadata_entry(
+ self,
+ gen: GenericArtifactGenerator,
+ manifest_data: Manifest,
+ entry: ArtifactEntry,
+ artifact_path: Path,
+ ) -> ValidationResult:
+ """Verify manifest-linked metadata for a single artifact file."""
+ result = ValidationResult()
+ content = artifact_path.read_text(encoding="utf-8")
+ metadata = GenericArtifactGenerator.parse_generation_metadata(content)
+ rel_path = self._label(artifact_path)
+
+ if metadata is None:
+ if "AUTO-GENERATED" not in content:
+ result.messages.append(
+ CheckMessage(
+ "fail",
+ f"{rel_path}: missing VSTACK-META and missing AUTO-GENERATED footer",
+ )
+ )
+ return result
+
+ result.messages.append(
+ CheckMessage(
+ "pass",
+ f"{rel_path}: missing VSTACK-META footer; "
+ "treating manifest entry as source of truth",
+ )
+ )
+ result.messages.append(
+ CheckMessage(
+ "pass",
+ f"{rel_path}: legacy artifact accepted from manifest tracking",
+ )
+ )
+ return result
+
+ for key, expected_value in self._expected_manifest_metadata(
+ gen, manifest_data, entry
+ ).items():
+ actual_value = metadata.get(key)
+ if actual_value == expected_value:
+ result.messages.append(CheckMessage("pass", f"{rel_path}: {key} matches manifest"))
+ else:
+ result.messages.append(
+ CheckMessage(
+ "fail",
+ f"{rel_path}: {key} mismatch "
+ f"(expected '{expected_value}', got '{actual_value}')",
+ )
+ )
+
+ return result
+
+ def _verify_manifest_metadata(
+ self,
+ gen: GenericArtifactGenerator,
+ manifest_data: Manifest,
+ install_dir: Path,
+ ) -> ValidationResult | None:
+ """Verify footer metadata for all manifest-tracked artifacts of one type."""
+ manifest_entries = manifest_data.entries_for(gen.config.manifest_key)
+ if not manifest_entries:
+ return None
+
+ result = ValidationResult()
+ for entry in manifest_entries:
+ artifact_path = install_dir / entry.file
+ if not artifact_path.exists():
+ continue
+ entry_result = self._verify_manifest_metadata_entry(
+ gen,
+ manifest_data,
+ entry,
+ artifact_path,
+ )
+ result.messages.extend(entry_result.messages)
+
+ return result if result.messages else None
+
+ def _existing_versions_for_install(
+ self,
+ gens: list[GenericArtifactGenerator],
+ existing_manifest: Manifest | None,
+ ) -> dict[str, str | None]:
+ """Build existing-version lookup for selected generators."""
+ if existing_manifest is None:
+ return {}
+
+ existing_versions: dict[str, str | None] = {}
+ for gen in gens:
+ for entry in existing_manifest.entries_for(gen.config.manifest_key):
+ key = f"{gen.config.type_name}/{entry.name}"
+ existing_versions[key] = entry.version
+ return existing_versions
+
+ def _preserved_manifest_entries(
+ self,
+ existing_manifest: Manifest | None,
+ selected_manifest_keys: set[str],
+ ) -> dict[str, list[ArtifactEntry]]:
+ """Keep existing manifest entries for artifact families not in this run."""
+ if existing_manifest is None:
+ return {}
+
+ preserved: dict[str, list[ArtifactEntry]] = {}
+ for manifest_key, entries in existing_manifest.artifacts.items():
+ if manifest_key not in selected_manifest_keys:
+ preserved[manifest_key] = list(entries)
+ return preserved
+
+ def _install_action(
+ self,
+ *,
+ force: bool,
+ update: bool,
+ out_file: Path,
+ existing_version: str | None,
+ new_version: str,
+ ) -> str:
+ """Return install action: install, skip, or update."""
+ if force:
+ return "install"
+ if out_file.exists() and existing_version is not None:
+ if update:
+ return "update" if _version_gt(new_version, existing_version) else "skip"
+ return "skip"
+ return "install"
+
+ def _print_install_action(
+ self,
+ *,
+ colors: type[_Colors],
+ prefix: str,
+ rel: str,
+ action: str,
+ existing_version: str | None,
+ new_version: str,
+ out_file: Path,
+ force: bool,
+ ) -> None:
+ """Print install/update/skip line for one artifact."""
+ if action == "skip":
+ print(
+ f" {colors.YELLOW}↷{colors.RESET} {rel}"
+ f" {colors.DIM}skipped — already v{existing_version}{colors.RESET}"
+ )
+ return
+
+ if action == "update":
+ print(
+ f" {prefix}{colors.CYAN}↑{colors.RESET} "
+ f"{colors.BOLD}{rel}{colors.RESET}"
+ f" v{existing_version} → {colors.GREEN}v{new_version}{colors.RESET}"
+ )
+ return
+
+ tag = "(forced) " if force and out_file.exists() else ""
+ print(
+ f" {prefix}{colors.GREEN}✓{colors.RESET} "
+ f"{colors.BOLD}{rel}{colors.RESET}"
+ f" {colors.DIM}{tag}{colors.RESET}{colors.GREEN}v{new_version}{colors.RESET}"
+ )
+
+ def _record_manifest_entry(
+ self,
+ *,
+ new_entries: dict[str, list[ArtifactEntry]],
+ gen: GenericArtifactGenerator,
+ artifact_name: str,
+ version: str,
+ ) -> None:
+ """Append one installed artifact entry to the in-memory manifest payload."""
+ new_entries.setdefault(gen.config.manifest_key, []).append(
+ ArtifactEntry(
+ name=artifact_name,
+ file=gen.install_relative_path(artifact_name),
+ version=version,
+ )
+ )
+
# ── validate ──────────────────────────────────────────────────────────────
def validate(self, only: list[str] | None = None) -> int:
@@ -167,16 +376,12 @@ def install(
# Read existing manifest once — used to look up installed versions.
mf = self._manifest(install_dir)
existing_manifest = mf.read()
- existing_versions: dict[str, str | None] = {}
- if existing_manifest:
- for gen in gens:
- for entry in existing_manifest.entries_for(gen.config.manifest_key):
- key = f"{gen.config.type_name}/{entry.name}"
- existing_versions[key] = entry.version
+ selected_manifest_keys = {gen.config.manifest_key for gen in gens}
+ existing_versions = self._existing_versions_for_install(gens, existing_manifest)
prefix = f"{_C.DIM}[dry-run]{_C.RESET} " if dry_run else ""
all_ok = True
- new_entries: dict[str, list[ArtifactEntry]] = {}
+ new_entries = self._preserved_manifest_entries(existing_manifest, selected_manifest_keys)
for gen in gens:
out_dir = install_dir / gen.config.output_subdir
@@ -196,48 +401,42 @@ def install(
file=sys.stderr,
)
- # Decide action.
- action: str # "install" | "skip" | "update"
- if force:
- action = "install"
- elif out_file.exists() and existing_version is not None:
- if update:
- action = "update" if _version_gt(new_version, existing_version) else "skip"
- else:
- action = "skip"
- else:
- action = "install"
+ action = self._install_action(
+ force=force,
+ update=update,
+ out_file=out_file,
+ existing_version=existing_version,
+ new_version=new_version,
+ )
- if action == "skip":
- print(
- f" {_C.YELLOW}↷{_C.RESET} {rel}"
- f" {_C.DIM}skipped — already v{existing_version}{_C.RESET}"
- )
- elif action == "update":
- print(
- f" {prefix}{_C.CYAN}↑{_C.RESET} "
- f"{_C.BOLD}{rel}{_C.RESET}"
- f" v{existing_version} → {_C.GREEN}v{new_version}{_C.RESET}"
- )
- else:
- tag = "(forced) " if force and out_file.exists() else ""
- print(
- f" {prefix}{_C.GREEN}✓{_C.RESET} "
- f"{_C.BOLD}{rel}{_C.RESET}"
- f" {_C.DIM}{tag}{_C.RESET}{_C.GREEN}v{new_version}{_C.RESET}"
+ if action == "skip" and out_file.exists():
+ existing_meta = GenericArtifactGenerator.parse_generation_metadata(
+ out_file.read_text(encoding="utf-8")
)
+ if existing_meta is not None and existing_meta.get("vstack_version") != VERSION:
+ action = "install"
+
+ self._print_install_action(
+ colors=_C,
+ prefix=prefix,
+ rel=rel,
+ action=action,
+ existing_version=existing_version,
+ new_version=new_version,
+ out_file=out_file,
+ force=force,
+ )
if not dry_run and action != "skip":
out_dir.mkdir(parents=True, exist_ok=True)
out_file.parent.mkdir(parents=True, exist_ok=True)
out_file.write_text(artifact.content, encoding="utf-8")
- new_entries.setdefault(gen.config.manifest_key, []).append(
- ArtifactEntry(
- name=artifact.name,
- file=gen.install_relative_path(artifact.name),
- version=new_version,
- )
+ self._record_manifest_entry(
+ new_entries=new_entries,
+ gen=gen,
+ artifact_name=artifact.name,
+ version=new_version,
)
# Verify source for unresolvable issues.
@@ -327,13 +526,16 @@ def _print_result(result: ValidationResult) -> None:
)
)
else:
- expected = (
- manifest_data.names_for(gen.config.type_name)
- if manifest_data
- else _EXPECTED_INPUT_NAMES.get(gen.config.type_name)
- )
+ expected = self._expected_output_names(gen, manifest_data)
_print_result(gen.verify_output(out_dir, expected))
+ if manifest_data:
+ metadata_result = self._verify_manifest_metadata(
+ gen, manifest_data, install_dir
+ )
+ if metadata_result:
+ _print_result(metadata_result)
+
print()
total_failures = sum(r.failures for r in results)
if total_failures:
diff --git a/src/vstack/cli/constants.py b/src/vstack/cli/constants.py
index b7732a5..ae10933 100644
--- a/src/vstack/cli/constants.py
+++ b/src/vstack/cli/constants.py
@@ -1,4 +1,4 @@
-"""Utilities and tests for constants."""
+"""Canonical CLI artifact names."""
from __future__ import annotations
diff --git a/src/vstack/cli/manifest.py b/src/vstack/cli/manifest.py
index 88e6039..7368e32 100644
--- a/src/vstack/cli/manifest.py
+++ b/src/vstack/cli/manifest.py
@@ -11,7 +11,9 @@
"installed_at": "…",
"artifacts": {
"skills": [{"name": "vision", "version": "1.0.1", "file": "skills/vision/SKILL.md"}],
- "agents": [{"name": "engineer", "file": "agents/engineer.agent.md"}]
+ "agents": [{"name": "engineer", "file": "agents/engineer.agent.md"}],
+ "instructions": [{"name": "python", "file": "instructions/python.instructions.md"}],
+ "prompts": [{"name": "code-review", "file": "prompts/code-review.prompt.md"}]
}
}
"""
@@ -27,7 +29,7 @@
@dataclass
class ArtifactEntry:
- """A single artifact tracked in the manifest."""
+ """Represent a single installed artifact entry in ``vstack.json``."""
name: str
file: str
@@ -36,7 +38,7 @@ class ArtifactEntry:
@dataclass
class Manifest:
- """In-memory representation of ``vstack.json``."""
+ """Represent the parsed install manifest stored in ``vstack.json``."""
vstack_version: str
installed_at: str
@@ -45,21 +47,37 @@ class Manifest:
# ── Accessors ─────────────────────────────────────────────────────────────
def entries_for(self, type_name: str) -> list[ArtifactEntry]:
- """Entries for."""
+ """Return manifest entries for a single artifact type key.
+
+ Args:
+ type_name: Manifest artifact key such as ``"skills"``.
+ """
return self.artifacts.get(type_name, [])
def names_for(self, type_name: str) -> list[str]:
- """Names for."""
+ """Return artifact names for a single manifest type key.
+
+ Args:
+ type_name: Manifest artifact key such as ``"skills"``.
+ """
return [e.name for e in self.entries_for(type_name)]
def files_for(self, type_name: str) -> list[str]:
- """Files for."""
+ """Return relative output file paths for a manifest type key.
+
+ Args:
+ type_name: Manifest artifact key such as ``"skills"``.
+ """
return [e.file for e in self.entries_for(type_name)]
# ── Serialisation ─────────────────────────────────────────────────────────
def to_dict(self) -> dict:
- """To dict."""
+ """Serialize the manifest into JSON-compatible primitives.
+
+ Returns:
+ A nested dictionary structure suitable for ``json.dumps``.
+ """
return {
"vstack_version": self.vstack_version,
"installed_at": self.installed_at,
@@ -78,7 +96,14 @@ def to_dict(self) -> dict:
@classmethod
def from_dict(cls, data: dict) -> Manifest:
- """From dict."""
+ """Create a :class:`Manifest` from parsed JSON data.
+
+ Args:
+ data: Parsed JSON object read from ``vstack.json``.
+
+ Returns:
+ A normalized in-memory manifest representation.
+ """
artifacts: dict[str, list[ArtifactEntry]] = {}
for type_name, entries in data.get("artifacts", {}).items():
artifacts[type_name] = [
@@ -98,18 +123,28 @@ def from_dict(cls, data: dict) -> Manifest:
class ManifestFile:
- """Reads and writes the ``vstack.json`` manifest inside *parent_dir*."""
+ """Read and write the ``vstack.json`` manifest inside an install root."""
def __init__(self, parent_dir: Path) -> None:
- """Initialize instance state."""
+ """Create a manifest accessor rooted at the provided install directory.
+
+ Args:
+ parent_dir: Install root that contains or will contain
+ ``vstack.json``.
+ """
self.path = parent_dir / MANIFEST_FILENAME
def exists(self) -> bool:
- """Exists."""
+ """Return ``True`` when the manifest file exists on disk."""
return self.path.exists()
def read(self) -> Manifest | None:
- """Parse the manifest. Returns ``None`` when missing or corrupt."""
+ """Parse the manifest file from disk.
+
+ Returns:
+ The parsed manifest, or ``None`` when the file is missing or
+ cannot be decoded safely.
+ """
if not self.path.exists():
return None
try:
@@ -119,7 +154,11 @@ def read(self) -> Manifest | None:
return None
def write(self, manifest: Manifest) -> None:
- """Write."""
+ """Write a manifest to disk in stable, human-readable JSON format.
+
+ Args:
+ manifest: Manifest data to persist.
+ """
self.path.parent.mkdir(parents=True, exist_ok=True)
self.path.write_text(
json.dumps(manifest.to_dict(), indent=2, ensure_ascii=False) + "\n",
diff --git a/src/vstack/cli/parser.py b/src/vstack/cli/parser.py
index 67b2c61..8dc7739 100644
--- a/src/vstack/cli/parser.py
+++ b/src/vstack/cli/parser.py
@@ -1,4 +1,4 @@
-"""Utilities and tests for parser."""
+"""CLI argument parser and install target resolution."""
from __future__ import annotations
@@ -9,96 +9,83 @@
from vstack.constants import VERSION
-def _vscode_user_dir() -> Path | None:
- """Detect VS Code / VS Code Server user data directory."""
- candidates = [
- Path.home() / ".vscode-server" / "data" / "User",
- Path.home() / ".config" / "Code" / "User",
- Path.home() / "Library" / "Application Support" / "Code" / "User",
- Path.home() / "AppData" / "Roaming" / "Code" / "User",
- ]
- return next((p for p in candidates if p.exists()), None)
-
-
-def resolve_targets(args: argparse.Namespace) -> Path:
- """Return the install root directory from parsed arguments."""
- if getattr(args, "use_global", False):
- user_dir = _vscode_user_dir()
- if user_dir is None:
- print(
- "ERROR: Could not detect VS Code user data directory.\n"
- "Specify manually with: vstack install --target ~/.config/Code/User",
- file=sys.stderr,
- )
- sys.exit(1)
- return user_dir
-
- if getattr(args, "target", None):
- target = Path(args.target).expanduser().resolve()
- if not target.exists():
- print(f"ERROR: target directory does not exist: {target}", file=sys.stderr)
- sys.exit(1)
- return target / ".github"
-
- # default: current working directory
- return Path.cwd() / ".github"
-
-
-def build_parser() -> argparse.ArgumentParser:
- """Build parser."""
- parser = argparse.ArgumentParser(
- prog="vstack",
- description="Manage vstack skill generation and installation.",
- )
- parser.add_argument("--version", action="version", version=f"vstack {VERSION}")
- sub = parser.add_subparsers(dest="command", metavar="")
- sub.required = True
-
- p_validate = sub.add_parser(
- "validate", help="Render templates in memory, report unresolved tokens"
- )
- p_validate.add_argument(
- "--only",
- nargs="+",
- metavar="",
- help="Validate only these artifact types, e.g. --only skill agent",
- )
-
- p = sub.add_parser("verify", help="Validate source templates and/or installed output")
- group = p.add_mutually_exclusive_group()
- group.add_argument("--target", metavar="", help="Install into /.github/")
- group.add_argument(
- "--global",
- dest="use_global",
- action="store_true",
- help="VS Code user profile (agents/prompts/instructions/skills)",
- )
- p.add_argument(
- "--only",
- nargs="+",
- metavar="",
- help="Verify only these artifact types, e.g. --only agent prompt",
- )
- p.add_argument(
- "--no-source",
- dest="source",
- action="store_false",
- default=True,
- help="Skip source template checks",
- )
- p.add_argument(
- "--no-output",
- dest="output",
- action="store_false",
- default=True,
- help="Skip installed output checks",
- )
-
- for cmd, help_text in [
- ("install", "Generate and install artifacts (--only to filter types)"),
- ("uninstall", "Remove vstack-managed files"),
- ]:
- p = sub.add_parser(cmd, help=help_text)
+class CommandLineParser:
+ """Create the vstack command-line parser and resolve install targets."""
+
+ def vscode_user_dir(self) -> Path | None:
+ """Return the first detected VS Code user data directory.
+
+ Returns:
+ The detected VS Code or VS Code Server user directory, or ``None``
+ when no known location exists on the current machine.
+ """
+ candidates = [
+ Path.home() / ".vscode-server" / "data" / "User",
+ Path.home() / ".config" / "Code" / "User",
+ Path.home() / "Library" / "Application Support" / "Code" / "User",
+ Path.home() / "AppData" / "Roaming" / "Code" / "User",
+ ]
+ return next((p for p in candidates if p.exists()), None)
+
+ def resolve_targets(self, args: argparse.Namespace) -> Path:
+ """Resolve the install root directory from parsed CLI arguments.
+
+ Args:
+ args: Parsed ``argparse`` namespace for an install-like command.
+
+ Returns:
+ The effective install root directory.
+
+ Raises:
+ SystemExit: If ``--global`` cannot be resolved or the explicit
+ ``--target`` directory does not exist.
+ """
+ if getattr(args, "use_global", False):
+ user_dir = self.vscode_user_dir()
+ if user_dir is None:
+ print(
+ "ERROR: Could not detect VS Code user data directory.\n"
+ "Specify manually with: vstack install --target ~/.config/Code/User",
+ file=sys.stderr,
+ )
+ sys.exit(1)
+ return user_dir
+
+ if getattr(args, "target", None):
+ target = Path(args.target).expanduser().resolve()
+ if not target.exists():
+ print(f"ERROR: target directory does not exist: {target}", file=sys.stderr)
+ sys.exit(1)
+ return target / ".github"
+
+ # default: current working directory
+ return Path.cwd() / ".github"
+
+ def build(self) -> argparse.ArgumentParser:
+ """Create and configure the top-level ``argparse`` parser for vstack.
+
+ Returns:
+ A fully configured parser with all supported subcommands and flags.
+ """
+ parser = argparse.ArgumentParser(
+ prog="vstack",
+ description="Manage vstack skill generation and installation.",
+ )
+ parser.add_argument("--version", action="version", version=f"vstack {VERSION}")
+ sub = parser.add_subparsers(dest="command", metavar="")
+ sub.required = True
+
+ p_validate = sub.add_parser(
+ "validate", help="Render templates in memory, report unresolved tokens"
+ )
+ p_validate.add_argument(
+ "--only",
+ nargs="+",
+ metavar="",
+ help="Validate only these artifact types, e.g. --only skill agent",
+ )
+
+ p = sub.add_parser("verify", help="Validate source templates and/or installed output")
group = p.add_mutually_exclusive_group()
group.add_argument("--target", metavar="", help="Install into /.github/")
group.add_argument(
@@ -107,29 +94,63 @@ def build_parser() -> argparse.ArgumentParser:
action="store_true",
help="VS Code user profile (agents/prompts/instructions/skills)",
)
- if cmd == "install":
- p.add_argument(
- "--only",
- nargs="+",
- metavar="",
- help="Install only these artifact types, e.g. --only skill agent",
- )
- mode = p.add_mutually_exclusive_group()
- mode.add_argument(
- "--force",
- action="store_true",
- help="Overwrite existing artifacts unconditionally",
- )
- mode.add_argument(
- "--update",
- action="store_true",
- help="Install only when a newer version is available",
- )
- p.add_argument(
- "--dry-run",
- dest="dry_run",
+ p.add_argument(
+ "--only",
+ nargs="+",
+ metavar="",
+ help="Verify only these artifact types, e.g. --only agent prompt",
+ )
+ p.add_argument(
+ "--no-source",
+ dest="source",
+ action="store_false",
+ default=True,
+ help="Skip source template checks",
+ )
+ p.add_argument(
+ "--no-output",
+ dest="output",
+ action="store_false",
+ default=True,
+ help="Skip installed output checks",
+ )
+
+ for cmd, help_text in [
+ ("install", "Generate and install artifacts (--only to filter types)"),
+ ("uninstall", "Remove vstack-managed files"),
+ ]:
+ p = sub.add_parser(cmd, help=help_text)
+ group = p.add_mutually_exclusive_group()
+ group.add_argument("--target", metavar="", help="Install into /.github/")
+ group.add_argument(
+ "--global",
+ dest="use_global",
action="store_true",
- help="Show what would be installed without writing files",
+ help="VS Code user profile (agents/prompts/instructions/skills)",
)
+ if cmd == "install":
+ p.add_argument(
+ "--only",
+ nargs="+",
+ metavar="",
+ help="Install only these artifact types, e.g. --only skill agent",
+ )
+ mode = p.add_mutually_exclusive_group()
+ mode.add_argument(
+ "--force",
+ action="store_true",
+ help="Overwrite existing artifacts unconditionally",
+ )
+ mode.add_argument(
+ "--update",
+ action="store_true",
+ help="Install only when a newer version is available",
+ )
+ p.add_argument(
+ "--dry-run",
+ dest="dry_run",
+ action="store_true",
+ help="Show what would be installed without writing files",
+ )
- return parser
+ return parser
diff --git a/src/vstack/constants.py b/src/vstack/constants.py
index 6cf96bd..192a2e4 100644
--- a/src/vstack/constants.py
+++ b/src/vstack/constants.py
@@ -1,4 +1,4 @@
-"""Utilities and tests for constants."""
+"""Project-wide constants and version helpers."""
from __future__ import annotations
diff --git a/src/vstack/frontmatter/__init__.py b/src/vstack/frontmatter/__init__.py
index 452c0db..ab4dcbb 100644
--- a/src/vstack/frontmatter/__init__.py
+++ b/src/vstack/frontmatter/__init__.py
@@ -1,8 +1,8 @@
"""vstack.frontmatter — YAML frontmatter parsing, building, and schema validation."""
-from vstack.frontmatter.builder import build_output as build_output
from vstack.frontmatter.parser import FrontmatterContent as FrontmatterContent
from vstack.frontmatter.parser import FrontmatterParser as FrontmatterParser
from vstack.frontmatter.schema import FieldSpec as FieldSpec
from vstack.frontmatter.schema import FieldType as FieldType
from vstack.frontmatter.schema import FrontmatterSchema as FrontmatterSchema
+from vstack.frontmatter.serializer import FrontmatterSerializer as FrontmatterSerializer
diff --git a/src/vstack/frontmatter/builder.py b/src/vstack/frontmatter/builder.py
deleted file mode 100644
index dc3560c..0000000
--- a/src/vstack/frontmatter/builder.py
+++ /dev/null
@@ -1,164 +0,0 @@
-"""Frontmatter output builder — schema-filtered YAML serialisation.
-
-:func:`build_output` converts a metadata dict to a ``---`` / ``---`` YAML
-frontmatter block, filtered and ordered by a :class:`~vstack.frontmatter.FrontmatterSchema`.
-Fields absent from the schema are silently dropped so generator-internal
-metadata (``version``, etc.) never leaks into output files.
-"""
-
-from __future__ import annotations
-
-import re
-import textwrap
-
-from vstack.frontmatter.schema import FieldSpec, FrontmatterSchema
-
-
-def _serialize_scalar(spec: FieldSpec, value: object) -> str:
- """Serialize a single ``"str"`` value according to *spec* options."""
- text = str(value)
- if spec.normalize_whitespace:
- text = re.sub(r"\s+", " ", text).strip()
- if spec.max_length:
- text = text[: spec.max_length]
- if spec.quoted:
- return f"'{text.replace(chr(39), chr(39) * 2)}'"
- return text
-
-
-def _serialize_multiline_scalar(name: str, value: object, base_indent: str = "") -> list[str]:
- """Serialize a string scalar as YAML folded block (``>-``) lines."""
- text = str(value).strip()
- wrapped_lines: list[str] = []
- for paragraph in text.splitlines() or [text]:
- if not paragraph.strip():
- wrapped_lines.append("")
- continue
- wrapped_lines.extend(textwrap.wrap(paragraph.strip(), width=100))
- out = [f"{base_indent}{name}: >-"]
- out.extend(f"{base_indent} {line}" for line in wrapped_lines if line != "")
- return out
-
-
-def _should_emit_multiline(value: object, preserve_multiline: bool) -> bool:
- """Return True when scalar should be emitted as a folded block string."""
- if not preserve_multiline:
- return False
- text = str(value)
- return "\n" in text or len(text) > 90
-
-
-def _serialize_bool(value: object) -> str:
- """Return ``"true"`` or ``"false"`` regardless of input representation."""
- if isinstance(value, bool):
- return str(value).lower()
- return "true" if str(value).strip().lower() == "true" else "false"
-
-
-def _serialize_object(
- item: dict,
- item_schema: FrontmatterSchema | None,
- preserve_multiline: bool = False,
-) -> list[str]:
- """Serialize one object-list item to a list of YAML lines (without leading `` - ``).
-
- When *item_schema* is provided the item is ordered and filtered by the
- schema. Without a schema the item keys are emitted in their natural order.
- """
- if item_schema is None:
- lines: list[str] = []
- for k, v in item.items():
- v_str = str(v).strip()
- if isinstance(v, bool) or v_str.lower() in ("true", "false"):
- lines.append(f"{k}: {_serialize_bool(v)}")
- elif _should_emit_multiline(v, preserve_multiline):
- lines.extend(_serialize_multiline_scalar(k, v))
- else:
- safe = v_str.replace("'", "''")
- lines.append(f"{k}: '{safe}'")
- return lines
-
- pairs = [
- (spec, item.get(spec.name))
- for spec in item_schema.fields
- if item.get(spec.name) is not None
- ]
- lines = []
- for spec, value in pairs:
- if spec.type == "bool":
- lines.append(f"{spec.name}: {_serialize_bool(value)}")
- elif spec.type == "list":
- if isinstance(value, list) and value:
- lines.append(f"{spec.name}:")
- for item_v in value:
- lines.append(f" - {item_v}")
- else: # "str" (object items don't recurse into object-list)
- if _should_emit_multiline(value, preserve_multiline):
- lines.extend(_serialize_multiline_scalar(spec.name, value))
- else:
- lines.append(f"{spec.name}: {_serialize_scalar(spec, value)}")
- return lines
-
-
-def build_output(meta: dict, schema: FrontmatterSchema, preserve_multiline: bool = False) -> str:
- """Build a VS Code frontmatter block from *meta* filtered by *schema*.
-
- Iterates *schema* fields in declaration order. Fields present in *meta*
- but absent from *schema* are silently dropped.
-
- Serialisation rules:
-
- * ``"str"`` — optionally single-quoted, whitespace-normalised, truncated.
- * ``"bool"`` — ``true`` / ``false`` (VS Code requires lowercase).
- * ``"list"`` — YAML block sequence (`` - item`` per element).
- * ``"object-list"`` — YAML block sequence of mappings. When the field's
- :attr:`~vstack.frontmatter.FieldSpec.item_schema`
- is set, each item is ordered and filtered by that
- schema; otherwise item keys are written in natural order.
- """
- lines = ["---"]
- for spec in schema.fields:
- value = meta.get(spec.name)
- if value is None:
- continue
-
- if spec.type == "bool":
- lines.append(f"{spec.name}: {_serialize_bool(value)}")
-
- elif spec.type == "list":
- if isinstance(value, list) and value:
- lines.append(f"{spec.name}:")
- for item in value:
- lines.append(f" - {item}")
-
- elif spec.type == "object-list":
- if isinstance(value, list) and value:
- lines.append(f"{spec.name}:")
- for item in value:
- if not isinstance(item, dict):
- continue
- obj_lines = _serialize_object(
- item,
- spec.item_schema,
- preserve_multiline=preserve_multiline,
- )
- for i, obj_line in enumerate(obj_lines):
- prefix = " - " if i == 0 else " "
- lines.append(f"{prefix}{obj_line}")
-
- elif spec.type == "raw":
- raw_str = str(value).strip() if value is not None else ""
- if raw_str:
- lines.append(f"{spec.name}:")
- for raw_line in str(value).split("\n"):
- lines.append(raw_line)
-
- else: # "str"
- if _should_emit_multiline(value, preserve_multiline):
- lines.extend(_serialize_multiline_scalar(spec.name, value))
- else:
- lines.append(f"{spec.name}: {_serialize_scalar(spec, value)}")
-
- lines.append("---")
- lines.append("")
- return "\n".join(lines)
diff --git a/src/vstack/frontmatter/parser.py b/src/vstack/frontmatter/parser.py
index b145765..2223518 100644
--- a/src/vstack/frontmatter/parser.py
+++ b/src/vstack/frontmatter/parser.py
@@ -45,12 +45,45 @@ def __getitem__(self, key: str) -> object:
return self.metadata[key]
def __bool__(self) -> bool:
- """True when metadata is non-empty."""
+ """Return ``True`` when parsed metadata is non-empty."""
return bool(self.metadata)
class FrontmatterParser:
- """Stateless parser for YAML frontmatter."""
+ """Parse the repository's supported subset of YAML frontmatter."""
+
+ @staticmethod
+ def _is_current_object_list_item(meta: dict, current_key: str) -> bool:
+ """Return ``True`` when ``current_key`` points to the active object-list item."""
+ return (
+ bool(current_key)
+ and isinstance(meta.get(current_key), list)
+ and bool(meta[current_key])
+ and isinstance(meta[current_key][-1], dict)
+ )
+
+ @staticmethod
+ def _flush_object_block_scalar(
+ *,
+ meta: dict,
+ current_key: str,
+ object_scalar_field: str,
+ object_block_lines: list[str],
+ ) -> None:
+ """Flush buffered block-scalar content into the active object-list item."""
+ text = " ".join(b for b in object_block_lines if b).strip()
+ if FrontmatterParser._is_current_object_list_item(meta, current_key):
+ meta[current_key][-1][object_scalar_field] = text
+
+ @staticmethod
+ def _flush_raw_block(*, meta: dict, current_key: str, raw_lines: list[str]) -> None:
+ """Flush buffered raw block content into the current top-level key."""
+ meta[current_key] = "\n".join(raw_lines).rstrip()
+
+ @staticmethod
+ def _flush_block_scalar(*, meta: dict, current_key: str, block_lines: list[str]) -> None:
+ """Flush a buffered top-level block scalar into the current key."""
+ meta[current_key] = " ".join(b for b in block_lines if b).strip()
@staticmethod
def parse(content: str) -> FrontmatterContent:
@@ -68,9 +101,13 @@ def parse(content: str) -> FrontmatterContent:
@staticmethod
def parse_yaml(raw: str) -> dict:
- """Parse a raw YAML string (no ``---`` delimiters) and return a dict.
+ """Parse a raw YAML string without frontmatter delimiters.
+
+ Args:
+ raw: YAML content without surrounding ``---`` delimiters.
- Used for loading ``config.yaml`` files.
+ Returns:
+ A parsed metadata dictionary.
"""
return FrontmatterParser._parse_yaml_block(raw)
@@ -111,14 +148,12 @@ def _parse_yaml_block(raw: str) -> dict:
object_block_lines.append(line.strip())
continue
else:
- text = " ".join(b for b in object_block_lines if b).strip()
- if (
- current_key
- and isinstance(meta.get(current_key), list)
- and meta[current_key]
- and isinstance(meta[current_key][-1], dict)
- ):
- meta[current_key][-1][object_scalar_field] = text
+ FrontmatterParser._flush_object_block_scalar(
+ meta=meta,
+ current_key=current_key,
+ object_scalar_field=object_scalar_field,
+ object_block_lines=object_block_lines,
+ )
in_object_block_scalar = False
object_scalar_field = ""
object_block_lines = []
@@ -130,7 +165,11 @@ def _parse_yaml_block(raw: str) -> dict:
continue
else:
# Non-indented line closes the raw block; fall through to process it
- meta[current_key] = "\n".join(raw_lines).rstrip()
+ FrontmatterParser._flush_raw_block(
+ meta=meta,
+ current_key=current_key,
+ raw_lines=raw_lines,
+ )
in_raw_block = False
raw_lines = []
@@ -139,19 +178,17 @@ def _parse_yaml_block(raw: str) -> dict:
block_lines.append(line.strip())
continue
else:
- meta[current_key] = " ".join(b for b in block_lines if b).strip()
+ FrontmatterParser._flush_block_scalar(
+ meta=meta,
+ current_key=current_key,
+ block_lines=block_lines,
+ )
in_block_scalar = False
block_lines = []
# 4-space key: continuation of an object-list item
obj_kv = re.match(r"^ ([a-zA-Z_-]+):\s*(.*)$", line)
- if (
- obj_kv
- and current_key
- and isinstance(meta.get(current_key), list)
- and meta[current_key]
- and isinstance(meta[current_key][-1], dict)
- ):
+ if obj_kv and FrontmatterParser._is_current_object_list_item(meta, current_key):
obj_key = obj_kv.group(1)
obj_val = obj_kv.group(2).strip()
if obj_val in ("|", "|-", "|+", ">", ">-", ">+"):
@@ -212,18 +249,24 @@ def _parse_yaml_block(raw: str) -> dict:
meta[current_key] = val.strip("\"'")
if in_object_block_scalar:
- text = " ".join(b for b in object_block_lines if b).strip()
- if (
- current_key
- and isinstance(meta.get(current_key), list)
- and meta[current_key]
- and isinstance(meta[current_key][-1], dict)
- and object_scalar_field
- ):
- meta[current_key][-1][object_scalar_field] = text
+ if object_scalar_field:
+ FrontmatterParser._flush_object_block_scalar(
+ meta=meta,
+ current_key=current_key,
+ object_scalar_field=object_scalar_field,
+ object_block_lines=object_block_lines,
+ )
if in_raw_block and raw_lines:
- meta[current_key] = "\n".join(raw_lines).rstrip()
+ FrontmatterParser._flush_raw_block(
+ meta=meta,
+ current_key=current_key,
+ raw_lines=raw_lines,
+ )
if in_block_scalar and block_lines:
- meta[current_key] = " ".join(b for b in block_lines if b).strip()
+ FrontmatterParser._flush_block_scalar(
+ meta=meta,
+ current_key=current_key,
+ block_lines=block_lines,
+ )
return meta
diff --git a/src/vstack/frontmatter/schema.py b/src/vstack/frontmatter/schema.py
index 769d75f..57f0452 100644
--- a/src/vstack/frontmatter/schema.py
+++ b/src/vstack/frontmatter/schema.py
@@ -58,6 +58,61 @@ def get(self, name: str) -> FieldSpec | None:
"""Return the :class:`FieldSpec` for *name*, or ``None`` if not declared."""
return next((f for f in self.fields if f.name == name), None)
+ @staticmethod
+ def _validate_bool_field(spec: FieldSpec, value: object, errors: list[str]) -> None:
+ """Validate one bool-like field value."""
+ if str(value).lower() not in ("true", "false"):
+ errors.append(f"field '{spec.name}' must be 'true' or 'false', got: {value!r}")
+
+ @staticmethod
+ def _validate_list_field(spec: FieldSpec, value: object, errors: list[str]) -> None:
+ """Validate one list field value."""
+ if not isinstance(value, list):
+ errors.append(f"field '{spec.name}' must be a list, got: {value!r}")
+
+ @staticmethod
+ def _validate_object_list_field(spec: FieldSpec, value: object, errors: list[str]) -> None:
+ """Validate one object-list field value."""
+ if not isinstance(value, list):
+ errors.append(f"field '{spec.name}' must be a list, got: {value!r}")
+ return
+
+ for i, item in enumerate(value):
+ if not isinstance(item, dict):
+ errors.append(f"field '{spec.name}[{i}]' must be a mapping, got: {item!r}")
+ continue
+ if spec.item_schema is None:
+ continue
+ for err in spec.item_schema.validate_meta(item):
+ errors.append(f"{spec.name}[{i}].{err}")
+
+ @staticmethod
+ def _validate_str_field(spec: FieldSpec, value: object, errors: list[str]) -> None:
+ """Validate one string-like field value."""
+ if spec.max_length and isinstance(value, str) and len(value) > spec.max_length:
+ errors.append(
+ f"field '{spec.name}' exceeds max length {spec.max_length} ({len(value)} chars)"
+ )
+ if spec.pattern and isinstance(value, str) and not re.fullmatch(spec.pattern, value):
+ errors.append(
+ f"field '{spec.name}' does not match required pattern {spec.pattern!r}: {value!r}"
+ )
+
+ def _validate_field_value(self, spec: FieldSpec, value: object, errors: list[str]) -> None:
+ """Validate a field value according to its declared field type."""
+ if spec.type == "bool":
+ self._validate_bool_field(spec, value, errors)
+ return
+ if spec.type == "list":
+ self._validate_list_field(spec, value, errors)
+ return
+ if spec.type == "object-list":
+ self._validate_object_list_field(spec, value, errors)
+ return
+ if spec.type == "raw":
+ return
+ self._validate_str_field(spec, value, errors)
+
def validate_meta(self, meta: dict) -> list[str]:
"""Validate *meta* against declared schema fields.
@@ -83,37 +138,5 @@ def validate_meta(self, meta: dict) -> list[str]:
continue
if value is None:
continue
- if spec.type == "bool":
- if str(value).lower() not in ("true", "false"):
- errors.append(f"field '{spec.name}' must be 'true' or 'false', got: {value!r}")
- elif spec.type == "list":
- if not isinstance(value, list):
- errors.append(f"field '{spec.name}' must be a list, got: {value!r}")
- elif spec.type == "object-list":
- if not isinstance(value, list):
- errors.append(f"field '{spec.name}' must be a list, got: {value!r}")
- else:
- for i, item in enumerate(value):
- if not isinstance(item, dict):
- errors.append(
- f"field '{spec.name}[{i}]' must be a mapping, got: {item!r}"
- )
- elif spec.item_schema is not None:
- for err in spec.item_schema.validate_meta(item):
- errors.append(f"{spec.name}[{i}].{err}")
- elif spec.type == "raw":
- pass # raw blocks are carried through verbatim; no structural validation
- else: # "str"
- if spec.max_length and isinstance(value, str) and len(value) > spec.max_length:
- errors.append(
- f"field '{spec.name}' exceeds max length {spec.max_length} ({len(value)} chars)"
- )
- if (
- spec.pattern
- and isinstance(value, str)
- and not re.fullmatch(spec.pattern, value)
- ):
- errors.append(
- f"field '{spec.name}' does not match required pattern {spec.pattern!r}: {value!r}"
- )
+ self._validate_field_value(spec, value, errors)
return errors
diff --git a/src/vstack/frontmatter/serializer.py b/src/vstack/frontmatter/serializer.py
new file mode 100644
index 0000000..a4ccb03
--- /dev/null
+++ b/src/vstack/frontmatter/serializer.py
@@ -0,0 +1,208 @@
+"""Frontmatter serializer — schema-filtered YAML output.
+
+:class:`FrontmatterSerializer` converts a metadata dict to a ``---`` / ``---`` YAML
+frontmatter block, filtered and ordered by a :class:`~vstack.frontmatter.FrontmatterSchema`.
+Fields absent from the schema are silently dropped so generator-internal
+metadata (``version``, etc.) never leaks into output files.
+
+Main entry point: :meth:`FrontmatterSerializer.serialize`.
+"""
+
+from __future__ import annotations
+
+import re
+import textwrap
+
+from vstack.frontmatter.schema import FieldSpec, FrontmatterSchema
+
+
+class FrontmatterSerializer:
+ """Frontmatter serializer — converts metadata dict to YAML.
+
+ Instantiate once, then call :meth:`serialize` to render frontmatter.
+ No mutable instance state is retained between calls.
+ """
+
+ def _serialize_scalar(self, spec: FieldSpec, value: object) -> str:
+ """Serialize a single ``"str"`` value according to *spec* options."""
+ text = str(value)
+ if spec.normalize_whitespace:
+ text = re.sub(r"\s+", " ", text).strip()
+ if spec.max_length:
+ text = text[: spec.max_length]
+ if spec.quoted:
+ return f"'{text.replace(chr(39), chr(39) * 2)}'"
+ return text
+
+ def _serialize_multiline_scalar(
+ self, name: str, value: object, base_indent: str = ""
+ ) -> list[str]:
+ """Serialize a string scalar as YAML folded block (``>-``) lines."""
+ text = str(value).strip()
+ wrapped_lines: list[str] = []
+ for paragraph in text.splitlines() or [text]:
+ if not paragraph.strip():
+ wrapped_lines.append("")
+ continue
+ wrapped_lines.extend(textwrap.wrap(paragraph.strip(), width=100))
+ out = [f"{base_indent}{name}: >-"]
+ out.extend(f"{base_indent} {line}" for line in wrapped_lines if line != "")
+ return out
+
+ def _should_emit_multiline(self, value: object, preserve_multiline: bool) -> bool:
+ """Return ``True`` when a scalar should use folded-block YAML output."""
+ if not preserve_multiline:
+ return False
+ text = str(value)
+ return "\n" in text or len(text) > 90
+
+ def _serialize_bool(self, value: object) -> str:
+ """Return ``"true"`` or ``"false"`` regardless of input representation."""
+ if isinstance(value, bool):
+ return str(value).lower()
+ return "true" if str(value).strip().lower() == "true" else "false"
+
+ def _serialize_object_unschematized(
+ self, item: dict, preserve_multiline: bool = False
+ ) -> list[str]:
+ """Serialize object without schema — accepts any keys/values."""
+ lines: list[str] = []
+ for k, v in item.items():
+ v_str = str(v).strip()
+ if isinstance(v, bool) or v_str.lower() in ("true", "false"):
+ lines.append(f"{k}: {self._serialize_bool(v)}")
+ elif self._should_emit_multiline(v, preserve_multiline):
+ lines.extend(self._serialize_multiline_scalar(k, v))
+ else:
+ safe = v_str.replace("'", "''")
+ lines.append(f"{k}: '{safe}'")
+ return lines
+
+ def _serialize_object_field_pair(
+ self,
+ spec: FieldSpec,
+ value: object,
+ preserve_multiline: bool = False,
+ ) -> list[str]:
+ """Serialize a single schematized field/value pair for an object-list item."""
+ if spec.type == "bool":
+ return [f"{spec.name}: {self._serialize_bool(value)}"]
+ if spec.type == "list":
+ if isinstance(value, list) and value:
+ lines = [f"{spec.name}:"]
+ lines.extend(f" - {item_v}" for item_v in value)
+ return lines
+ return []
+ if self._should_emit_multiline(value, preserve_multiline):
+ return self._serialize_multiline_scalar(spec.name, value)
+ return [f"{spec.name}: {self._serialize_scalar(spec, value)}"]
+
+ def _serialize_object(
+ self,
+ item: dict,
+ item_schema: FrontmatterSchema | None,
+ preserve_multiline: bool = False,
+ ) -> list[str]:
+ """Serialize one object-list item to YAML lines (without leading `` - ``)."""
+ if item_schema is None:
+ return self._serialize_object_unschematized(item, preserve_multiline)
+
+ pairs = [
+ (spec, item.get(spec.name))
+ for spec in item_schema.fields
+ if item.get(spec.name) is not None
+ ]
+ ordered_lines: list[str] = []
+ for spec, value in pairs:
+ ordered_lines.extend(self._serialize_object_field_pair(spec, value, preserve_multiline))
+ return ordered_lines
+
+ def _append_object_list_items(
+ self,
+ lines: list[str],
+ value: list,
+ item_schema: FrontmatterSchema | None,
+ preserve_multiline: bool = False,
+ ) -> None:
+ """Append object-list items to lines with proper YAML indentation."""
+ for item in value:
+ if not isinstance(item, dict):
+ continue
+ obj_lines = self._serialize_object(
+ item,
+ item_schema,
+ preserve_multiline=preserve_multiline,
+ )
+ for i, obj_line in enumerate(obj_lines):
+ prefix = " - " if i == 0 else " "
+ lines.append(f"{prefix}{obj_line}")
+
+ def _append_raw_field(self, lines: list[str], name: str, value: object) -> None:
+ """Append a raw YAML field value without additional serialization."""
+ raw_str = str(value).strip() if value is not None else ""
+ if raw_str:
+ lines.append(f"{name}:")
+ for raw_line in str(value).split("\n"):
+ lines.append(raw_line)
+
+ def _append_field_by_type(
+ self,
+ lines: list[str],
+ spec: FieldSpec,
+ value: object,
+ preserve_multiline: bool = False,
+ ) -> None:
+ """Dispatch field rendering by type."""
+ if spec.type == "bool":
+ lines.append(f"{spec.name}: {self._serialize_bool(value)}")
+ return
+ if spec.type == "list":
+ if isinstance(value, list) and value:
+ lines.append(f"{spec.name}:")
+ for item in value:
+ lines.append(f" - {item}")
+ return
+ if spec.type == "object-list":
+ if isinstance(value, list) and value:
+ lines.append(f"{spec.name}:")
+ self._append_object_list_items(
+ lines,
+ value,
+ spec.item_schema,
+ preserve_multiline=preserve_multiline,
+ )
+ return
+ if spec.type == "raw":
+ self._append_raw_field(lines, spec.name, value)
+ return
+ if self._should_emit_multiline(value, preserve_multiline):
+ lines.extend(self._serialize_multiline_scalar(spec.name, value))
+ else:
+ lines.append(f"{spec.name}: {self._serialize_scalar(spec, value)}")
+
+ def serialize(
+ self,
+ meta: dict,
+ schema: FrontmatterSchema,
+ preserve_multiline: bool = False,
+ ) -> str:
+ """Serialize metadata into a VS Code frontmatter block.
+
+ Args:
+ meta: Input metadata values to serialize.
+ schema: Ordered frontmatter schema that filters and formats fields.
+ preserve_multiline: When ``True``, emit long strings as folded YAML
+ block scalars where appropriate.
+
+ Returns:
+ A YAML frontmatter block including opening and closing ``---`` lines.
+ """
+ lines = ["---"]
+ for spec in schema.fields:
+ value = meta.get(spec.name)
+ if value is None:
+ continue
+ self._append_field_by_type(lines, spec, value, preserve_multiline)
+ lines.append("---")
+ lines.append("")
+ return "\n".join(lines)
diff --git a/src/vstack/instructions/config.py b/src/vstack/instructions/config.py
index e09fad7..bd860ce 100644
--- a/src/vstack/instructions/config.py
+++ b/src/vstack/instructions/config.py
@@ -1,4 +1,4 @@
-"""Utilities and tests for config."""
+"""Instruction artifact type configuration."""
from vstack.artifacts.config import INSTRUCTION_SCHEMA, ArtifactTypeConfig
from vstack.instructions.constants import (
diff --git a/src/vstack/instructions/constants.py b/src/vstack/instructions/constants.py
index 75edf0f..67fcc40 100644
--- a/src/vstack/instructions/constants.py
+++ b/src/vstack/instructions/constants.py
@@ -1,4 +1,4 @@
-"""Utilities and tests for constants."""
+"""Constants for instruction template and output locations."""
from vstack.constants import TEMPLATES_ROOT
diff --git a/src/vstack/instructions/generator.py b/src/vstack/instructions/generator.py
index 10bc793..fd67950 100644
--- a/src/vstack/instructions/generator.py
+++ b/src/vstack/instructions/generator.py
@@ -1,4 +1,4 @@
-"""Utilities and tests for generator."""
+"""Thin instruction generator wrapper over ``GenericArtifactGenerator``."""
from vstack.artifacts.generator import GenericArtifactGenerator
from vstack.constants import TEMPLATES_ROOT
@@ -6,8 +6,8 @@
class InstructionGenerator(GenericArtifactGenerator):
- """Represents InstructionGenerator."""
+ """Generate instruction artifacts using the built-in instruction configuration."""
def __init__(self) -> None:
- """Initialize instance state."""
+ """Create an instruction generator bound to the built-in template root."""
super().__init__(INSTRUCTION_TYPE, TEMPLATES_ROOT)
diff --git a/src/vstack/main.py b/src/vstack/main.py
index ccb2614..48dfb14 100644
--- a/src/vstack/main.py
+++ b/src/vstack/main.py
@@ -1,18 +1,26 @@
-"""Utilities and tests for main."""
+"""CLI entrypoint and command dispatch helpers."""
from __future__ import annotations
import sys
from vstack.cli.commands import CommandLineInterface
-from vstack.cli.parser import build_parser, resolve_targets
+from vstack.cli.parser import CommandLineParser
from vstack.constants import TEMPLATES_ROOT
_GLOBAL_SUPPORTED_TYPES = ["agent", "instruction", "prompt", "skill"]
def _resolve_only_for_scope(args: object) -> list[str] | None:
- """Return the type filter for this command/scope, validating global constraints."""
+ """Resolve the active artifact-type filter for the parsed CLI arguments.
+
+ Args:
+ args: Parsed CLI arguments object, typically from ``argparse``.
+
+ Returns:
+ The explicit ``--only`` filter for the active scope, the default
+ global-profile artifact set, or ``None`` when no filter applies.
+ """
requested_only = getattr(args, "only", None)
if not getattr(args, "use_global", False):
return requested_only
@@ -33,15 +41,16 @@ def _resolve_only_for_scope(args: object) -> list[str] | None:
def main() -> None:
- """Main."""
- parser = build_parser()
+ """Parse CLI arguments and dispatch the selected top-level command."""
+ cli_parser = CommandLineParser()
+ parser = cli_parser.build()
args = parser.parse_args()
cli = CommandLineInterface(templates_root=TEMPLATES_ROOT)
if args.command == "validate":
sys.exit(cli.validate(only=getattr(args, "only", None)))
- install_dir = resolve_targets(args)
+ install_dir = cli_parser.resolve_targets(args)
only = _resolve_only_for_scope(args) if args.command in {"install", "verify"} else None
dispatch = {
"verify": lambda: cli.verify(
diff --git a/src/vstack/models.py b/src/vstack/models.py
index fcc4c3e..5901b4b 100644
--- a/src/vstack/models.py
+++ b/src/vstack/models.py
@@ -1,4 +1,4 @@
-"""Utilities and tests for models."""
+"""Shared validation result models."""
from __future__ import annotations
@@ -7,7 +7,7 @@
@dataclass
class CheckMessage:
- """Represents CheckMessage."""
+ """Represent a single validation message with a pass/fail level."""
level: str # "pass" | "fail"
message: str
@@ -15,21 +15,21 @@ class CheckMessage:
@dataclass
class ValidationResult:
- """Represents ValidationResult."""
+ """Store validation messages and expose aggregate result helpers."""
messages: list[CheckMessage] = field(default_factory=list)
@property
def passes(self) -> int:
- """Passes."""
+ """Return the number of passing validation messages."""
return sum(1 for m in self.messages if m.level == "pass")
@property
def failures(self) -> int:
- """Failures."""
+ """Return the number of failing validation messages."""
return sum(1 for m in self.messages if m.level == "fail")
@property
def ok(self) -> bool:
- """Ok."""
+ """Return ``True`` when the validation result contains no failures."""
return self.failures == 0
diff --git a/src/vstack/prompts/config.py b/src/vstack/prompts/config.py
index e45a6ee..ab9fd3d 100644
--- a/src/vstack/prompts/config.py
+++ b/src/vstack/prompts/config.py
@@ -1,4 +1,4 @@
-"""Utilities and tests for config."""
+"""Prompt artifact type configuration."""
from vstack.artifacts.config import PROMPT_SCHEMA, ArtifactTypeConfig
from vstack.prompts.constants import (
diff --git a/src/vstack/prompts/constants.py b/src/vstack/prompts/constants.py
index 8e3d896..14d2c9d 100644
--- a/src/vstack/prompts/constants.py
+++ b/src/vstack/prompts/constants.py
@@ -1,4 +1,4 @@
-"""Utilities and tests for constants."""
+"""Constants for prompt template and output locations."""
from vstack.constants import TEMPLATES_ROOT
diff --git a/src/vstack/prompts/generator.py b/src/vstack/prompts/generator.py
index 9aa63f2..59f594b 100644
--- a/src/vstack/prompts/generator.py
+++ b/src/vstack/prompts/generator.py
@@ -1,4 +1,4 @@
-"""Utilities and tests for generator."""
+"""Thin prompt generator wrapper over ``GenericArtifactGenerator``."""
from vstack.artifacts.generator import GenericArtifactGenerator
from vstack.constants import TEMPLATES_ROOT
@@ -6,8 +6,8 @@
class PromptGenerator(GenericArtifactGenerator):
- """Represents PromptGenerator."""
+ """Generate prompt artifacts using the built-in prompt configuration."""
def __init__(self) -> None:
- """Initialize instance state."""
+ """Create a prompt generator bound to the built-in template root."""
super().__init__(PROMPT_TYPE, TEMPLATES_ROOT)
diff --git a/src/vstack/skills/constants.py b/src/vstack/skills/constants.py
index 9bef1ce..5dcdf63 100644
--- a/src/vstack/skills/constants.py
+++ b/src/vstack/skills/constants.py
@@ -1,4 +1,4 @@
-"""Utilities and tests for constants."""
+"""Constants for skill template and partial locations."""
from __future__ import annotations
diff --git a/src/vstack/skills/generator.py b/src/vstack/skills/generator.py
index 8f1603c..cf4a6ee 100644
--- a/src/vstack/skills/generator.py
+++ b/src/vstack/skills/generator.py
@@ -13,8 +13,8 @@
class SkillGenerator(GenericArtifactGenerator):
- """Generator for skill artifacts, pre-configured for :data:`~vstack.artifacts.type_config.SKILL_TYPE`."""
+ """Generate skill artifacts using the built-in skill type configuration."""
def __init__(self) -> None:
- """Initialize instance state."""
+ """Create a skill generator bound to the built-in template root."""
super().__init__(SKILL_TYPE, TEMPLATES_ROOT)
diff --git a/src/vstack/skills/models.py b/src/vstack/skills/models.py
index 1fd2175..8bff385 100644
--- a/src/vstack/skills/models.py
+++ b/src/vstack/skills/models.py
@@ -1,4 +1,4 @@
-"""Utilities and tests for models."""
+"""Backwards-compatible exports for skill artifact models."""
from __future__ import annotations
diff --git a/tests/conftest.py b/tests/conftest.py
index 4de8ac6..b85913d 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -1,4 +1,4 @@
-"""Utilities and tests for conftest."""
+"""Shared pytest fixtures and test helpers."""
from __future__ import annotations
diff --git a/tests/vstack/agents/test___init__.py b/tests/vstack/agents/test___init__.py
index 5f20b26..810d7e6 100644
--- a/tests/vstack/agents/test___init__.py
+++ b/tests/vstack/agents/test___init__.py
@@ -1,4 +1,4 @@
-"""Utilities and tests for test init."""
+"""Tests for agent package exports."""
from __future__ import annotations
diff --git a/tests/vstack/agents/test_config.py b/tests/vstack/agents/test_config.py
index 9b92146..679d2ae 100644
--- a/tests/vstack/agents/test_config.py
+++ b/tests/vstack/agents/test_config.py
@@ -1,4 +1,4 @@
-"""Utilities and tests for test config."""
+"""Tests for agent artifact configuration."""
from __future__ import annotations
diff --git a/tests/vstack/agents/test_constants.py b/tests/vstack/agents/test_constants.py
index b3b0d14..b3a7b16 100644
--- a/tests/vstack/agents/test_constants.py
+++ b/tests/vstack/agents/test_constants.py
@@ -1,4 +1,4 @@
-"""Utilities and tests for test constants."""
+"""Tests for agent constants."""
from __future__ import annotations
diff --git a/tests/vstack/agents/test_generation.py b/tests/vstack/agents/test_generation.py
index ef8f1ec..896fe15 100644
--- a/tests/vstack/agents/test_generation.py
+++ b/tests/vstack/agents/test_generation.py
@@ -1,4 +1,4 @@
-"""Utilities and tests for test generation."""
+"""Tests for generated agent artifacts."""
from __future__ import annotations
diff --git a/tests/vstack/agents/test_generator.py b/tests/vstack/agents/test_generator.py
index b77913c..a2d0ae0 100644
--- a/tests/vstack/agents/test_generator.py
+++ b/tests/vstack/agents/test_generator.py
@@ -1,4 +1,4 @@
-"""Utilities and tests for test generator."""
+"""Tests for agent generator behavior."""
from __future__ import annotations
diff --git a/tests/vstack/agents/test_template_config_style.py b/tests/vstack/agents/test_template_config_style.py
index 8eca0f8..720e2ba 100644
--- a/tests/vstack/agents/test_template_config_style.py
+++ b/tests/vstack/agents/test_template_config_style.py
@@ -1,4 +1,4 @@
-"""Utilities and tests for test template config style."""
+"""Tests for agent template configuration style rules."""
from __future__ import annotations
diff --git a/tests/vstack/artifacts/test___init__.py b/tests/vstack/artifacts/test___init__.py
index 843f408..0190f1b 100644
--- a/tests/vstack/artifacts/test___init__.py
+++ b/tests/vstack/artifacts/test___init__.py
@@ -1,4 +1,4 @@
-"""Utilities and tests for test init."""
+"""Tests for artifact package exports."""
from __future__ import annotations
diff --git a/tests/vstack/artifacts/test_config.py b/tests/vstack/artifacts/test_config.py
index d89a921..2970c62 100644
--- a/tests/vstack/artifacts/test_config.py
+++ b/tests/vstack/artifacts/test_config.py
@@ -1,4 +1,4 @@
-"""Utilities and tests for test config."""
+"""Tests for artifact type configuration."""
from __future__ import annotations
diff --git a/tests/vstack/artifacts/test_constants.py b/tests/vstack/artifacts/test_constants.py
index f40df61..d9b357b 100644
--- a/tests/vstack/artifacts/test_constants.py
+++ b/tests/vstack/artifacts/test_constants.py
@@ -1,4 +1,4 @@
-"""Utilities and tests for test constants."""
+"""Tests for artifact constants."""
from __future__ import annotations
diff --git a/tests/vstack/artifacts/test_generator.py b/tests/vstack/artifacts/test_generator.py
index 0eb6dca..20ea21a 100644
--- a/tests/vstack/artifacts/test_generator.py
+++ b/tests/vstack/artifacts/test_generator.py
@@ -1,4 +1,4 @@
-"""Utilities and tests for test generator."""
+"""Tests for generic artifact generation."""
from __future__ import annotations
@@ -7,6 +7,7 @@
import pytest
+import vstack.artifacts.generator as generator_module
from vstack.agents.config import AGENT_TYPE
from vstack.artifacts.generator import GenericArtifactGenerator
from vstack.skills.config import SKILL_TYPE
@@ -76,8 +77,53 @@ def test_render_produces_rendered_artifact(self, tmp_path: Path) -> None:
artifact = self._make_skill_gen(tmp_path).render(tmpl_dir)
assert artifact.name == "vision"
assert "AUTO-GENERATED" in artifact.content
+ assert "VSTACK-META" in artifact.content
assert artifact.unresolved == []
+ def test_render_emits_parseable_vstack_metadata_footer(self, tmp_path: Path) -> None:
+ """Test that render emits machine-readable footer metadata."""
+ (tmp_path / "templates" / "skills" / "_partials").mkdir(parents=True)
+ (tmp_path / "templates" / "skills" / "_partials" / "skill-context.md").write_text(
+ "context", encoding="utf-8"
+ )
+ tmpl_dir = tmp_path / "templates" / "skills" / "verify"
+ tmpl_dir.mkdir(parents=True)
+ (tmpl_dir / "template.md").write_text(
+ "---\nname: verify\nversion: 2.3.4\ndescription: d\n---\n{{SKILL_CONTEXT}}\n",
+ encoding="utf-8",
+ )
+
+ artifact = self._make_skill_gen(tmp_path).render(tmpl_dir)
+ metadata = GenericArtifactGenerator.parse_generation_metadata(artifact.content)
+
+ assert metadata is not None
+ assert metadata["generator"] == "vstack"
+ assert metadata["artifact_type"] == "skill"
+ assert metadata["artifact_name"] == "verify"
+ assert metadata["artifact_version"] == "2.3.4"
+
+ def test_parse_generation_metadata_returns_none_without_footer(self) -> None:
+ """Test that metadata parser returns none when footer is missing."""
+ assert GenericArtifactGenerator.parse_generation_metadata("plain content") is None
+
+ def test_parse_generation_metadata_returns_none_on_invalid_json(self) -> None:
+ """Test that metadata parser returns none when footer JSON is invalid."""
+ text = ""
+ assert GenericArtifactGenerator.parse_generation_metadata(text) is None
+
+ def test_parse_generation_metadata_returns_none_on_non_object(self) -> None:
+ """Test that metadata parser returns none when footer JSON is not an object."""
+ text = ''
+ assert GenericArtifactGenerator.parse_generation_metadata(text) is None
+
+ def test_parse_generation_metadata_returns_none_when_loader_returns_non_dict(
+ self, monkeypatch: pytest.MonkeyPatch
+ ) -> None:
+ """Test that metadata parser rejects non-dict payloads from JSON loader."""
+ monkeypatch.setattr(generator_module.json, "loads", lambda _text: ["not", "dict"])
+ text = ""
+ assert GenericArtifactGenerator.parse_generation_metadata(text) is None
+
def test_generate_writes_files(self, tmp_path: Path) -> None:
"""Test that generate writes files."""
tmpl_dir = tmp_path / "templates" / "skills" / "alpha"
diff --git a/tests/vstack/artifacts/test_models.py b/tests/vstack/artifacts/test_models.py
index 30fa031..6cfb831 100644
--- a/tests/vstack/artifacts/test_models.py
+++ b/tests/vstack/artifacts/test_models.py
@@ -1,4 +1,4 @@
-"""Utilities and tests for test models."""
+"""Tests for artifact result models."""
from __future__ import annotations
diff --git a/tests/vstack/artifacts/test_protocol.py b/tests/vstack/artifacts/test_protocol.py
index f0d0bec..8125b07 100644
--- a/tests/vstack/artifacts/test_protocol.py
+++ b/tests/vstack/artifacts/test_protocol.py
@@ -1,4 +1,4 @@
-"""Utilities and tests for test protocol."""
+"""Tests for artifact generator protocol compatibility."""
from __future__ import annotations
@@ -10,7 +10,7 @@
class _Impl:
- """Represents Impl."""
+ """Minimal implementation that satisfies the artifact generator protocol."""
def generate(self, output_dir: Path) -> ArtifactResult:
"""Generate."""
diff --git a/tests/vstack/cli/test___init__.py b/tests/vstack/cli/test___init__.py
index 6e819a4..843c5e0 100644
--- a/tests/vstack/cli/test___init__.py
+++ b/tests/vstack/cli/test___init__.py
@@ -1,4 +1,4 @@
-"""Utilities and tests for test init."""
+"""Tests for CLI package exports."""
from __future__ import annotations
diff --git a/tests/vstack/cli/test_commands.py b/tests/vstack/cli/test_commands.py
index d7bf9f0..66a729d 100644
--- a/tests/vstack/cli/test_commands.py
+++ b/tests/vstack/cli/test_commands.py
@@ -1,15 +1,17 @@
-"""Utilities and tests for test commands."""
+"""Tests for CLI command handlers."""
from __future__ import annotations
import json
+import re
from pathlib import Path
from typing import Any, cast
from tests.conftest import run_vstack
from vstack.cli.commands import CommandLineInterface, _version_gt
from vstack.cli.constants import EXPECTED_CANONICAL_NAMES
-from vstack.constants import TEMPLATES_ROOT
+from vstack.cli.manifest import ArtifactEntry, Manifest
+from vstack.constants import TEMPLATES_ROOT, VERSION
from vstack.models import CheckMessage, ValidationResult
@@ -107,6 +109,43 @@ def test_install_writes_expected_skill_count(self, tmp_path: Path) -> None:
md_files = list((tmp_path / ".github" / "skills").glob("*/SKILL.md"))
assert len(md_files) == len(EXPECTED_CANONICAL_NAMES)
+ def test_install_only_preserves_manifest_entries_for_other_types(self, tmp_path: Path) -> None:
+ """Test that --only install does not drop manifest entries from other artifact types."""
+ install_dir = tmp_path / ".github"
+ install_dir.mkdir(parents=True)
+ manifest: dict[str, Any] = {
+ "vstack_version": "0.1.0",
+ "installed_at": "2026-01-01T00:00:00Z",
+ "artifacts": {
+ "agents": [
+ {
+ "name": "engineer",
+ "file": "agents/engineer.agent.md",
+ "version": "0.1.0",
+ }
+ ],
+ "skills": [
+ {
+ "name": "verify",
+ "file": "skills/verify/SKILL.md",
+ "version": "0.1.0",
+ }
+ ],
+ },
+ }
+ (install_dir / "vstack.json").write_text(json.dumps(manifest), encoding="utf-8")
+
+ cli = CommandLineInterface(templates_root=TEMPLATES_ROOT)
+ rc = cli.install(install_dir, only=["instruction"])
+ assert rc == 0
+
+ updated: dict[str, Any] = json.loads(
+ (install_dir / "vstack.json").read_text(encoding="utf-8")
+ )
+ assert "instructions" in updated["artifacts"]
+ assert updated["artifacts"]["agents"] == manifest["artifacts"]["agents"]
+ assert updated["artifacts"]["skills"] == manifest["artifacts"]["skills"]
+
def test_install_update_skips_when_version_not_newer(self, tmp_path: Path) -> None:
"""Test that install update skips when version not newer."""
install_dir = tmp_path / ".github"
@@ -177,7 +216,7 @@ def test_validate_returns_non_zero_on_unresolved(self, tmp_path: Path) -> None:
"""Test that validate returns non zero on unresolved."""
class _FakeArtifact:
- """Represents FakeArtifact."""
+ """Test double for a rendered artifact."""
def __init__(self) -> None:
"""Initialize instance state."""
@@ -185,14 +224,14 @@ def __init__(self) -> None:
self.unresolved = ["MISSING"]
class _Cfg:
- """Represents Cfg."""
+ """Minimal generator config for this test."""
type_name = "skill"
manifest_key = "skills"
output_subdir = "skills"
class _FakeGen:
- """Represents FakeGen."""
+ """Test double for an artifact generator."""
config = _Cfg()
@@ -216,7 +255,7 @@ def test_install_force_and_verify_input_fail_path(self, tmp_path: Path) -> None:
"""Test that install force and verify input fail path."""
class _FakeArtifact:
- """Represents FakeArtifact."""
+ """Test double for a rendered artifact."""
def __init__(self) -> None:
"""Initialize instance state."""
@@ -226,7 +265,7 @@ def __init__(self) -> None:
self.content = "content"
class _Cfg:
- """Represents Cfg."""
+ """Minimal generator config for this test."""
type_name = "skill"
manifest_key = "skills"
@@ -234,7 +273,7 @@ class _Cfg:
artifact_is_dir = True
class _FakeGen:
- """Represents FakeGen."""
+ """Test double for an artifact generator."""
config = _Cfg()
@@ -269,13 +308,13 @@ def test_verify_source_with_no_messages_and_all_passed(self, tmp_path: Path) ->
"""Test that verify source with no messages and all passed."""
class _Cfg:
- """Represents Cfg."""
+ """Minimal generator config for this test."""
type_name = "skill"
output_subdir = "skills"
class _FakeGen:
- """Represents FakeGen."""
+ """Test double for an artifact generator."""
config = _Cfg()
@@ -334,7 +373,7 @@ def test_validate_returns_zero_when_all_clean(self, tmp_path: Path) -> None:
"""Test that validate returns zero when all clean."""
class _FakeArtifact:
- """Represents FakeArtifact."""
+ """Test double for a rendered artifact."""
def __init__(self) -> None:
"""Initialize instance state."""
@@ -342,14 +381,14 @@ def __init__(self) -> None:
self.unresolved: list[str] = []
class _Cfg:
- """Represents Cfg."""
+ """Minimal generator config for this test."""
type_name = "skill"
manifest_key = "skills"
output_subdir = "skills"
class _FakeGen:
- """Represents FakeGen."""
+ """Test double for an artifact generator."""
config = _Cfg()
@@ -373,7 +412,7 @@ def test_validate_handles_missing_generator_for_type(self, tmp_path: Path) -> No
"""Test that validate handles missing generator for type."""
class _FakeArtifact:
- """Represents FakeArtifact."""
+ """Test double for a rendered artifact."""
def __init__(self) -> None:
"""Initialize instance state."""
@@ -381,12 +420,12 @@ def __init__(self) -> None:
self.unresolved: list[str] = []
class _Cfg:
- """Represents Cfg."""
+ """Minimal generator config for this test."""
type_name = "skill"
class _FakeGen:
- """Represents FakeGen."""
+ """Test double for an artifact generator."""
config = _Cfg()
@@ -461,17 +500,147 @@ def test_install_update_newer_version_writes_file(self, tmp_path: Path) -> None:
assert new_content != "old"
assert "AUTO-GENERATED" in new_content
+ def test_expected_output_names_falls_back_without_manifest(self) -> None:
+ """Test that expected output names fallback is used when manifest data is absent."""
+ cli = CommandLineInterface(templates_root=TEMPLATES_ROOT)
+ gen = cli._gen_for("skill")
+ assert gen is not None
+ assert cli._expected_output_names(gen, None) == EXPECTED_CANONICAL_NAMES
+
+ def test_verify_manifest_metadata_skips_missing_artifact_files(self, tmp_path: Path) -> None:
+ """Test that missing manifest-tracked files are ignored during metadata verification."""
+ cli = CommandLineInterface(templates_root=TEMPLATES_ROOT)
+ gen = cli._gen_for("skill")
+ assert gen is not None
+
+ manifest_data = Manifest(
+ vstack_version=VERSION,
+ installed_at="2026-01-01T00:00:00Z",
+ artifacts={
+ "skills": [
+ ArtifactEntry(
+ name="missing-skill",
+ file="skills/missing-skill/SKILL.md",
+ version="1.0.0",
+ )
+ ]
+ },
+ )
+
+ result = cli._verify_manifest_metadata(gen, manifest_data, tmp_path / ".github")
+ assert result is None
+
+ def test_install_rewrites_skipped_artifact_when_footer_version_mismatches(
+ self, tmp_path: Path
+ ) -> None:
+ """Test that install rewrites a skipped artifact when footer vstack_version is stale."""
+ install_dir = tmp_path / ".github"
+ cli = CommandLineInterface(templates_root=TEMPLATES_ROOT)
+
+ rc_install = cli.install(install_dir, only=["skill"])
+ assert rc_install == 0
+
+ artifact_path = install_dir / "skills" / "vision" / "SKILL.md"
+ original = artifact_path.read_text(encoding="utf-8")
+ tampered = re.sub(
+ r'"vstack_version":"[^"]+"',
+ '"vstack_version":"stale-version"',
+ original,
+ count=1,
+ )
+ artifact_path.write_text(tampered, encoding="utf-8")
+
+ rc_reinstall = cli.install(install_dir, only=["skill"], update=False)
+ assert rc_reinstall == 0
+
+ updated = artifact_path.read_text(encoding="utf-8")
+ assert '"vstack_version":"stale-version"' not in updated
+ assert f'"vstack_version":"{VERSION}"' in updated
+
+ def test_verify_fails_on_vstack_meta_version_mismatch(self, tmp_path: Path) -> None:
+ """Test that verify fails when VSTACK-META footer differs from manifest values."""
+ install_dir = tmp_path / ".github"
+ cli = CommandLineInterface(templates_root=TEMPLATES_ROOT)
+ rc_install = cli.install(install_dir, only=["instruction"])
+ assert rc_install == 0
+
+ instruction_file = install_dir / "instructions" / "python.instructions.md"
+ content = instruction_file.read_text(encoding="utf-8")
+ tampered = re.sub(
+ r'"vstack_version":"[^"]+"',
+ '"vstack_version":"tampered-version"',
+ content,
+ count=1,
+ )
+ instruction_file.write_text(tampered, encoding="utf-8")
+
+ rc_verify = cli.verify(
+ install_dir=install_dir,
+ source=False,
+ output=True,
+ only=["instruction"],
+ )
+ assert rc_verify == 1
+
+ def test_verify_accepts_legacy_artifact_without_vstack_meta(self, tmp_path: Path) -> None:
+ """Test that verify accepts old artifacts that do not include VSTACK-META."""
+ install_dir = tmp_path / ".github"
+ cli = CommandLineInterface(templates_root=TEMPLATES_ROOT)
+ rc_install = cli.install(install_dir, only=["instruction"])
+ assert rc_install == 0
+
+ instruction_file = install_dir / "instructions" / "python.instructions.md"
+ content = instruction_file.read_text(encoding="utf-8")
+ legacy_content = re.sub(r"\n\n", "\n", content, count=1)
+ instruction_file.write_text(legacy_content, encoding="utf-8")
+
+ rc_verify = cli.verify(
+ install_dir=install_dir,
+ source=False,
+ output=True,
+ only=["instruction"],
+ )
+ assert rc_verify == 0
+
+ def test_verify_rejects_legacy_artifact_without_vstack_meta_and_autogen(
+ self, tmp_path: Path
+ ) -> None:
+ """Test that verify rejects legacy fallback when AUTO-GENERATED marker is missing."""
+ install_dir = tmp_path / ".github"
+ cli = CommandLineInterface(templates_root=TEMPLATES_ROOT)
+ rc_install = cli.install(install_dir, only=["instruction"])
+ assert rc_install == 0
+
+ instruction_file = install_dir / "instructions" / "python.instructions.md"
+ content = instruction_file.read_text(encoding="utf-8")
+ no_meta = re.sub(r"\n\n", "\n", content, count=1)
+ no_autogen = re.sub(
+ r"\n\n",
+ "\n",
+ no_meta,
+ count=1,
+ )
+ instruction_file.write_text(no_autogen, encoding="utf-8")
+
+ rc_verify = cli.verify(
+ install_dir=install_dir,
+ source=False,
+ output=True,
+ only=["instruction"],
+ )
+ assert rc_verify == 1
+
def test_verify_source_with_messages_path(self, tmp_path: Path) -> None:
"""Test that verify source with messages path."""
class _Cfg:
- """Represents Cfg."""
+ """Minimal generator config for this test."""
type_name = "skill"
output_subdir = "skills"
class _FakeGen:
- """Represents FakeGen."""
+ """Test double for an artifact generator."""
config = _Cfg()
diff --git a/tests/vstack/cli/test_constants.py b/tests/vstack/cli/test_constants.py
index 0660bd7..1b95e48 100644
--- a/tests/vstack/cli/test_constants.py
+++ b/tests/vstack/cli/test_constants.py
@@ -1,4 +1,4 @@
-"""Utilities and tests for test constants."""
+"""Tests for canonical CLI names."""
from __future__ import annotations
diff --git a/tests/vstack/cli/test_integration_runtime.py b/tests/vstack/cli/test_integration_runtime.py
index d3d4966..dc10715 100644
--- a/tests/vstack/cli/test_integration_runtime.py
+++ b/tests/vstack/cli/test_integration_runtime.py
@@ -1,4 +1,4 @@
-"""Utilities and tests for test integration runtime."""
+"""Tests for CLI runtime integration behavior."""
from __future__ import annotations
diff --git a/tests/vstack/cli/test_manifest.py b/tests/vstack/cli/test_manifest.py
index 1b07164..4b37210 100644
--- a/tests/vstack/cli/test_manifest.py
+++ b/tests/vstack/cli/test_manifest.py
@@ -1,4 +1,4 @@
-"""Utilities and tests for test manifest."""
+"""Tests for install manifest handling."""
from __future__ import annotations
diff --git a/tests/vstack/cli/test_parser.py b/tests/vstack/cli/test_parser.py
index 8905df9..5085353 100644
--- a/tests/vstack/cli/test_parser.py
+++ b/tests/vstack/cli/test_parser.py
@@ -1,4 +1,4 @@
-"""Utilities and tests for test parser."""
+"""Tests for CLI parser behavior."""
from __future__ import annotations
@@ -18,7 +18,7 @@ def test_vscode_user_dir_none_when_missing(
) -> None:
"""Test that vscode user dir none when missing."""
monkeypatch.setattr(Path, "home", lambda: tmp_path)
- assert parser_module._vscode_user_dir() is None
+ assert parser_module.CommandLineParser().vscode_user_dir() is None
class TestResolveTargets:
@@ -30,28 +30,28 @@ def test_resolve_targets_default_uses_cwd_dot_github(
"""Test that resolve targets default uses cwd dot github."""
monkeypatch.chdir(tmp_path)
args = argparse.Namespace(target=None, use_global=False)
- assert parser_module.resolve_targets(args) == tmp_path / ".github"
+ assert parser_module.CommandLineParser().resolve_targets(args) == tmp_path / ".github"
def test_resolve_targets_target_existing(self, tmp_path: Path) -> None:
"""Test that resolve targets target existing."""
target = tmp_path / "workspace"
target.mkdir()
args = argparse.Namespace(target=str(target), use_global=False)
- assert parser_module.resolve_targets(args) == target / ".github"
+ assert parser_module.CommandLineParser().resolve_targets(args) == target / ".github"
def test_resolve_targets_target_missing_exits(self, tmp_path: Path) -> None:
"""Test that resolve targets target missing exits."""
args = argparse.Namespace(target=str(tmp_path / "missing"), use_global=False)
with pytest.raises(SystemExit) as exc:
- parser_module.resolve_targets(args)
+ parser_module.CommandLineParser().resolve_targets(args)
assert exc.value.code == 1
def test_resolve_targets_global_missing_exits(self, monkeypatch: pytest.MonkeyPatch) -> None:
"""Test that resolve targets global missing exits."""
- monkeypatch.setattr(parser_module, "_vscode_user_dir", lambda: None)
+ monkeypatch.setattr(parser_module.CommandLineParser, "vscode_user_dir", lambda self: None)
args = argparse.Namespace(target=None, use_global=True)
with pytest.raises(SystemExit) as exc:
- parser_module.resolve_targets(args)
+ parser_module.CommandLineParser().resolve_targets(args)
assert exc.value.code == 1
def test_resolve_targets_global_returns_user_dir(
@@ -60,9 +60,11 @@ def test_resolve_targets_global_returns_user_dir(
"""Test that resolve targets global returns user dir."""
user_dir = tmp_path / "User"
user_dir.mkdir(parents=True)
- monkeypatch.setattr(parser_module, "_vscode_user_dir", lambda: user_dir)
+ monkeypatch.setattr(
+ parser_module.CommandLineParser, "vscode_user_dir", lambda self: user_dir
+ )
args = argparse.Namespace(target=None, use_global=True)
- assert parser_module.resolve_targets(args) == user_dir
+ assert parser_module.CommandLineParser().resolve_targets(args) == user_dir
class TestBuildParser:
@@ -70,14 +72,14 @@ class TestBuildParser:
def test_build_parser_has_expected_commands(self) -> None:
"""Test that build parser has expected commands."""
- parser = parser_module.build_parser()
+ parser = parser_module.CommandLineParser().build()
actions = [a for a in parser._actions if isinstance(a, argparse._SubParsersAction)]
subcommands = set(actions[0].choices.keys())
assert subcommands == {"validate", "verify", "install", "uninstall"}
def test_verify_accepts_only_filter(self) -> None:
"""Test that verify command supports --only type filters."""
- parser = parser_module.build_parser()
+ parser = parser_module.CommandLineParser().build()
args = parser.parse_args(["verify", "--only", "agent", "prompt", "--no-source"])
assert args.command == "verify"
assert args.only == ["agent", "prompt"]
diff --git a/tests/vstack/frontmatter/test___init__.py b/tests/vstack/frontmatter/test___init__.py
index 1079a33..fcdd011 100644
--- a/tests/vstack/frontmatter/test___init__.py
+++ b/tests/vstack/frontmatter/test___init__.py
@@ -1,4 +1,4 @@
-"""Utilities and tests for test init."""
+"""Tests for frontmatter package exports."""
from __future__ import annotations
@@ -8,7 +8,7 @@
class TestFrontmatterInit:
"""Test cases for FrontmatterInit."""
- def test_reexports_parser_builder_schema(self) -> None:
- """Test that reexports parser builder schema."""
+ def test_reexports_parser_serializer_schema(self) -> None:
+ """Test that reexports parser serializer schema."""
assert vstack.frontmatter.FrontmatterParser is not None
- assert vstack.frontmatter.build_output is not None
+ assert vstack.frontmatter.FrontmatterSerializer is not None
diff --git a/tests/vstack/frontmatter/test_parser.py b/tests/vstack/frontmatter/test_parser.py
index 1f43c6d..634a20e 100644
--- a/tests/vstack/frontmatter/test_parser.py
+++ b/tests/vstack/frontmatter/test_parser.py
@@ -1,4 +1,4 @@
-"""Utilities and tests for test parser."""
+"""Tests for frontmatter parser behavior."""
from __future__ import annotations
diff --git a/tests/vstack/frontmatter/test_schema.py b/tests/vstack/frontmatter/test_schema.py
index 913a5a6..4ce0088 100644
--- a/tests/vstack/frontmatter/test_schema.py
+++ b/tests/vstack/frontmatter/test_schema.py
@@ -1,4 +1,4 @@
-"""Utilities and tests for test schema."""
+"""Tests for frontmatter schema validation."""
from __future__ import annotations
@@ -90,6 +90,11 @@ def test_validate_meta_object_list_nested_schema_error(self) -> None:
errors = schema.validate_meta({"items": [{"enabled": "maybe"}]})
assert any("items[0].field 'enabled'" in e for e in errors)
+ def test_validate_meta_object_list_mapping_without_item_schema(self) -> None:
+ """Test that mapping items are accepted when object-list has no item schema."""
+ schema = FrontmatterSchema([FieldSpec("items", type="object-list")])
+ assert schema.validate_meta({"items": [{"k": "v"}]}) == []
+
def test_validate_meta_raw_field_has_no_structural_validation(self) -> None:
"""Test that validate meta raw field has no structural validation."""
schema = FrontmatterSchema([FieldSpec("meta", type="raw")])
diff --git a/tests/vstack/frontmatter/test_builder.py b/tests/vstack/frontmatter/test_serializer.py
similarity index 52%
rename from tests/vstack/frontmatter/test_builder.py
rename to tests/vstack/frontmatter/test_serializer.py
index ced7748..e6e46e3 100644
--- a/tests/vstack/frontmatter/test_builder.py
+++ b/tests/vstack/frontmatter/test_serializer.py
@@ -1,42 +1,44 @@
-"""Utilities and tests for test builder."""
+"""Tests for FrontmatterSerializer behavior."""
from __future__ import annotations
-from vstack.frontmatter import FieldSpec, FrontmatterSchema, build_output
+from vstack.frontmatter import FieldSpec, FrontmatterSchema, FrontmatterSerializer
from vstack.skills.config import SKILL_SCHEMA
-class TestBuildOutput:
- """Test cases for BuildOutput."""
+class TestFrontmatterSerializer:
+ """Test cases for FrontmatterSerializer."""
- def test_build_output_raw_field(self) -> None:
- """Test that build output raw field."""
+ def test_serialize_raw_field(self) -> None:
+ """Test that serialize renders raw field."""
schema = FrontmatterSchema(
[FieldSpec("name", quoted=False), FieldSpec("mcp-servers", type="raw")]
)
- output = build_output(
+ output = FrontmatterSerializer().serialize(
{"name": "agent", "mcp-servers": " srv:\n command: cmd"},
schema,
)
assert "mcp-servers:\n srv:" in output
- def test_build_output_raw_field_empty_skipped(self) -> None:
- """Test that build output raw field empty skipped."""
+ def test_serialize_raw_field_empty_skipped(self) -> None:
+ """Test that serialize skips empty raw field."""
schema = FrontmatterSchema(
[FieldSpec("name", quoted=False), FieldSpec("mcp-servers", type="raw")]
)
- output = build_output({"name": "agent", "mcp-servers": ""}, schema)
+ output = FrontmatterSerializer().serialize({"name": "agent", "mcp-servers": ""}, schema)
assert "mcp-servers" not in output
- def test_build_output_frontmatter_required_fields(self) -> None:
- """Test that build output frontmatter required fields."""
- output = build_output({"name": "vision", "description": "A test skill"}, SKILL_SCHEMA)
+ def test_serialize_frontmatter_required_fields(self) -> None:
+ """Test that serialize includes required frontmatter fields."""
+ output = FrontmatterSerializer().serialize(
+ {"name": "vision", "description": "A test skill"}, SKILL_SCHEMA
+ )
assert output.startswith("---\n")
assert "name: vision" in output
- def test_build_output_frontmatter_optional_fields(self) -> None:
- """Test that build output frontmatter optional fields."""
- output = build_output(
+ def test_serialize_frontmatter_optional_fields(self) -> None:
+ """Test that serialize includes optional frontmatter fields."""
+ output = FrontmatterSerializer().serialize(
{
"name": "x",
"description": "d",
@@ -48,19 +50,19 @@ def test_build_output_frontmatter_optional_fields(self) -> None:
assert "argument-hint: 'some hint'" in output
assert "user-invocable: true" in output
- def test_build_output_strips_extra_fields(self) -> None:
- """Test that build output strips extra fields."""
- output = build_output(
+ def test_serialize_strips_extra_fields(self) -> None:
+ """Test that serialize strips fields not in schema."""
+ output = FrontmatterSerializer().serialize(
{"name": "x", "description": "d", "version": "1.0.0", "extra": "value"},
SKILL_SCHEMA,
)
assert "version: 1.0.0" not in output
assert "extra" not in output
- def test_build_output_object_list_without_item_schema(self) -> None:
- """Test that build output object list without item schema."""
+ def test_serialize_object_list_without_item_schema(self) -> None:
+ """Test that serialize renders object-list without item schema."""
schema = FrontmatterSchema([FieldSpec("handoffs", type="object-list")])
- output = build_output(
+ output = FrontmatterSerializer().serialize(
{
"handoffs": [
{"label": "A", "send": True, "prompt": "go"},
@@ -72,8 +74,8 @@ def test_build_output_object_list_without_item_schema(self) -> None:
assert "- label: 'A'" in output
assert "send: true" in output
- def test_build_output_object_list_with_item_schema_and_list_field(self) -> None:
- """Test that build output object list with item schema and list field."""
+ def test_serialize_object_list_with_item_schema_and_list_field(self) -> None:
+ """Test that serialize renders object-list with schematized fields and nested lists."""
item_schema = FrontmatterSchema(
[
FieldSpec("label"),
@@ -84,7 +86,7 @@ def test_build_output_object_list_with_item_schema_and_list_field(self) -> None:
schema = FrontmatterSchema(
[FieldSpec("handoffs", type="object-list", item_schema=item_schema)]
)
- output = build_output(
+ output = FrontmatterSerializer().serialize(
{
"handoffs": [
{"label": "handoff", "send": "false", "roles": ["reader", "writer"]},
@@ -97,16 +99,33 @@ def test_build_output_object_list_with_item_schema_and_list_field(self) -> None:
assert "roles:" in output
assert "- reader" in output
- def test_build_output_object_list_skips_non_dict_items(self) -> None:
- """Test that build output object list skips non dict items."""
+ def test_serialize_object_list_with_empty_nested_list_omits_list_field(self) -> None:
+ """Test that empty list fields in object-list items are omitted."""
+ item_schema = FrontmatterSchema([FieldSpec("label"), FieldSpec("roles", type="list")])
+ schema = FrontmatterSchema(
+ [FieldSpec("handoffs", type="object-list", item_schema=item_schema)]
+ )
+ output = FrontmatterSerializer().serialize(
+ {
+ "handoffs": [
+ {"label": "handoff", "roles": []},
+ ]
+ },
+ schema,
+ )
+ assert "- label: 'handoff'" in output
+ assert "roles:" not in output
+
+ def test_serialize_object_list_skips_non_dict_items(self) -> None:
+ """Test that serialize skips non-dict items in object-list."""
schema = FrontmatterSchema([FieldSpec("handoffs", type="object-list")])
- output = build_output({"handoffs": ["bad", {"label": "ok"}]}, schema)
+ output = FrontmatterSerializer().serialize({"handoffs": ["bad", {"label": "ok"}]}, schema)
assert "- label: 'ok'" in output
assert "bad" not in output
- def test_build_output_multiline_scalar_when_enabled(self) -> None:
- """Test that build output multiline scalar when enabled."""
- output = build_output(
+ def test_serialize_multiline_scalar_when_enabled(self) -> None:
+ """Test that serialize uses folded blocks when preserve_multiline=True."""
+ output = FrontmatterSerializer().serialize(
{
"name": "x",
"description": "This is a very long description that should be emitted as a folded block scalar when multiline output is enabled for readability in generated frontmatter.",
@@ -116,10 +135,10 @@ def test_build_output_multiline_scalar_when_enabled(self) -> None:
)
assert "description: >-" in output
- def test_build_output_multiline_object_list_scalar_when_enabled(self) -> None:
- """Test that build output multiline object list scalar when enabled."""
+ def test_serialize_multiline_object_list_scalar_when_enabled(self) -> None:
+ """Test that serialize uses folded blocks in object-list scalars."""
schema = FrontmatterSchema([FieldSpec("handoffs", type="object-list")])
- output = build_output(
+ output = FrontmatterSerializer().serialize(
{
"handoffs": [
{
@@ -133,9 +152,9 @@ def test_build_output_multiline_object_list_scalar_when_enabled(self) -> None:
)
assert " prompt: >-" in output
- def test_build_output_multiline_scalar_preserves_content_around_blank_lines(self) -> None:
- """Test that build output multiline scalar preserves content around blank lines."""
- output = build_output(
+ def test_serialize_multiline_scalar_preserves_content_around_blank_lines(self) -> None:
+ """Test that serialize preserves blank lines in folded block scalars."""
+ output = FrontmatterSerializer().serialize(
{
"name": "x",
"description": "First paragraph.\n\nSecond paragraph.",
diff --git a/tests/vstack/instructions/test_generator.py b/tests/vstack/instructions/test_generator.py
index 5412791..218b30c 100644
--- a/tests/vstack/instructions/test_generator.py
+++ b/tests/vstack/instructions/test_generator.py
@@ -1,4 +1,4 @@
-"""Utilities and tests for test generator."""
+"""Tests for instruction generator behavior."""
from __future__ import annotations
diff --git a/tests/vstack/prompts/test_generator.py b/tests/vstack/prompts/test_generator.py
index 03e2ef1..778cddd 100644
--- a/tests/vstack/prompts/test_generator.py
+++ b/tests/vstack/prompts/test_generator.py
@@ -1,4 +1,4 @@
-"""Utilities and tests for test generator."""
+"""Tests for prompt generator behavior."""
from __future__ import annotations
diff --git a/tests/vstack/skills/test___init__.py b/tests/vstack/skills/test___init__.py
index 6b3eca1..0f17340 100644
--- a/tests/vstack/skills/test___init__.py
+++ b/tests/vstack/skills/test___init__.py
@@ -1,4 +1,4 @@
-"""Utilities and tests for test init."""
+"""Tests for skill package exports."""
from __future__ import annotations
diff --git a/tests/vstack/skills/test_config.py b/tests/vstack/skills/test_config.py
index 75d2c6b..768a771 100644
--- a/tests/vstack/skills/test_config.py
+++ b/tests/vstack/skills/test_config.py
@@ -1,4 +1,4 @@
-"""Utilities and tests for test config."""
+"""Tests for skill artifact configuration."""
from __future__ import annotations
diff --git a/tests/vstack/skills/test_constants.py b/tests/vstack/skills/test_constants.py
index 7ce07cd..0a7223f 100644
--- a/tests/vstack/skills/test_constants.py
+++ b/tests/vstack/skills/test_constants.py
@@ -1,4 +1,4 @@
-"""Utilities and tests for test constants."""
+"""Tests for skill constants."""
from __future__ import annotations
diff --git a/tests/vstack/skills/test_generator.py b/tests/vstack/skills/test_generator.py
index c1862b2..5fa9ade 100644
--- a/tests/vstack/skills/test_generator.py
+++ b/tests/vstack/skills/test_generator.py
@@ -1,4 +1,4 @@
-"""Utilities and tests for test generator."""
+"""Tests for skill generator behavior."""
from __future__ import annotations
diff --git a/tests/vstack/skills/test_models.py b/tests/vstack/skills/test_models.py
index 390efbe..4ddd574 100644
--- a/tests/vstack/skills/test_models.py
+++ b/tests/vstack/skills/test_models.py
@@ -1,4 +1,4 @@
-"""Utilities and tests for test models."""
+"""Tests for skill model aliases."""
from __future__ import annotations
diff --git a/tests/vstack/skills/test_templates.py b/tests/vstack/skills/test_templates.py
index db941ae..a7c6748 100644
--- a/tests/vstack/skills/test_templates.py
+++ b/tests/vstack/skills/test_templates.py
@@ -1,7 +1,8 @@
-"""Utilities and tests for test templates."""
+"""Tests for skill template source files."""
from __future__ import annotations
+import re
from pathlib import Path
from tests.conftest import SKILLS_TEMPLATES_DIR
@@ -66,10 +67,12 @@ def test_generated_files_have_auto_gen_footer(self, generated_dir: Path) -> None
"""Test that generated files have auto gen footer."""
for md in generated_dir.glob("*/SKILL.md"):
content = md.read_text(encoding="utf-8")
- assert "AUTO-GENERATED" in content
- assert content.endswith(
- "\n"
+ lines = content.rstrip().splitlines()
+ assert len(lines) >= 2
+ assert (
+ lines[-2] == ""
)
+ assert re.fullmatch(r"", lines[-1])
def test_generated_count_matches_expected(self, generated_dir: Path) -> None:
"""Test that generated count matches expected."""
diff --git a/tests/vstack/test___init__.py b/tests/vstack/test___init__.py
index 9a236fd..73586f5 100644
--- a/tests/vstack/test___init__.py
+++ b/tests/vstack/test___init__.py
@@ -1,4 +1,4 @@
-"""Utilities and tests for test init."""
+"""Tests for top-level package exports."""
from __future__ import annotations
diff --git a/tests/vstack/test___main__.py b/tests/vstack/test___main__.py
index 40389af..7f5eb72 100644
--- a/tests/vstack/test___main__.py
+++ b/tests/vstack/test___main__.py
@@ -1,4 +1,4 @@
-"""Utilities and tests for test main."""
+"""Tests for the ``python -m vstack`` entrypoint."""
from __future__ import annotations
diff --git a/tests/vstack/test_constants.py b/tests/vstack/test_constants.py
index 856c814..abe1749 100644
--- a/tests/vstack/test_constants.py
+++ b/tests/vstack/test_constants.py
@@ -1,4 +1,4 @@
-"""Utilities and tests for test constants."""
+"""Tests for project constants and version helpers."""
from __future__ import annotations
diff --git a/tests/vstack/test_main.py b/tests/vstack/test_main.py
index ae7a3fa..3787bb8 100644
--- a/tests/vstack/test_main.py
+++ b/tests/vstack/test_main.py
@@ -1,4 +1,4 @@
-"""Utilities and tests for test main."""
+"""Tests for CLI entrypoint dispatch behavior."""
from __future__ import annotations
@@ -10,7 +10,7 @@
class _Args:
- """Represents parsed CLI args for main() dispatch tests."""
+ """Minimal parsed-argument object for main() dispatch tests."""
def __init__(
self,
@@ -35,7 +35,7 @@ def __init__(
class _CLI:
- """Represents CLI."""
+ """Test double for the CLI command handler."""
def __init__(self) -> None:
"""Initialize instance state."""
@@ -63,7 +63,7 @@ def uninstall(self, *args, **kwargs):
class TestMain:
- """Test cases for Main."""
+ """Test cases for the CLI entrypoint."""
def test_resolve_only_for_scope_returns_requested_only_for_non_global(self) -> None:
"""Test that non-global commands keep the explicit type filter."""
@@ -76,7 +76,7 @@ def test_main_dispatch_validate(self, monkeypatch: pytest.MonkeyPatch) -> None:
cli = _CLI()
parser = type("P", (), {"parse_args": lambda self: _Args("validate")})()
- monkeypatch.setattr(main_module, "build_parser", lambda: parser)
+ monkeypatch.setattr(main_module.CommandLineParser, "build", lambda self: parser)
monkeypatch.setattr(main_module, "CommandLineInterface", lambda templates_root: cli)
monkeypatch.setattr(
main_module.sys, "exit", lambda code: (_ for _ in ()).throw(SystemExit(code))
@@ -101,8 +101,10 @@ def _build_cli(templates_root):
args = _Args("install", only=["skill"], force=True, update=False, dry_run=True)
parser = type("P", (), {"parse_args": lambda self: args})()
- monkeypatch.setattr(main_module, "build_parser", lambda: parser)
- monkeypatch.setattr(main_module, "resolve_targets", lambda _args: tmp_path)
+ monkeypatch.setattr(main_module.CommandLineParser, "build", lambda self: parser)
+ monkeypatch.setattr(
+ main_module.CommandLineParser, "resolve_targets", lambda self, _args: tmp_path
+ )
monkeypatch.setattr(main_module, "CommandLineInterface", _build_cli)
monkeypatch.setattr(
main_module.sys, "exit", lambda code: (_ for _ in ()).throw(SystemExit(code))
@@ -132,8 +134,10 @@ def test_main_global_install_defaults_to_supported_types(
args = _Args("install", only=None, use_global=True)
parser = type("P", (), {"parse_args": lambda self: args})()
- monkeypatch.setattr(main_module, "build_parser", lambda: parser)
- monkeypatch.setattr(main_module, "resolve_targets", lambda _args: tmp_path)
+ monkeypatch.setattr(main_module.CommandLineParser, "build", lambda self: parser)
+ monkeypatch.setattr(
+ main_module.CommandLineParser, "resolve_targets", lambda self, _args: tmp_path
+ )
monkeypatch.setattr(main_module, "CommandLineInterface", lambda templates_root: cli)
monkeypatch.setattr(
main_module.sys, "exit", lambda code: (_ for _ in ()).throw(SystemExit(code))
@@ -168,8 +172,10 @@ def test_main_global_install_rejects_unknown_type(
args = _Args("install", only=["unknown"], use_global=True)
parser = type("P", (), {"parse_args": lambda self: args})()
- monkeypatch.setattr(main_module, "build_parser", lambda: parser)
- monkeypatch.setattr(main_module, "resolve_targets", lambda _args: tmp_path)
+ monkeypatch.setattr(main_module.CommandLineParser, "build", lambda self: parser)
+ monkeypatch.setattr(
+ main_module.CommandLineParser, "resolve_targets", lambda self, _args: tmp_path
+ )
monkeypatch.setattr(main_module, "CommandLineInterface", lambda templates_root: cli)
monkeypatch.setattr(
main_module.sys, "exit", lambda code: (_ for _ in ()).throw(SystemExit(code))
@@ -189,8 +195,10 @@ def test_main_dispatch_verify_includes_only_filter(
args = _Args("verify", use_global=True, source=False, output=True)
parser = type("P", (), {"parse_args": lambda self: args})()
- monkeypatch.setattr(main_module, "build_parser", lambda: parser)
- monkeypatch.setattr(main_module, "resolve_targets", lambda _args: tmp_path)
+ monkeypatch.setattr(main_module.CommandLineParser, "build", lambda self: parser)
+ monkeypatch.setattr(
+ main_module.CommandLineParser, "resolve_targets", lambda self, _args: tmp_path
+ )
monkeypatch.setattr(main_module, "CommandLineInterface", lambda templates_root: cli)
monkeypatch.setattr(
main_module.sys, "exit", lambda code: (_ for _ in ()).throw(SystemExit(code))
@@ -217,8 +225,10 @@ def test_main_dispatch_uninstall(self, monkeypatch: pytest.MonkeyPatch, tmp_path
args = _Args("uninstall")
parser = type("P", (), {"parse_args": lambda self: args})()
- monkeypatch.setattr(main_module, "build_parser", lambda: parser)
- monkeypatch.setattr(main_module, "resolve_targets", lambda _args: tmp_path)
+ monkeypatch.setattr(main_module.CommandLineParser, "build", lambda self: parser)
+ monkeypatch.setattr(
+ main_module.CommandLineParser, "resolve_targets", lambda self, _args: tmp_path
+ )
monkeypatch.setattr(main_module, "CommandLineInterface", lambda templates_root: cli)
monkeypatch.setattr(
main_module.sys, "exit", lambda code: (_ for _ in ()).throw(SystemExit(code))
diff --git a/tests/vstack/test_models.py b/tests/vstack/test_models.py
index 607aa20..4c133aa 100644
--- a/tests/vstack/test_models.py
+++ b/tests/vstack/test_models.py
@@ -1,4 +1,4 @@
-"""Utilities and tests for test models."""
+"""Tests for shared validation models."""
from __future__ import annotations