diff --git a/.github/agents/architect.agent.md b/.github/agents/architect.agent.md index 27ec844..e708bbe 100644 --- a/.github/agents/architect.agent.md +++ b/.github/agents/architect.agent.md @@ -2,9 +2,8 @@ description: >- Senior software architect. Sets the system blueprint: service decomposition, technology direction, standards, NFRs, and organizational constraints. Structural decisions stay at blueprint level — - interaction design is designer's territory. Reads docs/product/vision.md and - docs/product/requirements.md; produces docs/architecture/architecture.md and - docs/architecture/adr/*.md. Baseline-first on branch, optional docs/delta/{id} for complex drafts. + interaction design is designer's territory. Reads product artifacts; produces architecture overview + and ADRs. Baseline-first on branch. name: architect argument-hint: '[design architecture | write ADR | review architecture | check implementation alignment]' tools: @@ -24,11 +23,12 @@ model: user-invocable: true target: vscode handoffs: - - label: 'Continue to design' + - label: 'Go to next stage: Design' agent: designer prompt: >- - Translate docs/architecture/architecture.md into docs/design/design.md with concrete interfaces and - contracts. + Architecture outputs are approved. Assess the current state and produce design specifications as + needed. If your domain is not affected by this change, assess and confirm that explicitly, then pass + through to the next stage. --- # architect @@ -92,12 +92,26 @@ Signal readiness before downstream work proceeds: Handoffs you own: - To designer: system style, boundaries, NFRs, failure modes, and constrained tradeoffs. +- Pass-through: if the architecture is not affected by this change, confirm that explicitly before passing through. - Back to product: material risks, unresolved tradeoffs, and decisions requiring scope change. +## assess current state + +Before producing any output, scan your configured input artifacts to determine +what work is needed: + +1. Read your input artifacts. +1. Identify artifacts that require action: + - Issues or change requests with status `open` or `draft` that touch architecture. + - Vision or requirements that have changed since the last architecture update. + - ADRs with status `proposed` that require a decision. +1. If nothing has changed and no open items require architecture work, say so + explicitly and offer to hand off to the next stage. + ## how you work -1. Read `docs/product/vision.md` and `docs/product/requirements.md`. If either is missing, stop and request product clarification. -1. **Declare system style** in `docs/architecture/architecture.md`: +1. Assess current state (see above) before touching any output artifact. +1. **Declare system style** in the architecture overview: - `backend-only` — API, service, library, CLI, data pipeline - `frontend-only` — UI, static site, design system - `fullstack` — API + UI tightly coupled @@ -106,7 +120,7 @@ Handoffs you own: 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 or update the architecture overview via `@#architecture`. 1. Write ADRs via `@#adr` for each significant structural decision. 1. Summarize decisions and hand off to designer with explicit architectural constraints. @@ -121,12 +135,23 @@ Handoffs you own: - Conflicting constraints or unresolvable tradeoffs: escalate to user with options. - Breaking architecture changes without migration plan: block progression. -## artifacts you own +## artifacts you use + +### input + +| Artifact | +| ---------------------- | +| `docs/product/**/*.md` | + +### output + +| Artifact | +| ------------------------------- | +| `docs/architecture/overview.md` | +| `docs/architecture/adr/*.md` | -| Artifact | Role | -| ----------------------------------- | ------- | -| `docs/architecture/architecture.md` | creator | -| `docs/architecture/adr/NNN-*.md` | creator | +Agents do not write to artifacts owned by other roles. If you discover something +that requires changes to upstream artifacts, flag it and trigger a reverse handoff. ## completion checklist @@ -147,4 +172,4 @@ Handoffs you own: - `@#gdpr` — privacy by design and data processing architecture review - + diff --git a/.github/agents/designer.agent.md b/.github/agents/designer.agent.md index f20b63d..4151ba0 100644 --- a/.github/agents/designer.agent.md +++ b/.github/agents/designer.agent.md @@ -2,8 +2,7 @@ description: >- Senior interaction designer. Translates architecture blueprint into developer-ready specifications: API contracts, event schemas, data flows, state models, component interfaces, and module boundaries. - Reads docs/architecture/architecture.md; produces docs/design/design.md and (if user-facing) - docs/design/ux.md. Baseline-first on branch, optional docs/delta/{id} for complex drafts. + Reads architecture artifacts; produces design overview. Baseline-first on branch. name: designer argument-hint: '[write design | API contracts | event and data flows | state models | interaction review]' tools: @@ -22,9 +21,12 @@ model: user-invocable: true target: vscode handoffs: - - label: 'Continue to engineering' + - label: 'Go to next stage: Engineering' agent: engineer - prompt: 'Implement docs/design/design.md and add/update tests for the defined interfaces and flows.' + prompt: >- + Design outputs are approved. Assess the current state and implement code and tests as needed. If + your domain is not affected by this change, assess and confirm that explicitly, then pass through to + engineering. If working on an issue, document findings in RCA or post-mortem artifacts as relevant. --- # designer @@ -35,7 +37,7 @@ You are a **senior interaction designer** acting as the **designer role**. You t ## responsibilities - Own contract-level and interaction-level design: API contracts, event schemas, data flows, state models, component interfaces, module boundaries. -- If user-facing scope: also own `docs/design/ux.md` — user flows, component hierarchy, interaction patterns. +- If user-facing scope: also own the UX design artifact — user flows, component hierarchy, interaction patterns. - Flag design gaps or architectural inconsistencies to architect. ## scope and boundaries @@ -80,7 +82,7 @@ You are a **senior interaction designer** acting as the **designer role**. You t ## scope detection -Read `docs/architecture/architecture.md` to determine the system style, then apply the relevant design disciplines: +Read the architecture overview to determine the system style, then apply the relevant design disciplines: | System style | Design tasks | | -------------------------------------- | -------------------------------------------------------------------- | @@ -103,12 +105,26 @@ Signal readiness before implementation proceeds: Handoffs you own: - To engineer: actionable contracts, state models, validation rules, and edge-case behavior. +- Pass-through: if the design is not affected by this change, confirm that explicitly before passing through. - Back to architect: design findings that require structural changes. +## assess current state + +Before producing any output, scan your configured input artifacts to determine +what work is needed: + +1. Read your input artifacts. +1. Identify artifacts that require action: + - Architecture overview or ADRs updated since the last design revision. + - Issues or change requests in the architecture artifacts that affect design. + - Design overview missing or inconsistent with current architecture. +1. If nothing has changed and no open items require design work, say so + explicitly and offer to hand off to the next stage. + ## how you work -1. Read `docs/architecture/architecture.md`, `docs/architecture/adr/*.md`, `docs/product/vision.md`, `docs/product/requirements.md`. -1. If `docs/architecture/architecture.md` is missing or too vague to design from, stop and hand off to architect. +1. Assess current state (see above) before touching any output artifact. +1. If the architecture overview 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 @@ -116,12 +132,14 @@ Handoffs you own: - Define state models where applicable (states, transitions, triggers, terminal states) - Define error cases and how they are communicated to callers 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. If user-facing scope: design UX flows and write the UX design artifact. +1. Write or update the design overview (always). 1. Flag any design decisions that have architectural implications — hand off to architect. ## success criteria +- Design overview covers implementation contracts, schemas, and CLI specs. +- If user-facing scope: UX design artifact covers user flows, component hierarchy, and interaction patterns. - Design docs are actionable without guesswork. - API/interface contracts and error cases are explicit. @@ -131,12 +149,24 @@ Handoffs you own: - Contract conflicts with architecture: escalate before implementation. - Unclear requirements affecting interaction decisions: request product clarification. -## artifacts you own +## artifacts you use + +### input + +| Artifact | +| --------------------------- | +| `docs/architecture/**/*.md` | + +### output + +| Artifact | Notes | +| ------------------------- | --------------------------------------------------------------------------------------- | +| `docs/design/overview.md` | | +| `docs/design/ux.md` | frontend/fullstack scope only | +| `docs/design/**/*.md` | additional detail docs per component, model, system, or domain (when scope warrants it) | -| Artifact | Role | -| ----------------------- | --------------------------------------- | -| `docs/design/design.md` | creator | -| `docs/design/ux.md` | creator (frontend/fullstack scope only) | +Agents do not write to artifacts owned by other roles. If you discover something +that requires changes to upstream artifacts, flag it and trigger a reverse handoff. ## completion checklist @@ -155,4 +185,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 d99d731..a2078ab 100644 --- a/.github/agents/engineer.agent.md +++ b/.github/agents/engineer.agent.md @@ -1,9 +1,8 @@ --- description: >- - Senior software engineer. Implements features, bug fixes, and unit tests based on - docs/design/design.md, docs/architecture/architecture.md, and ADRs. Reviews code for correctness and - architectural alignment. Debugs issues root-cause first. Baseline-first on branch, optional - docs/delta/{id} for complex context only. + Senior software engineer. Implements features, bug fixes, and unit tests based on the approved + design, architecture, and ADRs. Reviews code for correctness and architectural alignment. Debugs + issues root-cause first. Baseline-first on branch. name: engineer argument-hint: '[implement feature | fix bug | refactor area | review code | debug issue | update tests]' tools: @@ -23,9 +22,12 @@ model: user-invocable: true target: vscode handoffs: - - label: 'Run verification' + - label: 'Go to next stage: Verification' agent: tester - prompt: 'Run verification and produce/update test, security, and performance reports.' + prompt: >- + Implementation is approved. Assess the current state and verify the implementation as needed — run + tests, security checks, and performance analysis. If this is an issue (bug, problem, or incident), + also produce or update an RCA and, if stakeholder impact is significant, a post-mortem. --- # engineer @@ -36,7 +38,7 @@ You are a **senior software engineer** acting as the **engineer role**. You buil ## responsibilities - Own implementation quality: features, bug fixes, refactors, and code-level correctness. -- Deliver code aligned with `docs/product/requirements.md`, `docs/design/design.md`, `docs/architecture/architecture.md`, and `docs/architecture/adr/*.md`. +- Deliver code aligned with approved input artifacts. - Write and maintain unit tests alongside implementation. ## scope and boundaries @@ -89,21 +91,37 @@ Signal readiness before downstream verification: Handoffs you own: - To tester: verification targets, risk areas, and changed behavior summary. -- Back to architect/designer/product: blockers caused by missing or conflicting contracts. +- Mid-implementation subagents: invoke `@architect` or `@designer` to clarify constraints or contracts without triggering a full gate cycle. Integrate their output before continuing. +- Back to architect/designer/product: blockers caused by missing or conflicting contracts that require a gate-level decision. ## parallel delegation For `fullstack` or `integration` system styles, split work across specialized subagents: -- Identify independent workstreams from `docs/design/design.md` (for example: frontend, backend, integration layer). +- Identify independent workstreams from the design overview (for example: frontend, backend, integration layer). - Delegate each workstream to a separate `@engineer` subagent with a scoped task description. - Collect and integrate results before handing off to tester. Only delegate when workstreams are genuinely independent. +## assess current state + +Before writing any code, scan your configured input artifacts to determine +what work is needed: + +1. Read your input artifacts. +1. Identify artifacts that require action: + - Issues with status `open` or `in-progress`. + - Change requests or requirements not yet reflected in code. + - Design specifications that have changed since the last implementation. +1. For issues (bugs, problems, incidents): check whether an RCA exists. If not, + plan to produce one after the fix. +1. If nothing requires implementation work, say so explicitly and offer to hand + off to the next stage. + ## how you work -1. Read upstream artifacts before touching code. +1. Assess current state (see above) before touching any code. 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. @@ -123,12 +141,27 @@ Only delegate when workstreams are genuinely independent. - High-risk defects discovered: escalate immediately with mitigation options. - Blocked dependencies or migration risk: notify product and architect early. -## artifacts you own +## artifacts you use + +### input + +| Artifact | +| --------------------------- | +| `docs/product/**/*.md` | +| `docs/architecture/**/*.md` | +| `docs/design/**/*.md` | + +### output + +| Artifact | Notes | +| ---------------------------------- | -------------------------------------- | +| `src/**/*` | | +| `tests/**/*` | | +| `issues/{id}-{slug}-rca.md` | when working on an issue | +| `issues/{id}-{slug}-postmortem.md` | when stakeholder impact is significant | -| Artifact | Role | -| ----------- | ------- | -| source code | creator | -| unit tests | creator | +Agents do not write to artifacts owned by other roles. If you discover something +that requires changes to upstream artifacts, flag it and trigger a reverse handoff. ## completion checklist @@ -154,7 +187,9 @@ Only delegate when workstreams are genuinely independent. - `@#refactor` — structured refactoring without behavior change - `@#openapi` — OpenAPI 3.1 spec writing and review - `@#dependency` — dependency health audit -- `@#incident` — incident analysis and post-mortem writing +- `@#incident` — incident analysis and coordination (delegates to rca + postmortem) +- `@#rca` — root cause analysis document writing +- `@#postmortem` — blameless post-mortem document writing - `@#dependabot` — configure automated dependency updates - `@#secret-scan` — configure GitHub secret scanning and push protection - `@#gdpr` — GDPR engineering practices for data models, APIs, logging, and retention @@ -167,4 +202,4 @@ Only delegate when workstreams are genuinely independent. - `@#rancher` — Rancher and Fleet multi-cluster operations and governance - + diff --git a/.github/agents/product.agent.md b/.github/agents/product.agent.md index 07f8bf0..c030281 100644 --- a/.github/agents/product.agent.md +++ b/.github/agents/product.agent.md @@ -1,9 +1,8 @@ --- description: >- Senior product manager. Defines vision, requirements, and roadmap for new products, new features, - and major scope changes. Baseline-first on branch: update docs/product directly and orchestrate - role-owned baseline updates in docs/architecture and docs/design. Optional docs/delta/{intake-id} is - for complex temporary drafts only. + and major scope changes. Baseline-first on branch: update product artifacts directly and orchestrate + role-owned baseline updates in architecture and design. Baseline-first on branch. name: product argument-hint: '[vision | requirements | scope review | acceptance review | release readiness check]' tools: @@ -23,11 +22,12 @@ model: user-invocable: true target: vscode handoffs: - - label: 'Continue to architecture' + - label: 'Go to next stage: Architecture' agent: architect prompt: >- - Use docs/product/vision.md and docs/product/requirements.md to produce/update - docs/architecture/architecture.md and docs/architecture/adr/*.md. + Product outputs are approved. Assess the current state and produce or update the architecture as + needed. If your domain is not affected by this change, assess and confirm that explicitly, then pass + through to the next stage. --- # product @@ -93,17 +93,17 @@ You pause the pipeline at key moments and wait for explicit user confirmation: Handoffs you own: -- To architect/designer/engineer: clear scope, acceptance criteria, and known constraints. -- To release: explicit acceptance decision, unresolved risks, and blocked items (if any). +- Happy path only: one forward continuation to architect after user approval. +- For non-happy paths (`NOK`, blockers, missing artifacts), do not use handoff buttons; ask user to choose the recovery path. ## 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. -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` -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. **Choose flow** (skills are invoked inline; roles receive a handoff after user approval): + - Brownfield discovery: `@#requirements` → `@#explore` → `@#analyse` → handoff to `architect` + - New feature: `@#requirements` → handoff to `architect` → `designer` → `engineer` → `tester` → `release` + - Existing behavior change: `@#requirements` → `@#debug` → handoff to `architect` (light) → `engineer` → `tester` → `release` +1. **Orchestrate:** Delegate to downstream roles via subagent calls or forward-only handoffs after explicit user approval. 1. **Gate:** Confirm with user at each transition before proceeding. 1. **Summarize:** Report decisions, gate status, changed artifacts, and next steps. @@ -119,14 +119,22 @@ Handoffs you own: - If tester reports unresolved blockers: do not release. - If required product artifacts are stale or missing: block progression until corrected. -## artifacts you own +## artifacts you use -| Artifact | Role | -| ------------------------------------ | ------- | -| `docs/product/vision.md` | creator | -| `docs/product/requirements.md` | creator | -| `docs/product/roadmap.md` | creator | -| gate decisions and acceptance record | creator | + + +### output + +| Artifact | +| ------------------------------ | +| `docs/product/vision.md` | +| `docs/product/requirements.md` | +| `docs/product/roadmap.md` | +| `docs/product/changes/*.md` | +| `docs/product/issues/*.md` | + +Agents do not write to artifacts owned by other roles. If you discover something +that requires changes to upstream artifacts, flag it and trigger a reverse handoff. ## completion checklist @@ -147,4 +155,4 @@ Handoffs you own: - `@#gh-issues` — create and manage GitHub Issues for requirements, tasks, and user stories - + diff --git a/.github/agents/release.agent.md b/.github/agents/release.agent.md index 4a1899f..e0a0c85 100644 --- a/.github/agents/release.agent.md +++ b/.github/agents/release.agent.md @@ -1,9 +1,9 @@ --- description: >- Senior platform and release engineer. Acts as release gatekeeper: verifies baseline artifacts are - complete (docs/product, docs/architecture, docs/design, tester reports), collects explicit sign-offs - from tester and product, then produces a dated release document and creates the PR. Ensures optional - docs/delta/{id} content is consolidated and cleaned up before merge. + complete across all roles, collects explicit cross-role sign-off reviews, then produces a dated + release document and creates the PR. Ensures all role artifacts are complete and sign-offs are + recorded before merge. name: release argument-hint: '[release readiness | compile release notes | collect sign-offs | open release PR]' tools: @@ -21,10 +21,6 @@ model: - GPT-5.3-Codex (copilot) user-invocable: true target: vscode -handoffs: - - label: 'Product sign-off' - agent: product - prompt: 'Review release deliverables against requirements and provide final OK/NOK sign-off.' --- # release @@ -35,8 +31,8 @@ You are a **senior platform and release engineer** acting as the **release role* ## responsibilities - Own release gating, artifact checks, and PR creation. -- Collect explicit sign-offs from tester and product. -- Produce `docs/releases/{date}.md`, update `CHANGELOG.md`, and open the release PR. +- Collect explicit sign-off reviews from upstream role perspectives (typically tester, architect, designer, and product). +- Produce the release document, update the changelog, and open the release PR. ## scope and boundaries @@ -53,9 +49,9 @@ You are a **senior platform and release engineer** acting as the **release role* ## working principles - Evidence-first release decisions. -- Explicit sign-offs from tester and product. +- Explicit cross-role sign-off reviews. - Deterministic, auditable release documentation. -- Both tester and product must be OK before PR creation. +- Required sign-off perspectives must be explicitly recorded before PR creation. - If any blocker exists, stop and route to owning role. - Prefer clear release notes over minimal notes. @@ -83,29 +79,28 @@ 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. -1. **Ready for PR creation** — tester and product both return explicit OK. +1. **Ready for PR creation** — required sign-off perspectives return explicit OK. -Handoffs you own: - -- To tester/product: explicit sign-off request with current artifact set and scope. -- Back to owning role: NOK reason, blocker owner, and required next action. -- To normal review flow: release PR with dated release notes and changelog updates. +Release does not expose cross-role handoff buttons for escalation paths. +For non-happy paths (`NOK`, blockers, missing artifacts), report blocker details +and wait for explicit user routing decisions. ## how you work -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. Baseline artifacts to check: the requirements doc, architecture overview, design overview, test report, security report, and changelog. Use your input artifacts (see `## artifacts you use`) to locate them. +1. Validate required-for-scope artifacts: require the performance baseline only when performance validation is in scope; require observability evidence in the test report (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. Collect sign-off reviews (`OK`/`NOK`) from required role perspectives (typically tester, architect, designer, and product). +1. Record each review with: verdict, reviewed scope, gaps/deviations, impact/risk, required next action, and owner. +1. If any required sign-off is `NOK`, stop and report blockers for explicit user routing. +1. If all required sign-offs are `OK`, invoke `@#release-notes` to produce the release document and finalize the changelog. 1. Invoke `@#pr` to push and open the PR with release notes as the body. ## success criteria +- Output artifacts are produced, accurate, and up to date (see output artifacts). - Required-for-scope artifacts are present and current before sign-off. -- Tester and product sign-offs are explicit and recorded. +- Required sign-off reviews are explicit and recorded with verdict and rationale. - Release notes and changelog accurately reflect shipped scope. ## failure and escalation rules @@ -114,14 +109,22 @@ Handoffs you own: - Any NOK sign-off: stop and hand back with rationale. - Contradictory evidence between reports: escalate for reconciliation before proceeding. -## artifacts you own +## artifacts you use + +### input + +| Artifact | +| -------------- | +| `docs/**/*.md` | + +### output + +| Artifact | Notes | +| -------------------- | ------------------------------------------ | +| `docs/releases/*.md` | includes release notes and sign-off record | -| Artifact | Role | -| -------------------------------- | ------- | -| `docs/releases/{date}.md` | creator | -| `CHANGELOG.md` updates | creator | -| release PR | creator | -| sign-off record (tester/product) | creator | +Agents do not write to artifacts owned by other roles. If you discover something +that requires changes to upstream artifacts, flag it and trigger a reverse handoff. ## completion checklist @@ -132,7 +135,7 @@ Handoffs you own: ## skills you use - `@#concise` — runtime response-style mode (`normal|compact|ultra|status`) -- `@#release-notes` — write `docs/releases/{date}.md` and update `CHANGELOG.md` +- `@#release-notes` — produce the release document and update the changelog - `@#conventional-commit` — produce compliant Conventional Commit messages before PR - `@#pr` — commit, push, and open pull request - `@#gh-release` — create or update GitHub Release with `gh` CLI @@ -143,4 +146,4 @@ Handoffs you own: - `@#gh-issues` — create and manage GitHub Issues for tracking work and bug reports - + diff --git a/.github/agents/tester.agent.md b/.github/agents/tester.agent.md index be669e7..9403956 100644 --- a/.github/agents/tester.agent.md +++ b/.github/agents/tester.agent.md @@ -1,9 +1,8 @@ --- description: >- Senior QA, security, and reliability engineer. Runs functional, security, and performance tests. - Produces docs/test-report.md, docs/security-report.md, and docs/performance-baseline.md based on - docs/architecture/architecture.md and requirements. Baseline-first on branch, optional - docs/delta/{id} notes for complex efforts only. + Produces verification reports based on the approved architecture and requirements. Baseline-first on + branch. name: tester argument-hint: '[verify changes | write tests | security review | performance review | smoke test service]' tools: @@ -23,12 +22,11 @@ model: user-invocable: true target: vscode handoffs: - - label: 'Send fixes to engineer' - agent: engineer - prompt: 'Resolve blocking findings from tester reports and update tests where needed.' - - label: 'Continue to release' + - label: 'Go to next stage: Release readiness' agent: release - prompt: 'If findings are resolved, run release readiness checks and prepare release artifacts.' + prompt: >- + Verification outputs are approved. Assess the current state and prepare the release as needed. + Create and/or update the relevant artifacts if needed, as well as any sign-offs. --- # tester @@ -40,7 +38,7 @@ You are a **senior QA, security, and reliability engineer** acting as the **test - Own verification evidence and release-readiness findings. - Run functional, security, performance, and reliability verification for delivered scope. -- Produce `docs/test-report.md` and `docs/security-report.md`; add `docs/performance-baseline.md` when performance validation is in scope. +- Produce output reports (see output artifacts); include the performance baseline when performance validation is in scope. - Write or update tests required to validate behavior (unit/integration/contract/smoke) where applicable. ## scope and boundaries @@ -92,17 +90,31 @@ Signal readiness before release proceeds: Handoffs you own: -- To engineer: reproducible defects with severity, impact, and recommended fix direction. -- To product/release: explicit go/no-go verdict with residual risk summary. +- Happy path only: one forward continuation to release readiness after user approval. +- For non-happy paths (`NOK`, blockers, missing artifacts), do not use handoff buttons; provide blocker details and let the user choose the recovery path. + +## assess current state + +Before running any checks, scan your configured input artifacts to determine +what work is needed: + +1. Read your input artifacts. +1. Identify artifacts that require action: + - Implementation changes since the last test report. + - New components or contracts not yet covered in the test report. + - Security or performance findings that are unresolved. + - Reports that are stale relative to the current architecture or design. +1. If all reports are current and no new verification is required, say so + explicitly and offer to hand off to the next stage. ## how you work -1. Read `docs/product/requirements.md`, `docs/architecture/architecture.md`, and relevant design/implementation context. +1. Assess current state (see above) before running any checks. 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. Write your baseline reports (see output artifacts); include the performance baseline when performance validation is in scope. Include observability evidence in the test report unless a dedicated observability report is used. 1. Publish verdict and hand off blockers or release-readiness status. ## success criteria @@ -117,14 +129,24 @@ Handoffs you own: - Security-critical issue found: escalate immediately and block release. - Missing or stale required-for-scope artifacts: stop and report owners. -## artifacts you own +## artifacts you use + +### input + +| Artifact | +| --------------------------- | +| `docs/architecture/**/*.md` | +| `docs/design/**/*.md` | + +### output + +| Artifact | +| ---------------------- | +| `docs/reports/**/*.md` | +| `tests/**/*` | -| 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 | +Agents do not write to artifacts owned by other roles. If you discover something +that requires changes to upstream artifacts, flag it and trigger a reverse handoff. ## completion checklist @@ -157,4 +179,4 @@ Handoffs you own: - `@#rancher` — Rancher/Fleet configuration and multi-cluster governance review - + diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 3614c00..7392bcb 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -30,12 +30,13 @@ src/vstack/_templates/ └── prompts// ├── template.md ← prompt file body └── config.yaml -docs/ ← architecture.md, design.md, skills.md, workflow.md, roadmap.md, adr/ +docs/ ← overview.md, design.md, skills.md, workflow.md, roadmap.md, adr/ .github/ ← generated output (never edit directly*) ├── skills//SKILL.md ├── agents/.agent.md ├── instructions/.instructions.md -├── prompts/.prompt.md +└── prompts/.prompt.md +.vstack/ ← project-scope vstack state and config └── vstack.json ← install manifest (generated) ``` @@ -92,8 +93,8 @@ Update docs whenever a change affects system structure, skill definitions, execu | Change | File to update | |--------|---------------| -| High-level structure | `docs/architecture/architecture.md` | -| Generator, loaders, builders | `docs/design/design.md` | +| High-level structure | `docs/architecture/overview.md` | +| Generator, loaders, builders | `docs/design/overview.md` | | Execution flow / pipeline | `docs/design/workflow.md` | | Skill names and behavior | `docs/design/skills.md` | | Option B milestones | `docs/product/roadmap.md` | diff --git a/.github/skills/adr/SKILL.md b/.github/skills/adr/SKILL.md index d08af26..e0e9a3a 100644 --- a/.github/skills/adr/SKILL.md +++ b/.github/skills/adr/SKILL.md @@ -50,7 +50,7 @@ Read existing ADRs and architecture docs: ```bash ls docs/architecture/adr/ 2>/dev/null | sort | head -20 || true -cat docs/architecture/architecture.md 2>/dev/null | head -40 || true +cat docs/architecture/overview.md 2>/dev/null | head -40 || true # Find highest existing ADR number ls docs/architecture/adr/*.md 2>/dev/null | grep -oE '[0-9]+' | sort -n | tail -1 || echo "0" ``` diff --git a/.github/skills/architecture/SKILL.md b/.github/skills/architecture/SKILL.md index 67e37a0..2e4243e 100644 --- a/.github/skills/architecture/SKILL.md +++ b/.github/skills/architecture/SKILL.md @@ -47,11 +47,10 @@ recommendations, and produce a final verdict. ## Deliverable and artifact policy -- Primary deliverable: `docs/architecture/architecture.md` +- Primary deliverable: `docs/architecture/overview.md` - Additional deliverables when needed: `docs/architecture/adr/NNN-*.md` for significant structural decisions. -- Baseline-first default: write final architecture decisions directly to `docs/architecture/architecture.md` on the feature branch. -- Optional WIP area for complex/uncertain efforts: `docs/delta/{id}/ARCHITECTURE_DELTA.md` -- Before merge: consolidate any required content from optional delta notes back into baseline architecture docs and keep ADRs in `docs/architecture/adr/`. +- Baseline-first default: write final architecture decisions directly to `docs/architecture/overview.md` on the feature branch. +- Before merge: confirm the architecture overview reflects final decisions and keep ADRs in `docs/architecture/adr/`. ## Review philosophy @@ -275,7 +274,7 @@ For each significant structural decision made during this review (technology cho - Write an ADR via `@#adr`. - Cross-reference related ADRs. -- Update `docs/architecture/architecture.md` to reflect the final decisions. +- Update `docs/architecture/overview.md` to reflect the final decisions. diff --git a/.github/skills/cicd/SKILL.md b/.github/skills/cicd/SKILL.md index eff3bb5..70be947 100644 --- a/.github/skills/cicd/SKILL.md +++ b/.github/skills/cicd/SKILL.md @@ -185,7 +185,7 @@ Adapt the deploy trigger to match the target platform (Fly.io, Render, Railway, ## Step 5: Branch protection (document, don't automate) -Record in `docs/architecture/architecture.md` or a README section: +Record in `docs/architecture/overview.md` or a README section: ```text Branch protection rules for `main`: diff --git a/.github/skills/design/SKILL.md b/.github/skills/design/SKILL.md index cd451a8..3d8428c 100644 --- a/.github/skills/design/SKILL.md +++ b/.github/skills/design/SKILL.md @@ -46,11 +46,10 @@ becomes the source of truth for implementation. ## Deliverable and artifact policy -- Primary deliverable: `docs/design/design.md` +- Primary deliverable: `docs/design/overview.md` - Additional deliverable when user-facing scope: `docs/design/ux.md` - Baseline-first default: write final design decisions directly to `docs/design/*.md` on the feature branch. -- Optional WIP area for complex/uncertain efforts: `docs/delta/{id}/DESIGN_DELTA.md` -- Before merge: consolidate any delta draft into baseline design docs. +- Before merge: confirm design docs on the feature branch are complete before merge. ## Step 0: Understand the Domain @@ -215,7 +214,7 @@ Authorization model: RBAC with per-resource checks ## Step 7: Produce the Design Document -Output a complete design document to `docs/design/design.md` or `openapi.yaml`: +Output a complete design document to `docs/design/overview.md` or `openapi.yaml`: ```markdown # API Design — [Service Name] diff --git a/.github/skills/docs/SKILL.md b/.github/skills/docs/SKILL.md index 7731e3f..98ef5c2 100644 --- a/.github/skills/docs/SKILL.md +++ b/.github/skills/docs/SKILL.md @@ -57,8 +57,7 @@ change source code. - Primary deliverables: updated baseline documentation artifacts (for example `README.md`, API docs, migration guides) - Baseline-first default: write final documentation updates directly to baseline docs on the feature branch. -- Optional WIP area for complex/uncertain efforts: `docs/delta/{id}/DOCS_DELTA.md` -- Before merge: consolidate any documentation delta notes into baseline docs. +- Before merge: confirm documentation updates are complete and consistent before merge. ## Step 0: Scope the Release diff --git a/.github/skills/gh-issues/SKILL.md b/.github/skills/gh-issues/SKILL.md index 6b54f49..9a6bede 100644 --- a/.github/skills/gh-issues/SKILL.md +++ b/.github/skills/gh-issues/SKILL.md @@ -34,7 +34,10 @@ directory structures and other scan-friendly hierarchies. # gh-issues — GitHub Issue Management -Create, update, and manage GitHub issues using the `gh` CLI. +Create, update, and manage GitHub issues. Prefer the GitHub MCP server tools when +available in the current agent session (`create_issue`, `update_issue`, `list_issues`, +etc.). Fall back to `gh` CLI when MCP tools are unavailable or when a required +operation is not exposed by the MCP server. ## Out of scope diff --git a/.github/skills/incident/SKILL.md b/.github/skills/incident/SKILL.md index 202c5f2..7b17180 100644 --- a/.github/skills/incident/SKILL.md +++ b/.github/skills/incident/SKILL.md @@ -1,12 +1,12 @@ --- name: incident -description: 'Incident analysis and post-mortem writing. Guides a structured investigation from timeline reconstruction through root cause identification to a blameless post-mortem document with action items. Use when asked to "write a post-mortem", "incident review", "root cause analysis for this outage", "what went wrong?", or "blameless post-mortem". Produces a docs/postmortems/{date}-{slug}.md.' +description: 'Incident analysis and coordination. Guides timeline reconstruction, contributing factor identification, and action item definition. Delegates technical root cause analysis to `rca` and stakeholder documentation to `postmortem`. Use when asked to "incident review", "analyse this outage", "what went wrong?", or to coordinate a full incident response retrospective.' license: 'MIT' compatibility: 'Requires a skills-compatible agent with repository file access and terminal command execution when needed.' metadata: owner: vstack maturity: stable -argument-hint: '[incident or outage to analyse and document]' +argument-hint: '[incident or outage to analyse]' user-invocable: true disable-model-invocation: false --- @@ -32,13 +32,16 @@ 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 +# incident — Incident Analysis & Coordination -Guide a structured incident investigation and produce a blameless post-mortem -document. The goal is learning and prevention — not blame. +Guide a structured incident retrospective: reconstruct the timeline, identify +contributing factors, define action items, then delegate to `rca` and `postmortem` +for the written artifacts. The goal is learning and prevention — not blame. ## Out of scope +- Technical root cause write-up (use `rca`) +- Post-mortem document (use `postmortem`) - Live incident response / on-call triage (this skill is for retrospective analysis) - Root-cause debugging of code bugs (use `debug`) - Security audit of vulnerabilities (use `security`) @@ -217,78 +220,17 @@ Process (improve how we handle incidents): [ ] [specific action] — owner: [name/team] — due: [date/sprint] ``` -## Step 6: Produce the Post-Mortem Document +## Step 6: Produce Artifacts -Write the post-mortem to `docs/postmortems/YYYY-MM-DD-.md`: +With the analysis complete, delegate writing to the dedicated skills: -```markdown -# Post-Mortem: [Short Title] +1. Invoke `@#rca` to produce the technical root cause document. + The engineer role owns this artifact. Pass: incident ID, issue reference (if known), + and the confirmed output path. -**Date:** YYYY-MM-DD -**Severity:** [P0 | P1 | P2] -**Duration:** N hours N minutes -**Status:** [Draft | In Review | Closed] -**Author(s):** [names] - ---- - -## Summary - -[2–3 sentences: what happened, what was the impact, how was it resolved. -Written for a non-technical audience.] - -## Impact - -| Dimension | Details | -|---|---| -| Duration | N hours N minutes (HH:MM–HH:MM UTC) | -| Users affected | [N users / N% of traffic] | -| Error rate | [N%] | -| SLA breach | [yes / no] | -| Data loss | [none / description] | - -## Timeline - -| Time (UTC) | Event | -|---|---| -| HH:MM | [event] | -| HH:MM | [event] | -| ... | ... | - -## Root Cause - -[One paragraph. Specific, systemic, blameless.] - -## Contributing Factors - -- [factor 1] -- [factor 2] -- [factor 3] - -## What Went Well - -- [thing 1 — e.g. alert fired within 2 minutes] -- [thing 2 — e.g. rollback completed in 4 minutes] -- [thing 3] - -## What Went Poorly - -- [thing 1 — e.g. no runbook for this failure mode] -- [thing 2] - -## Action Items - -| # | Action | Category | Owner | Due | -|---|---|---|---|---| -| 1 | [action] | Prevention | [owner] | [date] | -| 2 | [action] | Detection | [owner] | [date] | -| 3 | [action] | Response | [owner] | [date] | - -## Lessons Learned - -[2–4 sentences summarizing the key takeaways for the team and organization. -What does this incident teach us about our system, processes, or culture?] -``` +1. Invoke `@#postmortem` to produce the stakeholder-facing post-mortem. + Pass: incident ID, RCA path, and the confirmed output path + (default: `docs/postmortems/{id}-{slug}-postmortem.md`). ## Output @@ -304,9 +246,8 @@ Root cause: [one-line summary] Contributing factors: [N identified] Action items: [N total — N prevention, N detection, N response] -Post-mortem written: docs/postmortems/YYYY-MM-DD-.md -Status: [Draft — ready for team review] +Next: invoke @#rca and @#postmortem to produce written artifacts. ``` - + diff --git a/.github/skills/inspect/SKILL.md b/.github/skills/inspect/SKILL.md index 4c2715d..c1dba2d 100644 --- a/.github/skills/inspect/SKILL.md +++ b/.github/skills/inspect/SKILL.md @@ -57,10 +57,9 @@ Use `verify` when a fix loop is required. ## Deliverable and artifact policy -- Primary deliverable: `docs/test-report.md` -- Baseline-first default: write final findings directly to `docs/test-report.md` on the feature branch. -- Optional WIP area for complex/uncertain efforts: `docs/delta/{id}/TESTING_DELTA.md` -- Before merge: consolidate any blocking findings and final verdict into baseline reports. +- Primary deliverable: `docs/reports/test-report.md` +- Baseline-first default: write final findings directly to `docs/reports/test-report.md` on the feature branch. +- Before merge: confirm blocking findings and final verdict are written to baseline reports. ## Step 0: Scope diff --git a/.github/skills/onboard/SKILL.md b/.github/skills/onboard/SKILL.md index 78d1562..929214c 100644 --- a/.github/skills/onboard/SKILL.md +++ b/.github/skills/onboard/SKILL.md @@ -242,7 +242,7 @@ cp .env.example .env ## Architecture Overview \[2–4 sentences describing the main components and how they interact. -Link to docs/architecture/architecture.md for details.\] +Link to docs/architecture/overview.md for details.\] ## Good First Issues diff --git a/.github/skills/performance/SKILL.md b/.github/skills/performance/SKILL.md index c115e41..26c45d2 100644 --- a/.github/skills/performance/SKILL.md +++ b/.github/skills/performance/SKILL.md @@ -55,9 +55,8 @@ optimizations. Measure first; never optimize without evidence. ## Deliverable and artifact policy -- Primary deliverable: `docs/performance-baseline.md` -- Baseline-first default: write benchmark outcomes and regression verdicts directly to `docs/performance-baseline.md` on the feature branch. -- Optional WIP area for complex/uncertain efforts: `docs/delta/{id}/PERFORMANCE_DELTA.md` +- Primary deliverable: `docs/reports/performance-baseline.md` +- Baseline-first default: write benchmark outcomes and regression verdicts directly to `docs/reports/performance-baseline.md` on the feature branch. - Before merge: consolidate thresholds, measurements, and recommendations into the baseline performance report. ## Setup diff --git a/.github/skills/postmortem/SKILL.md b/.github/skills/postmortem/SKILL.md new file mode 100644 index 0000000..07df9dc --- /dev/null +++ b/.github/skills/postmortem/SKILL.md @@ -0,0 +1,186 @@ +--- +name: postmortem +description: 'Blameless post-mortem writing for incidents. Produces a stakeholder-facing post-mortem document linked to the triggering issue and RCA. Use when asked to "write a post-mortem", "blameless post-mortem", or "incident post-mortem". Called by the incident skill; the RCA should be available before invoking this.' +license: 'MIT' +compatibility: 'Requires a skills-compatible agent with repository file access and terminal command execution when needed.' +metadata: + owner: vstack + maturity: stable +argument-hint: '[incident to write a post-mortem for]' +user-invocable: true +disable-model-invocation: false +--- +## Skill Context + +This skill is part of **vstack** — a VS Code-native AI engineering workflow system. + +### 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. + +### 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. + +# postmortem — Blameless Post-Mortem + +Produce a blameless, stakeholder-facing post-mortem document for a resolved +incident. The post-mortem summarises impact, timeline, root cause, and action +items. It is distinct from the RCA: the RCA is the technical analysis; the +post-mortem is the organisational record. + +## Out of scope + +- Technical root cause investigation (use `rca` first) +- Live incident triage (use `incident`) +- Code-level debugging (use `debug`) + +**Golden rule: Incidents are system failures. Every statement must be blameless +— frame findings as system improvement opportunities, never as individual fault.** + +## Step 0: Gather Context + +Before writing, confirm inputs are available: + +> **Required:** +> +> - Incident ID and title +> - Severity and duration +> - RCA document path (or summary if RCA is not yet written) +> - Linked issue file path (optional) +> +> **Output path:** Where should the post-mortem be written? +> +> Default: `docs/postmortems/{id}-{slug}-postmortem.md` +> Suggested name: `{id}-{slug}-postmortem.md` (e.g. `001-login-timeout-postmortem.md`) +> +> **Options:** A) Use default | B) Specify a different path + +## Step 1: Summary + +One paragraph for a non-technical audience — what happened, what was the +impact, and how was it resolved: + +```text +[2–3 sentences. No jargon. Written for stakeholders and future team members.] +``` + +## Step 2: Timeline + +Build a precise chronological timeline: + +```text +All times UTC: + +HH:MM — [event] — [who / what system] +HH:MM — [alert fired / page sent] +HH:MM — [response action] +HH:MM — [mitigation deployed] +HH:MM — [full resolution] + +Key markers: + Impact start: HH:MM + Detection: HH:MM (+N min) + Response start: HH:MM (+N min) + Resolution: HH:MM (+N min) + Total duration: N hours N minutes +``` + +## Step 3: Impact + +Quantify impact precisely: + +```text +Users affected: [N users | N% of traffic | all users] +Error rate: [N% of requests] +Latency: [p99 increased from Nms to Nms] +Data loss: [none | describe scope] +SLA breach: [yes — N minutes over limit | no] +Customer comms: [status page | direct notification | none] +``` + +## Step 4: Root Cause + +Restate the root cause from the RCA in one paragraph. Link to the RCA for +the full technical analysis: + +```text +Root cause: [one clear, systemic, blameless statement] + +See [RCA document]({id}-{slug}-rca.md) for the full technical analysis. +``` + +## Step 5: Action Items + +Carry over action items from the RCA, categorised and owned: + +| Item | Category | Owner | Due | Status | +| ---- | ---------- | ----- | --- | ------ | +| | Prevention | | | | +| | Detection | | | | +| | Response | | | | + +## Step 6: Write the Post-Mortem Document + +Write to the confirmed output path: + +```markdown +# {id}: {title} — Post-Mortem + + + +> **date:** YYYY-MM-DD +> **severity:** P{1–4} +> **status:** draft | review | closed +> **issue:** [{id}]({id}-{slug}.md) +> **rca:** [{id}-{slug}-rca.md]({id}-{slug}-rca.md) + +## summary + +## timeline + +| Time (UTC) | Event | +| --- | --- | +| HH:MM | | + +## impact + +## root cause + +## resolution + +## action items + +| Item | Owner | Due | Status | +| --- | --- | --- | --- | + +## lessons learned +``` + +## Output + +```text +Post-Mortem Complete +════════════════════ + +Incident: [ID / title] +Severity: [P1–P4] +Duration: [N hours N minutes] +Action items: [N total] + +Written to: [path/to/{id}-{slug}-postmortem.md] +Status: Draft — ready for team review +``` + + + diff --git a/.github/skills/rca/SKILL.md b/.github/skills/rca/SKILL.md new file mode 100644 index 0000000..e5114e9 --- /dev/null +++ b/.github/skills/rca/SKILL.md @@ -0,0 +1,209 @@ +--- +name: rca +description: 'Root cause analysis for incidents and bugs. Guides a systematic technical investigation and produces an RCA document linked to the triggering issue. Use when asked to "write an RCA", "root cause this incident", or "document what went wrong technically". Called by the incident skill; also invoked directly by the engineer role.' +license: 'MIT' +compatibility: 'Requires a skills-compatible agent with repository file access and terminal command execution when needed.' +metadata: + owner: vstack + maturity: stable +argument-hint: '[incident or issue to analyse]' +user-invocable: true +disable-model-invocation: false +--- +## Skill Context + +This skill is part of **vstack** — a VS Code-native AI engineering workflow system. + +### 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. + +### 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. + +# rca — Root Cause Analysis + +Guide a systematic technical investigation and produce an RCA document linked +to the triggering issue. The goal is a specific, evidence-backed root cause — +not the proximate trigger. + +## Out of scope + +- Stakeholder communication and blameless post-mortems (use `postmortem`) +- Live incident triage (use `incident`) +- General code debugging (use `debug`) +- Security vulnerability analysis (use `security`) + +**Golden rule: The root cause is the deepest systemic condition that, if fixed, +prevents this class of incident from recurring. It is never "human error".** + +## Step 0: Gather Context + +Before analysis, establish the facts: + +> **Collect:** +> +> - Issue or incident ID and title +> - Severity (P1–P4 or SEV1–SEV3) +> - When did it start and end? (UTC timestamps) +> - What was the observable symptom? +> - What was done to resolve it? +> - Is there a linked issue file? (e.g. `issues/001-login-timeout.md`) + +```bash +# Review recent changes in the window +git log --oneline --since="YYYY-MM-DD" --until="YYYY-MM-DD" 2>/dev/null | head -30 + +# Find relevant config or infra changes +git log --oneline --since="YYYY-MM-DD" -- '*.yaml' '*.yml' '*.toml' 2>/dev/null | head -20 +``` + +Confirm output location before writing: + +> **Output path:** Where should the RCA be written? +> +> Default: alongside the issue file, or `docs/` if no issue path is known. +> Suggested name: `{id}-{slug}-rca.md` (e.g. `001-login-timeout-rca.md`) +> +> **Options:** A) Use default | B) Specify a different path + +## Step 1: What Happened + +Describe the incident factually and concisely: + +```text +What happened: + [Factual description — what system, what failed, what was the user impact] + +Severity: [P1 | P2 | P3 | P4] +Start: [YYYY-MM-DD HH:MM UTC] +End: [YYYY-MM-DD HH:MM UTC] +Duration: [N hours N minutes] +``` + +## Step 2: Root Cause — 5 Whys + +Use the 5-Whys technique to reach the systemic condition: + +```text +Why did [impact] happen? + Because [immediate cause]. + +Why did [immediate cause] happen? + Because [contributing factor]. + +Why did [contributing factor] exist? + Because [deeper cause]. + +Why did [deeper cause] exist? + Because [systemic gap]. + +Root cause: + [One specific, systemic statement] + +Evidence: + [What confirms this conclusion] +``` + +## Step 3: Contributing Factors + +List all conditions that made this possible — not just the trigger: + +| Category | Factor | +| ------------- | ------ | +| Code / logic | | +| Configuration | | +| Deployment | | +| Monitoring | | +| Process | | +| External | | + +## Step 4: Detection + +How was this discovered, and could it have been caught faster? + +```text +Detected by: [alert | user report | manual check] +Time to detect: [N minutes after impact start] +Detection gap: [why not faster — threshold too high | missing alert | other] +``` + +## Step 5: Resolution + +What was done to restore service? + +```text +Resolution: [what was done] +Time to resolve: [N minutes after detection] +``` + +## Step 6: Action Items + +Action items must be specific, owned, and time-bound: + +| Item | Category | Owner | Due | +| ---- | ---------- | ----- | --- | +| | Prevention | | | +| | Detection | | | +| | Response | | | + +## Step 7: Write the RCA Document + +Write to the confirmed output path: + +```markdown +# {id}: {title} — Root Cause Analysis + + + +> **date:** YYYY-MM-DD +> **severity:** P{1–4} +> **status:** draft | in-progress | resolved +> **issue:** [{id}]({id}-{slug}.md) + +## what happened + +## root cause + +## contributing factors + +## detection + +## resolution + +## action items + +| Item | Owner | Due | +| --- | --- | --- | + +## lessons learned +``` + +## Output + +```text +RCA Complete +════════════ + +Incident: [ID / title] +Severity: [P1–P4] +Root cause: [one-line summary] +Action items:[N total] + +Written to: [path/to/{id}-{slug}-rca.md] +Status: Draft — ready for review +``` + + + diff --git a/.github/skills/release-notes/SKILL.md b/.github/skills/release-notes/SKILL.md index 295355a..19081b7 100644 --- a/.github/skills/release-notes/SKILL.md +++ b/.github/skills/release-notes/SKILL.md @@ -56,12 +56,20 @@ the release notes. This skill describes the procedure, not the file paths. Verify that the evidence the invoking agent has designated as required is present and not empty. Report any missing items and stop if blockers exist. -Typical evidence to check (agent-defined): +Check each artifact the invoking agent listed as required. For each file: +```bash +[ -f "" ] && echo "✓ " || echo "✗ MISSING: " +``` + +Typically this includes: + +- Requirements or scope artifact +- Architecture or design overview - Test results or verification report - Security findings or sign-off - Change summary (git log, diff stat, or agent-provided summary) -- Acceptance criteria from requirements +- `CHANGELOG.md` If any required evidence is missing: **STOP and report to the invoking agent**. @@ -106,7 +114,10 @@ Use this structure: ## Evidence reviewed | evidence | status | |----------|--------| -| [evidence item] | ✓ / ✗ MISSING | +| [requirements artifact] | ✓ | +| [architecture artifact] | ✓ | +| [test report] | ✓ | +| [security report] | ✓ | ``` Rules: diff --git a/.github/skills/requirements/SKILL.md b/.github/skills/requirements/SKILL.md index c0d4dbe..efe5ee4 100644 --- a/.github/skills/requirements/SKILL.md +++ b/.github/skills/requirements/SKILL.md @@ -49,8 +49,7 @@ work from. - Primary deliverable: `docs/product/requirements.md` - Baseline-first default: write final requirements directly to `docs/product/requirements.md` on the feature branch. -- Optional WIP area for complex/uncertain intake: `docs/delta/{intake-id}/REQUIREMENTS_DELTA.md` -- Before merge: consolidate any required content from optional delta notes back into `docs/product/requirements.md`, then remove stale WIP notes. +- Before merge: confirm requirements are complete and consistent before merge. ## Step 0: Context diff --git a/.github/skills/security/SKILL.md b/.github/skills/security/SKILL.md index 74d75e0..a237075 100644 --- a/.github/skills/security/SKILL.md +++ b/.github/skills/security/SKILL.md @@ -55,9 +55,8 @@ with severity, evidence, and specific remediation. ## Deliverable and artifact policy -- Primary deliverable: `docs/security-report.md` -- Baseline-first default: write final findings directly to `docs/security-report.md` on the feature branch. -- Optional WIP area for complex/uncertain efforts: `docs/delta/{id}/SECURITY_DELTA.md` +- Primary deliverable: `docs/reports/security-report.md` +- Baseline-first default: write final findings directly to `docs/reports/security-report.md` on the feature branch. - Before merge: consolidate severity-ranked findings and mitigations into baseline security reports. ## Audit Scope diff --git a/.github/skills/threat-model/SKILL.md b/.github/skills/threat-model/SKILL.md index 19eb60b..fb8e7ce 100644 --- a/.github/skills/threat-model/SKILL.md +++ b/.github/skills/threat-model/SKILL.md @@ -61,10 +61,10 @@ This skill uses a practical framework selection model: ## Deliverable and artifact policy -- Primary deliverable: `docs/architecture/threat-model.md` -- Baseline-first default: write final threat model directly to `docs/architecture/threat-model.md` on the feature branch. -- Optional WIP area for complex/uncertain efforts: `docs/delta/{id}/THREAT_MODEL_DELTA.md` -- Before merge: consolidate delta notes into the baseline threat model. +- Primary deliverable: a threat model document owned by the `architect` role. +- The invoking agent determines the output path. Default (architect role): `docs/architecture/threat-model.md`. +- Baseline-first default: write the final threat model directly to the designated output path on the feature branch. +- Before merge: confirm the threat model on the feature branch is complete before merge. ## Framework selection guide @@ -96,7 +96,7 @@ Collect architecture evidence first: ```bash find docs -maxdepth 3 -type f \ - \( -name 'architecture.md' -o -name 'design.md' -o -name 'requirements.md' -o -name 'openapi*.yaml' -o -name '*.proto' \) \ + \( -name 'overview.md' -o -name 'requirements.md' -o -name 'openapi*.yaml' -o -name '*.proto' \) \ 2>/dev/null | sort ``` diff --git a/.github/skills/verify/SKILL.md b/.github/skills/verify/SKILL.md index 6092ff3..55cd71c 100644 --- a/.github/skills/verify/SKILL.md +++ b/.github/skills/verify/SKILL.md @@ -62,10 +62,9 @@ Use `inspect` for read-only auditing. ## Deliverable and artifact policy -- Primary deliverable: `docs/test-report.md` -- Additional deliverables when applicable: `docs/security-report.md`, `docs/performance-baseline.md` +- Primary deliverable: `docs/reports/test-report.md` +- Additional deliverables when applicable: `docs/reports/security-report.md`, `docs/reports/performance-baseline.md` - Baseline-first default: write final verification outcomes directly to baseline reports on the feature branch. -- Optional WIP area for complex/uncertain efforts: `docs/delta/{id}/VERIFY_DELTA.md` - Before merge: consolidate final findings, severity, and ship-readiness verdict into baseline reports. ## Step 0: Route Mode diff --git a/.github/skills/vision/SKILL.md b/.github/skills/vision/SKILL.md index e67dc6e..2e0d25f 100644 --- a/.github/skills/vision/SKILL.md +++ b/.github/skills/vision/SKILL.md @@ -57,8 +57,7 @@ during this review — that comes after the plan is approved. - Primary deliverable: `docs/product/vision.md` - Baseline-first default: write approved vision decisions directly to `docs/product/vision.md` on the feature branch. -- Optional WIP area for complex/uncertain scope exploration: `docs/delta/{intake-id}/VISION_DELTA.md` -- Before merge: consolidate any required content from optional delta notes back into `docs/product/vision.md`, then remove stale WIP notes. +- Before merge: confirm the vision doc on the feature branch is complete and approved before merge. ## Review posture Envision the platform, not just the feature. Push scope UP. Ask "what would make this 10x better for 2x the effort?" Present each scope-expanding idea as a question. The user opts in or out. diff --git a/.markdownlint.yaml b/.markdownlint.yaml index 0b536f5..42d0610 100644 --- a/.markdownlint.yaml +++ b/.markdownlint.yaml @@ -4,6 +4,7 @@ MD003: false # heading-style — disabled (atx style intentional throughout; s MD004: false # ul-style — disabled (mixed bullets are allowed) MD007: false # ul-indent — disabled (indent style varies across templates) MD010: false # no-hard-tabs — disabled (tab-indented JSON snippets are intentional) +MD012: false # no-multiple-blanks — disabled (release-please CHANGELOG format emits double blank lines) MD013: false # line-length — disabled (long lines allowed) MD022: false # blanks-around-headings — disabled (frontmatter/template structure differs) MD026: false # no-trailing-punctuation — disabled (headings like "Step 1:" are intentional) diff --git a/.vstack/.gitignore b/.vstack/.gitignore new file mode 100644 index 0000000..9f3c0e4 --- /dev/null +++ b/.vstack/.gitignore @@ -0,0 +1,2 @@ +# Generated by vstack install — do not edit manually. +tmp/ diff --git a/.vstack/config.yaml b/.vstack/config.yaml new file mode 100644 index 0000000..840037a --- /dev/null +++ b/.vstack/config.yaml @@ -0,0 +1,28 @@ +# vstack project configuration +# +# vstack install — first-run setup. Seeds this file if missing (never overwrites). +# Then runs init to generate .github/ artifacts from templates. +# Use once per project, or when onboarding a new machine. +# +# vstack init — idempotent regeneration. Reads this file on every run. +# Safe to re-run in CI after pip install --upgrade vstack. +# +# This file is committed to git. It expresses stable project preferences — +# editing it is a deliberate choice that affects all future vstack init runs. + +# Selective install exclusions — remove or comment out to install everything. +# +# exclude: +# skills: +# - terraform +# - terragrunt +# - helm +# - k8s +# instructions: all +# prompts: all + +# Root directory for generated agent artifact paths. +# Default: docs +# +# artifacts: +# root: docs diff --git a/.vstack/templates/architect/artifacts/adr/NNN-template.md b/.vstack/templates/architect/artifacts/adr/NNN-template.md new file mode 100644 index 0000000..ac015e0 --- /dev/null +++ b/.vstack/templates/architect/artifacts/adr/NNN-template.md @@ -0,0 +1,27 @@ +# ADR-NNN: {title} + + + +> **date:** YYYY-MM-DD\ +> **status:** proposed | accepted | superseded by ADR-NNN + +## context + + + +## decision + + + +## alternatives considered + + + +## rationale + + + +## consequences + + diff --git a/.vstack/templates/architect/artifacts/overview.md b/.vstack/templates/architect/artifacts/overview.md new file mode 100644 index 0000000..4bf345b --- /dev/null +++ b/.vstack/templates/architect/artifacts/overview.md @@ -0,0 +1,41 @@ +# Architecture Overview + + + +> Maintained by: **architect** role\ +> Last updated: YYYY-MM-DD + +## overview + + + +## system structure + + + +```text +{system}/ +├── ... +``` + +## components + + + +## principles + + + +## data flow + + + +## key decisions + + + +## open questions + + diff --git a/.vstack/templates/designer/artifacts/overview.md b/.vstack/templates/designer/artifacts/overview.md new file mode 100644 index 0000000..37fdc1f --- /dev/null +++ b/.vstack/templates/designer/artifacts/overview.md @@ -0,0 +1,39 @@ +# Design Overview + + + +> Maintained by: **designer** role\ +> Last updated: YYYY-MM-DD + +## overview + + + +## domain model + + + +## interfaces + + + +## data flows + + + +## error contract + + + +## design principles + + + +## open questions + + diff --git a/.vstack/templates/engineer/artifacts/issues/postmortem.md b/.vstack/templates/engineer/artifacts/issues/postmortem.md new file mode 100644 index 0000000..7039841 --- /dev/null +++ b/.vstack/templates/engineer/artifacts/issues/postmortem.md @@ -0,0 +1,42 @@ +# {id}: {title} — Post-Mortem + + + +> **date:** YYYY-MM-DD\ +> **severity:** P{1–4}\ +> **status:** draft | review | closed\ +> **issue:** [{id}](%7Bid%7D-%7Bslug%7D.md)\ +> **rca:** [{id}-{slug}-rca.md](%7Bid%7D-%7Bslug%7D-rca.md) + +## summary + + + +## timeline + +| Time (UTC) | Event | +| ---------- | ----- | +| HH:MM | | + +## impact + + + +## root cause + + + +## resolution + + + +## action items + +| Item | Owner | Due | Status | +| ---- | ----- | --- | ------ | +| | | | | + +## lessons learned + + diff --git a/.vstack/templates/engineer/artifacts/issues/rca.md b/.vstack/templates/engineer/artifacts/issues/rca.md new file mode 100644 index 0000000..ce48e5a --- /dev/null +++ b/.vstack/templates/engineer/artifacts/issues/rca.md @@ -0,0 +1,39 @@ +# {id}: {title} — Root Cause Analysis + + + +> **date:** YYYY-MM-DD\ +> **severity:** P{1–4}\ +> **status:** draft | in-progress | resolved\ +> **issue:** [{id}](%7Bid%7D-%7Bslug%7D.md) + +## what happened + + + +## root cause + + + +## contributing factors + + + +## detection + + + +## resolution + + + +## action items + +| Item | Owner | Due | +| ---- | ----- | --- | +| | | | + +## lessons learned + + diff --git a/.vstack/templates/product/artifacts/changes/change-request.md b/.vstack/templates/product/artifacts/changes/change-request.md new file mode 100644 index 0000000..592ec44 --- /dev/null +++ b/.vstack/templates/product/artifacts/changes/change-request.md @@ -0,0 +1,42 @@ +# Change Request: {title} + + + +> **date:** YYYY-MM-DD\ +> **status:** draft | review | approved | rejected\ +> **author:** @handle\ +> **stakeholders:** @handle, @handle + +## summary + + + +## rationale + + + +## as-is + + + +## to-be + + + +## proposed approach + + + +## alternatives considered + + + +## risks and dependencies + + + +## success criteria + + diff --git a/.vstack/templates/product/artifacts/issues/issue.md b/.vstack/templates/product/artifacts/issues/issue.md new file mode 100644 index 0000000..446c4fc --- /dev/null +++ b/.vstack/templates/product/artifacts/issues/issue.md @@ -0,0 +1,38 @@ +# Issue: {title} + + + +> **date:** YYYY-MM-DD\ +> **status:** open | in-progress | resolved\ +> **author:** @handle\ +> **kind:** bug | problem | incident + +## summary + + + +## steps to reproduce + + + +## expected behaviour + + + +## actual behaviour + + + +## environment + + + +## workaround + + + +## next steps + + diff --git a/.vstack/templates/product/artifacts/requirements.md b/.vstack/templates/product/artifacts/requirements.md new file mode 100644 index 0000000..66196f7 --- /dev/null +++ b/.vstack/templates/product/artifacts/requirements.md @@ -0,0 +1,42 @@ +# Requirements + + + +> Maintained by: **product** role\ +> Last updated: YYYY-MM-DD + +______________________________________________________________________ + +## context + + + +______________________________________________________________________ + +## functional requirements + +| ID | Requirement | Status | +| ---- | ------------ | ------ | +| FR-1 | {capability} | draft | + +### FR-1 — {capability} + + + +______________________________________________________________________ + +## non-functional requirements + +| ID | Requirement | +| ----- | ----------- | +| NFR-1 | | + +______________________________________________________________________ + +## success criteria + + + +1. {criterion} diff --git a/.vstack/templates/product/artifacts/roadmap.md b/.vstack/templates/product/artifacts/roadmap.md new file mode 100644 index 0000000..344ab79 --- /dev/null +++ b/.vstack/templates/product/artifacts/roadmap.md @@ -0,0 +1,73 @@ +# Roadmap + + + +> Maintained by: **product** role\ +> Last updated: YYYY-MM-DD + +______________________________________________________________________ + +## status overview + + + +| feature | status | notes | +| ------- | ------ | ----- | +| | | | + +Legend: **shipped** = done; **planned** = committed for next release; **candidate** = optional, not committed; **not planned** = evaluated and intentionally excluded. + +______________________________________________________________________ + +## shipped + + + +### {feature group} [shipped — v{version}] + + + +- what was built +- what changed and why it matters +- any ADR or design doc reference + +______________________________________________________________________ + +## planned + + + +### {feature or milestone} [planned — v{version}] + + + +Planned direction: + +- key deliverable +- key deliverable +- any dependency or prerequisite + +______________________________________________________________________ + +## candidates + + + +### {candidate feature} [candidate] + + + +______________________________________________________________________ + +## not planned + + + +| item | reason | +| ---- | ------ | +| | | diff --git a/.vstack/templates/product/artifacts/vision.md b/.vstack/templates/product/artifacts/vision.md new file mode 100644 index 0000000..833a89c --- /dev/null +++ b/.vstack/templates/product/artifacts/vision.md @@ -0,0 +1,38 @@ +# Vision + + + +> Maintained by: **product** role\ +> Last updated: YYYY-MM-DD + +## what is {product} + + + +______________________________________________________________________ + +## why it exists + + + +______________________________________________________________________ + +## who it is for + + + +______________________________________________________________________ + +## what success looks like + + + +______________________________________________________________________ + +## out of scope + + diff --git a/.vstack/templates/release/artifacts/release-summary.md b/.vstack/templates/release/artifacts/release-summary.md new file mode 100644 index 0000000..da5cc83 --- /dev/null +++ b/.vstack/templates/release/artifacts/release-summary.md @@ -0,0 +1,48 @@ +# Release Summary — YYYY-MM-DD + + + +> **date:** YYYY-MM-DD\ +> **version:** vX.Y.Z — assign when tagging\ +> **status:** draft | published + +## summary + + + +## breaking changes + + + +## what's new + + + +## fixed + + + +## internal + + + +## upgrade notes + + + +## artifacts reviewed + + + +| Artifact | Status | +| -------- | ----------- | +| | ✓ / updated | + +## sign-offs + +| Role | Sign-off | +| ------- | -------- | +| Tester | | +| Product | | diff --git a/.vstack/templates/tester/artifacts/performance-baseline.md b/.vstack/templates/tester/artifacts/performance-baseline.md new file mode 100644 index 0000000..9f6631d --- /dev/null +++ b/.vstack/templates/tester/artifacts/performance-baseline.md @@ -0,0 +1,25 @@ +# Performance Baseline + + + +> Maintained by: **tester** role\ +> Last updated: YYYY-MM-DD\ +> Scope: {what was benchmarked} + +## baseline + +| Operation / Endpoint | P50 | P95 | P99 | Threshold | Status | +| -------------------- | --- | --- | --- | --------- | ----------- | +| | | | | | PASS / FAIL | + +## methodology + + + +## regressions + + + +## notes + + diff --git a/.vstack/templates/tester/artifacts/security-report.md b/.vstack/templates/tester/artifacts/security-report.md new file mode 100644 index 0000000..5394356 --- /dev/null +++ b/.vstack/templates/tester/artifacts/security-report.md @@ -0,0 +1,26 @@ +# Security Report + + + +> Maintained by: **tester** role\ +> Last updated: YYYY-MM-DD\ +> Scope: {what was scanned} — method: {static analysis | OWASP | STRIDE | ...} + +## verdict + +| Category | Findings | Blocking | +| ----------------- | ---------------------------- | -------- | +| Static analysis | {N} LOW / {N} MED / {N} HIGH | Yes / No | +| Dependency CVEs | {N} | Yes / No | +| Secrets in source | None | — | +| Injection risk | None identified | — | + + + +## findings + + + +## notes + + diff --git a/.vstack/templates/tester/artifacts/test-report.md b/.vstack/templates/tester/artifacts/test-report.md new file mode 100644 index 0000000..7564cea --- /dev/null +++ b/.vstack/templates/tester/artifacts/test-report.md @@ -0,0 +1,32 @@ +# Test Report + + + +> Maintained by: **tester** role\ +> Last updated: YYYY-MM-DD\ +> Scope: {what was tested} + +## verdict + +| Dimension | Result | +| ------------- | --------------------------------- | +| Functional | PASS / FAIL — {N}/{N} tests green | +| Lint / Style | PASS / FAIL | +| Type checking | PASS / FAIL | +| Coverage | PASS / FAIL — {N}% | +| Security | See security-report.md | +| Performance | See performance-baseline.md | + + + +## test execution + + + +## coverage summary + + + +## findings + + diff --git a/.github/vstack.json b/.vstack/vstack.json similarity index 84% rename from .github/vstack.json rename to .vstack/vstack.json index 1e6cb55..c85cee4 100644 --- a/.github/vstack.json +++ b/.vstack/vstack.json @@ -2,14 +2,14 @@ "manifest_version": 2, "hash_algorithm": "sha256", "vstack_version": "0.0.0.post3.dev0+df3fe6e", - "installed_at": "2026-05-02T16:26:18.650080+00:00", + "installed_at": "2026-05-06T22:03:21.641306+00:00", "artifacts": { "skills": [ { "name": "adr", "file": "skills/adr/SKILL.md", "version": "20260421003", - "checksum": "551bc8607dc49d9d92968e0d22568efc6eb5b9b85bed17aeed981f33022a891b", + "checksum": "0838ffc14c5b86ea8b3df93cf6ce76c9bcd27b6c55c4ba47a3d01cc000fd7ee0", "checksum_algorithm": "sha256" }, { @@ -23,7 +23,7 @@ "name": "architecture", "file": "skills/architecture/SKILL.md", "version": "20260421005", - "checksum": "1cf90ff73efd2411d5da1434c0273c42f5f77a2e8a1c47f7f877545863b783b3", + "checksum": "4ed22957e392aa89cf4949d2a88e707bf913948c2805a257039f5376810af754", "checksum_algorithm": "sha256" }, { @@ -37,7 +37,7 @@ "name": "cicd", "file": "skills/cicd/SKILL.md", "version": "20260421006", - "checksum": "fba15036cec658c33c8a179acfdac52722b8adcf534d9e1b8cc2f3c026b862f5", + "checksum": "ffe0df7fe8c425844e9fc1a20976a9d6c116d828af96c1a568317728bfdefdcf", "checksum_algorithm": "sha256" }, { @@ -114,14 +114,14 @@ "name": "design", "file": "skills/design/SKILL.md", "version": "20260421013", - "checksum": "a45a862c2d721e015a2b9a7d0d6112982932216b07734af58323784d26cba887", + "checksum": "e21a674324244412f7b4a5bc010da4a7b9dfcf3522d54281bc9cf69f0b1126bd", "checksum_algorithm": "sha256" }, { "name": "docs", "file": "skills/docs/SKILL.md", "version": "20260421014", - "checksum": "1519e8b95149469f029b8f6c57ec09517e9de09a86ba865cdcb6f746d44dc2f1", + "checksum": "7d34421800bf04fe5a0782abfc5702a421419d31a5c66fd495d7dc8a6e2c64e3", "checksum_algorithm": "sha256" }, { @@ -142,7 +142,7 @@ "name": "gh-issues", "file": "skills/gh-issues/SKILL.md", "version": "20260502025", - "checksum": "36d07b676604a4f698369f8bf6b65b43f5256f676fd56664717ff2007da126f0", + "checksum": "7a2b9b7463a40436fe77c77d066dd455ddb4abef877726ba041b89cbaf030280", "checksum_algorithm": "sha256" }, { @@ -169,15 +169,15 @@ { "name": "incident", "file": "skills/incident/SKILL.md", - "version": "20260421017", - "checksum": "7f20cb1592971846391431384e24e193af47a4cbfe6ccef86b11ef074baf0549", + "version": "20260503002", + "checksum": "d337c4f145c09af856f4a05f41b11f2e46f3822f77e067e1a6e39de5f2af1fe8", "checksum_algorithm": "sha256" }, { "name": "inspect", "file": "skills/inspect/SKILL.md", "version": "20260421018", - "checksum": "53bcdb75baf11a82ce91e70b102d7fb07f9329b825f566c7c1c5d0531080623b", + "checksum": "7997cb4477f9b1a8a6753082a4f3ea0c9b5a1a77520a3f20527f8c153c968a27", "checksum_algorithm": "sha256" }, { @@ -198,7 +198,7 @@ "name": "onboard", "file": "skills/onboard/SKILL.md", "version": "20260421020", - "checksum": "3c817dbdea5404a01c8b81d87196fcef559b65ad184a0044bdcb0b3943de8be1", + "checksum": "b96fb4067121eabbdf686641b55d32f253282e0b734fa4b2df6e4a9e85938cde", "checksum_algorithm": "sha256" }, { @@ -212,7 +212,14 @@ "name": "performance", "file": "skills/performance/SKILL.md", "version": "20260421022", - "checksum": "6bd122818ff0ad7e4f1d29ed221d6ef33405a697b2a0d82a14dc3d0ea9065b01", + "checksum": "e8cf2eba2a3fdc162d03afeaba260bd0c2f0aac47121c26dc9d63614bff971ec", + "checksum_algorithm": "sha256" + }, + { + "name": "postmortem", + "file": "skills/postmortem/SKILL.md", + "version": "20260503001", + "checksum": "ead97f2a5b28672f4ac483e467eec5b0d2a7e3b96eceaaa2017c918972c2d827", "checksum_algorithm": "sha256" }, { @@ -229,6 +236,13 @@ "checksum": "c86759f257553554f2069cb6d0eda93bd98fd5c2fb251b1c8cc423302f33b01d", "checksum_algorithm": "sha256" }, + { + "name": "rca", + "file": "skills/rca/SKILL.md", + "version": "20260503001", + "checksum": "4d2347e1bab1d0717669b78160a5703ba4d1bb3486a9499924ca569e6f049f0e", + "checksum_algorithm": "sha256" + }, { "name": "refactor", "file": "skills/refactor/SKILL.md", @@ -240,14 +254,14 @@ "name": "release-notes", "file": "skills/release-notes/SKILL.md", "version": "20260502014", - "checksum": "b1148eeaf2c7d9f2041c13bf060dd5404b3c22ea09a6182660c880644207edaa", + "checksum": "81fd326b93789aad264e1ad27a73d00efd76c4486b5a20935bafcb18ebd43758", "checksum_algorithm": "sha256" }, { "name": "requirements", "file": "skills/requirements/SKILL.md", "version": "20260421024", - "checksum": "4fa0a00259210b7e200d188cd00b9921c934385520eb21d00fa8361bbe4e6e57", + "checksum": "312e1c24bc43828798e86b881204196ebcad4f90ff5cfa63d099b8836e4f1e49", "checksum_algorithm": "sha256" }, { @@ -261,7 +275,7 @@ "name": "security", "file": "skills/security/SKILL.md", "version": "20260421025", - "checksum": "5bdab8583938f7256889d02853a660d132a37024843b1654394193442fc898be", + "checksum": "f58a629804bec6f6b5839eea1e7754458bf85cceb7403b9470f80513119a306d", "checksum_algorithm": "sha256" }, { @@ -282,21 +296,21 @@ "name": "threat-model", "file": "skills/threat-model/SKILL.md", "version": "20260502021", - "checksum": "1e3d42f73f1c3deb3589cd433d57dc9f689c5b53892865d091170c9d5155b319", + "checksum": "11504d508ddfc6e9e2ded8dfac8fe6fb7af691c7ef27fdc539c5f8f870fc06bb", "checksum_algorithm": "sha256" }, { "name": "verify", "file": "skills/verify/SKILL.md", "version": "20260421026", - "checksum": "8c391cbade20645aba1af062020990724078f34036707de51cacb726a8875ad8", + "checksum": "b8f80fda903c0d55c556374626e308dcc61787d59e2e642f9c9513d06b0ac2b9", "checksum_algorithm": "sha256" }, { "name": "vision", "file": "skills/vision/SKILL.md", "version": "20260421027", - "checksum": "5f961984068b88b6f6a819ffc33f8f08fe2d6e42f4f6c16a2f4c24d0342dae48", + "checksum": "a74271c10816e3cee75fc1489d0b2defb391035b22f8d3da0cc951ae1afb7ef2", "checksum_algorithm": "sha256" } ], @@ -304,43 +318,43 @@ { "name": "architect", "file": "agents/architect.agent.md", - "version": "20260502015", - "checksum": "2fad8dc4129453a8cb382b52267f967d339befd8d51cb059d6146cd9c346448f", + "version": "20260503022", + "checksum": "524572e506ea4836237141a1bb2f9eee36de9f7342bf42f41442576b032e916a", "checksum_algorithm": "sha256" }, { "name": "designer", "file": "agents/designer.agent.md", - "version": "20260502016", - "checksum": "fd3ded2f4889c36d40f9f782f78a05efe8ae8440b3ee1e933c13fdf7457a7454", + "version": "20260503024", + "checksum": "a7afb743e80494e6a4a4fa2a93cc469b9eff87eafb5bde1a27ab90391f590aca", "checksum_algorithm": "sha256" }, { "name": "engineer", "file": "agents/engineer.agent.md", - "version": "20260502017", - "checksum": "82fb449131d97a74c907374d10b57ad33c1a770c0ff42083323b7bea05cafe7f", + "version": "20260503024", + "checksum": "1d06a70f56a98b30b0148a6b6d7df5b75f8cefb34aaa5100521faa7afe068ebf", "checksum_algorithm": "sha256" }, { "name": "product", "file": "agents/product.agent.md", - "version": "20260502018", - "checksum": "9311a59e88726f1cb1afe464624cd5a3bc34e0a5e64bdb3c78b75d5c479ed57f", + "version": "20260503021", + "checksum": "ac22faaeb1577458a88d6314e83d0c79b1479c45b066f7f989afb59f86b190a5", "checksum_algorithm": "sha256" }, { "name": "release", "file": "agents/release.agent.md", - "version": "20260502019", - "checksum": "68c8bc0748872d5f426a3bb8c12b6438c9070f3a4d5003103b59bea4f9fd9d4f", + "version": "20260503020", + "checksum": "5dcee462a58ec95b03110fdf59986ded0f86c1cc1db6b817e6c9a44fc5ba5f14", "checksum_algorithm": "sha256" }, { "name": "tester", "file": "agents/tester.agent.md", - "version": "20260502020", - "checksum": "877eb94928d123c8b4834f6f8baa08fa23c95a8aa989d2f3f21673468eddbe42", + "version": "20260503026", + "checksum": "ecce547f703fcee918d1b4991f5ce5e850392d4f3fbf01b186723cc0a8961ecf", "checksum_algorithm": "sha256" } ], diff --git a/CHANGELOG.md b/CHANGELOG.md index 1d6945b..0ad2355 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,75 +4,67 @@ ## [2.2.0](https://github.com/eschaar/vstack/compare/2.1.0...2.2.0) (2026-05-02) - ### Features -* **instructions:** add markdown authoring conventions instruction ([b4cf606](https://github.com/eschaar/vstack/commit/b4cf606ded0fc0cdd367d01a385be237803ebdd0)) -* **instructions:** add security policy instruction ([612d37f](https://github.com/eschaar/vstack/commit/612d37f06efdccc776f75421de6fa96a585136c3)) -* **instructions:** add terraform and terragrunt coding conventions ([6b82d4b](https://github.com/eschaar/vstack/commit/6b82d4b7576828a9ad85c4de980a324332de700d)) -* **instructions:** add typescript and java coding conventions ([62f99b3](https://github.com/eschaar/vstack/commit/62f99b3ee25696688932fc76f365a09d02143375)) -* **instructions:** add universal testing conventions instruction ([ec12899](https://github.com/eschaar/vstack/commit/ec128999616a562467b1930f10540dd35bc31e82)) -* **prompts:** add api-design-review, dependency-audit; improve existing prompts ([e4630b7](https://github.com/eschaar/vstack/commit/e4630b7b0fadfb49bc1aed423d9369d8c5f74972)) -* **prompts:** add architecture-risk, incident-timeline, migration-safety, release-readiness prompts ([7823539](https://github.com/eschaar/vstack/commit/7823539170e9c2299a657711818018de170a812d)) -* **skills:** add conventional-commit skill ([cba3f84](https://github.com/eschaar/vstack/commit/cba3f84ac60c8de1b8594ddee4c1cce44bce20ca)) -* **skills:** add gdpr, terraform, terragrunt, cloudformation, and aws-cli skills ([8b9e0eb](https://github.com/eschaar/vstack/commit/8b9e0eb7c327401f766c888154940b04461c94ba)) -* **skills:** add gh-issues, codeql, dependabot, and secret-scan skills ([1909578](https://github.com/eschaar/vstack/commit/190957835a78c1999944c058c8f441bbfb009bcf)) -* **skills:** add gh-release skill with hardened gh release workflow ([bcac399](https://github.com/eschaar/vstack/commit/bcac399b226e5765b9d065407af9111515aaf07e)) -* **skills:** add k8s, helm, and rancher skills and instructions ([169005a](https://github.com/eschaar/vstack/commit/169005a42748dfaf93bcdf6b4a8b476de64db1f7)) -* **skills:** add threat-model skill and optimize template token usage ([057168c](https://github.com/eschaar/vstack/commit/057168c33e414376b0c2fff9d76542b12ce8d477)) -* **skills:** audit and harden all skill templates ([ff968ff](https://github.com/eschaar/vstack/commit/ff968ffefde28ff649d4bbb3a84e45db42668d0a)) - +- **instructions:** add markdown authoring conventions instruction ([b4cf606](https://github.com/eschaar/vstack/commit/b4cf606ded0fc0cdd367d01a385be237803ebdd0)) +- **instructions:** add security policy instruction ([612d37f](https://github.com/eschaar/vstack/commit/612d37f06efdccc776f75421de6fa96a585136c3)) +- **instructions:** add terraform and terragrunt coding conventions ([6b82d4b](https://github.com/eschaar/vstack/commit/6b82d4b7576828a9ad85c4de980a324332de700d)) +- **instructions:** add typescript and java coding conventions ([62f99b3](https://github.com/eschaar/vstack/commit/62f99b3ee25696688932fc76f365a09d02143375)) +- **instructions:** add universal testing conventions instruction ([ec12899](https://github.com/eschaar/vstack/commit/ec128999616a562467b1930f10540dd35bc31e82)) +- **prompts:** add api-design-review, dependency-audit; improve existing prompts ([e4630b7](https://github.com/eschaar/vstack/commit/e4630b7b0fadfb49bc1aed423d9369d8c5f74972)) +- **prompts:** add architecture-risk, incident-timeline, migration-safety, release-readiness prompts ([7823539](https://github.com/eschaar/vstack/commit/7823539170e9c2299a657711818018de170a812d)) +- **skills:** add conventional-commit skill ([cba3f84](https://github.com/eschaar/vstack/commit/cba3f84ac60c8de1b8594ddee4c1cce44bce20ca)) +- **skills:** add gdpr, terraform, terragrunt, cloudformation, and aws-cli skills ([8b9e0eb](https://github.com/eschaar/vstack/commit/8b9e0eb7c327401f766c888154940b04461c94ba)) +- **skills:** add gh-issues, codeql, dependabot, and secret-scan skills ([1909578](https://github.com/eschaar/vstack/commit/190957835a78c1999944c058c8f441bbfb009bcf)) +- **skills:** add gh-release skill with hardened gh release workflow ([bcac399](https://github.com/eschaar/vstack/commit/bcac399b226e5765b9d065407af9111515aaf07e)) +- **skills:** add k8s, helm, and rancher skills and instructions ([169005a](https://github.com/eschaar/vstack/commit/169005a42748dfaf93bcdf6b4a8b476de64db1f7)) +- **skills:** add threat-model skill and optimize template token usage ([057168c](https://github.com/eschaar/vstack/commit/057168c33e414376b0c2fff9d76542b12ce8d477)) +- **skills:** audit and harden all skill templates ([ff968ff](https://github.com/eschaar/vstack/commit/ff968ffefde28ff649d4bbb3a84e45db42668d0a)) ### Refactoring -* **agents:** enforce canonical template structure ([8c54d8a](https://github.com/eschaar/vstack/commit/8c54d8a9f122f7fdea5458d467598ba83d247bd4)) -* **skills:** make pr and release-notes skills generic; add ADR-018 skill-agent boundary ([b0b8956](https://github.com/eschaar/vstack/commit/b0b895684dbdbae271138c09bbe602f897d50d94)) -* **skills:** optimise 7 new skills for token efficiency ([6fe6ed2](https://github.com/eschaar/vstack/commit/6fe6ed276ab49bbdc1a66a72d9d4709b0be342fb)) -* **templates:** migrate template versions to YYYYMMDDNNN ([5cd239f](https://github.com/eschaar/vstack/commit/5cd239f30cb1bdf999480297fc0cc197474dde20)) - +- **agents:** enforce canonical template structure ([8c54d8a](https://github.com/eschaar/vstack/commit/8c54d8a9f122f7fdea5458d467598ba83d247bd4)) +- **skills:** make pr and release-notes skills generic; add ADR-018 skill-agent boundary ([b0b8956](https://github.com/eschaar/vstack/commit/b0b895684dbdbae271138c09bbe602f897d50d94)) +- **skills:** optimise 7 new skills for token efficiency ([6fe6ed2](https://github.com/eschaar/vstack/commit/6fe6ed276ab49bbdc1a66a72d9d4709b0be342fb)) +- **templates:** migrate template versions to YYYYMMDDNNN ([5cd239f](https://github.com/eschaar/vstack/commit/5cd239f30cb1bdf999480297fc0cc197474dde20)) ### Documentation -* **design:** update skills and instructions tables; add new skills to agent mappings ([0947bc7](https://github.com/eschaar/vstack/commit/0947bc7fa97b2b567bd50fb4bbb491b8ce73b57e)) -* **instructions:** add instruction catalog to design doc and README ([f9889ff](https://github.com/eschaar/vstack/commit/f9889ff0dfa3a66f95a4b13a1d9589f7907e8410)) -* **product:** expand copilot roadmap and customization strategy ([de2a364](https://github.com/eschaar/vstack/commit/de2a364f8e3faad33956f6f5e55c350f05f9733d)) -* **skills:** add official documentation references to all tool-specific skills and instructions ([7fb7b3e](https://github.com/eschaar/vstack/commit/7fb7b3edd032fc4a32a6c7064021adfad6962991)) - +- **design:** update skills and instructions tables; add new skills to agent mappings ([0947bc7](https://github.com/eschaar/vstack/commit/0947bc7fa97b2b567bd50fb4bbb491b8ce73b57e)) +- **instructions:** add instruction catalog to design doc and README ([f9889ff](https://github.com/eschaar/vstack/commit/f9889ff0dfa3a66f95a4b13a1d9589f7907e8410)) +- **product:** expand copilot roadmap and customization strategy ([de2a364](https://github.com/eschaar/vstack/commit/de2a364f8e3faad33956f6f5e55c350f05f9733d)) +- **skills:** add official documentation references to all tool-specific skills and instructions ([7fb7b3e](https://github.com/eschaar/vstack/commit/7fb7b3edd032fc4a32a6c7064021adfad6962991)) ### Maintenance -* **review:** quote applyTo glob in generated frontmatter; add test/tests commit types ([aa579ba](https://github.com/eschaar/vstack/commit/aa579bab74745cdfc1c8e33931a0a851ccfbe851)) -* **templates:** bump code-review, architecture-risk, release-readiness prompt versions to 0.1.1 ([0d1da60](https://github.com/eschaar/vstack/commit/0d1da60e9f6ef1602345ee333dc05c6e4bcc8e55)) -* **templates:** bump pr and release-notes skill versions to 1.1.0 ([e4f0923](https://github.com/eschaar/vstack/commit/e4f09231b594153c4f1508cf9544652b002108c5)) - +- **review:** quote applyTo glob in generated frontmatter; add test/tests commit types ([aa579ba](https://github.com/eschaar/vstack/commit/aa579bab74745cdfc1c8e33931a0a851ccfbe851)) +- **templates:** bump code-review, architecture-risk, release-readiness prompt versions to 0.1.1 ([0d1da60](https://github.com/eschaar/vstack/commit/0d1da60e9f6ef1602345ee333dc05c6e4bcc8e55)) +- **templates:** bump pr and release-notes skill versions to 1.1.0 ([e4f0923](https://github.com/eschaar/vstack/commit/e4f09231b594153c4f1508cf9544652b002108c5)) ### Tests -* **agents:** fold artifacts-section order into canonical lint rule ([a435bc5](https://github.com/eschaar/vstack/commit/a435bc5419cb4ece9257d14697b917bcd6d3b8fa)) -* **frontmatter:** add regression tests for applyTo quoting and version comparison ([20943e9](https://github.com/eschaar/vstack/commit/20943e97be459e7d3ac16870409aad9522f95992)) +- **agents:** fold artifacts-section order into canonical lint rule ([a435bc5](https://github.com/eschaar/vstack/commit/a435bc5419cb4ece9257d14697b917bcd6d3b8fa)) +- **frontmatter:** add regression tests for applyTo quoting and version comparison ([20943e9](https://github.com/eschaar/vstack/commit/20943e97be459e7d3ac16870409aad9522f95992)) ## [2.1.0](https://github.com/eschaar/vstack/compare/2.0.4...2.1.0) (2026-05-02) - ### Features -* **cli:** add dry-run install with type/name selector summary and conflict resolution hints ([c783552](https://github.com/eschaar/vstack/commit/c7835526eddabea99b0642acf645ec14d2518eae)) - +- **cli:** add dry-run install with type/name selector summary and conflict resolution hints ([c783552](https://github.com/eschaar/vstack/commit/c7835526eddabea99b0642acf645ec14d2518eae)) ### Fixes -* **ci:** use job-scoped PyPI fallback token checks ([267417a](https://github.com/eschaar/vstack/commit/267417a2fd6899d9d3c4ecd7bbdfab5ba602d84f)) -* **cli:** align dry-run summary guidance and tests ([c11b67d](https://github.com/eschaar/vstack/commit/c11b67d53da9e9b706b187278f88ad9d87fcad93)) - +- **ci:** use job-scoped PyPI fallback token checks ([267417a](https://github.com/eschaar/vstack/commit/267417a2fd6899d9d3c4ecd7bbdfab5ba602d84f)) +- **cli:** align dry-run summary guidance and tests ([c11b67d](https://github.com/eschaar/vstack/commit/c11b67d53da9e9b706b187278f88ad9d87fcad93)) ### Maintenance -* **ci:** add trusted publishing fallback and publish concurrency group ([ef4702d](https://github.com/eschaar/vstack/commit/ef4702d04839aa1f16c4343f8e27b517c90508f2)) -* **deps:** bump cachetools to 7.1.0 and tox to 4.53.1 ([5d60804](https://github.com/eschaar/vstack/commit/5d60804b72d111ca7444e79b233aa57325c08754)) -* **docs:** add Development Commands and CLI Architecture to copilot-instructions ([5bf4c24](https://github.com/eschaar/vstack/commit/5bf4c244df64f2adacf9a020e76828423222baab)) -* **docs:** add first-install dry-run workflow and preserved-file conflict resolution ([fa8ef18](https://github.com/eschaar/vstack/commit/fa8ef18230c5f43c0c4439e5c74cb15839c8fc04)) -* **docs:** fix BaseCommand signature, model type, role assignments, and contributor workflow ([45a19be](https://github.com/eschaar/vstack/commit/45a19be8f17459a158915bfda8a6d9508af16b7c)) -* **docs:** fix maintainer role, stale paths, gate count, and tool names in ADRs 001-012 ([dcf596a](https://github.com/eschaar/vstack/commit/dcf596a2b145c1ae31e7c3a326edd5fae728f11a)) +- **ci:** add trusted publishing fallback and publish concurrency group ([ef4702d](https://github.com/eschaar/vstack/commit/ef4702d04839aa1f16c4343f8e27b517c90508f2)) +- **deps:** bump cachetools to 7.1.0 and tox to 4.53.1 ([5d60804](https://github.com/eschaar/vstack/commit/5d60804b72d111ca7444e79b233aa57325c08754)) +- **docs:** add Development Commands and CLI Architecture to copilot-instructions ([5bf4c24](https://github.com/eschaar/vstack/commit/5bf4c244df64f2adacf9a020e76828423222baab)) +- **docs:** add first-install dry-run workflow and preserved-file conflict resolution ([fa8ef18](https://github.com/eschaar/vstack/commit/fa8ef18230c5f43c0c4439e5c74cb15839c8fc04)) +- **docs:** fix BaseCommand signature, model type, role assignments, and contributor workflow ([45a19be](https://github.com/eschaar/vstack/commit/45a19be8f17459a158915bfda8a6d9508af16b7c)) +- **docs:** fix maintainer role, stale paths, gate count, and tool names in ADRs 001-012 ([dcf596a](https://github.com/eschaar/vstack/commit/dcf596a2b145c1ae31e7c3a326edd5fae728f11a)) ## [2.0.4](https://github.com/eschaar/vstack/compare/2.0.3...2.0.4) (2026-04-28) diff --git a/README-pypi.md b/README-pypi.md index 3f11a38..b3ad40f 100644 --- a/README-pypi.md +++ b/README-pypi.md @@ -105,7 +105,19 @@ Profile-wide install (optional defaults for all projects): vstack install --global ``` -By default, `vstack install` preserves existing unmanaged files and local edits to tracked files by comparing the current file contents with the SHA-256 checksum recorded in `vstack.json`. Use `--adopt-name ` to start tracking one existing unmanaged file without overwriting it. `vstack uninstall` also preserves locally modified tracked files unless you explicitly pass `--force` or `--force-name `. Use `vstack manifest status --target ...` (or `vstack status --target ...`) to see what still matches the manifest. If a legacy manifest schema is detected, run `vstack manifest upgrade --target ...` first. +`vstack install` is the first-run command: it seeds `.vstack/config.yaml` in your project (never overwrites), then generates `.github/` artifacts from templates. `vstack init` re-runs generation idempotently — safe to use in CI after upgrading vstack. + +By default, `vstack install` preserves existing unmanaged files and local edits to tracked files by comparing the current file contents with the SHA-256 checksum recorded in `.vstack/vstack.json`. Use `--adopt-name ` to start tracking one existing unmanaged file without overwriting it. `vstack uninstall` also preserves locally modified tracked files unless you explicitly pass `--force` or `--force-name `. Use `vstack manifest status --target ...` (or `vstack status --target ...`) to see what still matches the manifest. If a legacy manifest schema is detected, run `vstack manifest upgrade --target ...` first. + +To skip artifact types or individual artifacts you do not need, edit `.vstack/config.yaml`: + +```yaml +exclude: + skills: + - terraform + - helm + instructions: all # skip the entire type +``` If you already have agents, skills, or other files in `.github/`, run a dry-run first to see what would be preserved before committing: diff --git a/README.md b/README.md index c293d44..af9f74d 100644 --- a/README.md +++ b/README.md @@ -401,20 +401,23 @@ ______________________________________________________________________ | `vstack --version` | Show vstack version | | `vstack validate` | Validate source templates only | | `vstack verify` | Verify source templates and installed output, including checksum drift | -| `vstack status --target DIR` | Report which installed artifacts still match `vstack.json` | +| `vstack status --target DIR` | Report which installed artifacts still match `.vstack/vstack.json` | | `vstack manifest status --target DIR` | Manifest-scoped status for installed output and ownership | | `vstack manifest verify --target DIR` | Manifest-scoped verify for installed output only | -| `vstack manifest upgrade --target DIR` | Upgrade a legacy `vstack.json` schema to current format | +| `vstack manifest upgrade --target DIR` | Upgrade a legacy `.vstack/vstack.json` schema to current format | | `vstack verify --target DIR` | Verify installed artifacts in DIR/.github | | `vstack verify --global` | Verify artifacts in your VS Code global profile | -| `vstack install --target DIR` | Install vstack artifacts into a project, preserving existing unmanaged files | +| `vstack install --target DIR` | First-run setup: seeds `.vstack/`, then generates `.github/` artifacts from templates | +| `vstack install` | Same as above, using the current directory as target | | `vstack install --global` | Install vstack artifacts into your VS Code profile, preserving local edits unless forced | | `vstack install --dry-run` | Preview install actions without writing files | +| `vstack init --target DIR` | Idempotent regeneration: reads `.vstack/config.yaml` and updates `.github/` artifacts | +| `vstack init` | Same as above, using the current directory as target | | `vstack uninstall --target DIR` | Uninstall tracked artifacts that still match the manifest | | `vstack uninstall --global` | Uninstall vstack artifacts from your VS Code profile | | `vstack uninstall` | Uninstall from the current directory default target | -By default, `vstack install` is conservative: if a target file already exists but is not tracked by `vstack`, it is left in place. For tracked files, `--update` only rewrites artifacts whose on-disk content still matches the SHA-256 checksum of the last installed version recorded in `vstack.json`. Use `--force` to overwrite everything, `--force-name ` to overwrite one specific managed artifact, or `--adopt-name ` to start tracking one existing unmanaged file without overwriting it. +By default, `vstack install` is conservative: if a target file already exists but is not tracked by `vstack`, it is left in place. For tracked files, `--update` only rewrites artifacts whose on-disk content still matches the SHA-256 checksum of the last installed version recorded in `.vstack/vstack.json`. Use `--force` to overwrite everything, `--force-name ` to overwrite one specific managed artifact, or `--adopt-name ` to start tracking one existing unmanaged file without overwriting it. If you already have agents, skills, or other files in `.github/`, run a dry-run first to see what would be preserved before committing: @@ -444,6 +447,46 @@ When a legacy manifest schema is detected, verification/status/install paths now For smaller terminals, `vstack manifest status` (and `vstack status`) defaults to a compact issues-focused text view with color markers. For tooling or exports, use `--format json` or `--format yaml`. Add `--verbose` to include managed entries, and `--no-color` when plain text is preferred. +### install vs init + +`vstack install` and `vstack init` are complementary: + +| Command | When to use | What it does | +| ---------------- | ----------------------------------------------- | --------------------------------------------------------------------------------- | +| `vstack install` | Once per project (or when onboarding a machine) | Seeds `.vstack/config.yaml` if missing (never overwrites), then runs `init` | +| `vstack init` | On every vstack upgrade, or in CI | Idempotent regeneration — reads `.vstack/config.yaml` and applies it on every run | + +Both commands default to the current working directory when `--target` is omitted — run them from the repository root. Both accept the same flags (`--only`, `--force`, `--update`, `--dry-run`, etc.). + +### `.vstack/config.yaml` — project configuration + +When you run `vstack install`, a `.vstack/config.yaml` file is seeded in your project. This file is yours: vstack never overwrites it. Commit it to version control — it expresses stable project preferences that apply on every future `vstack init` run. + +The two most useful settings: + +**Exclude specific artifacts** — skip artifact types or individual artifacts you do not need: + +```yaml +exclude: + skills: + - terraform + - helm + - k8s + instructions: all # skip the entire instructions type + prompts: all # skip the entire prompts type +``` + +Type-level entries (`all`) remove the type from generation entirely. Name-level entries skip individual artifacts within a type while keeping the rest. + +**Override the docs root path** — change where agent artifact paths point (default: `docs`): + +```yaml +artifacts: + root: documentation # use a different path prefix in generated agent files +``` + +All fields are optional. An absent or commented-out block restores the default behaviour. + ______________________________________________________________________ ## 🤝 How to contribute @@ -646,7 +689,7 @@ use `normal` regardless of active mode. ______________________________________________________________________ -More info: [docs/product/roadmap.md](docs/product/roadmap.md), [docs/architecture/architecture.md](docs/architecture/architecture.md) +More info: [docs/product/roadmap.md](docs/product/roadmap.md), [docs/architecture/overview.md](docs/architecture/overview.md) ______________________________________________________________________ @@ -761,8 +804,8 @@ ______________________________________________________________________ ## 📚 Further Reading -- [docs/architecture/architecture.md](docs/architecture/architecture.md) -- [docs/design/design.md](docs/design/design.md) +- [docs/architecture/overview.md](docs/architecture/overview.md) +- [docs/design/overview.md](docs/design/overview.md) - [docs/design/cicd.md](docs/design/cicd.md) - [docs/design/workflow.md](docs/design/workflow.md) - [docs/design/skills.md](docs/design/skills.md) diff --git a/docs/architecture/adr/009-role-model.md b/docs/architecture/adr/009-role-model.md index f77da43..284e5ad 100644 --- a/docs/architecture/adr/009-role-model.md +++ b/docs/architecture/adr/009-role-model.md @@ -22,14 +22,32 @@ which artifacts they own. Define **6 fixed agent roles**: -| Role | Persona | Primary artifacts | -| ----------- | ---------------------------------------------- | ----------------------------------------------------------------------------------- | -| `product` | Initiates pipeline, owns vision + requirements | `docs/product/vision.md`, `docs/product/requirements.md`, `docs/product/roadmap.md` | -| `architect` | Locks in technical plan, writes ADRs | `docs/architecture/architecture.md`, `docs/architecture/adr/*.md` | -| `designer` | API design + DX; backend-mode skips UI/UX | `docs/design/design.md`, API specs | -| `engineer` | Implements code + unit tests | source code, unit tests | -| `tester` | Verification, security, and performance audit | `docs/test-report.md`, `docs/security-report.md`, `docs/performance-baseline.md` | -| `release` | Release preparation and PR handoff | `docs/releases/{date}.md`, `CHANGELOG.md`, release PR | +- **`product`** — Initiates pipeline, owns vision + requirements + + - Artifacts: vision, requirements, roadmap + +- **`architect`** — Locks in technical plan, writes ADRs + + - Artifacts: architecture overview, ADRs + +- **`designer`** — API design + DX; backend-mode skips UI/UX + + - Artifacts: design overview, API specs, UX artifacts (frontend scope only) + +- **`engineer`** — Implements code + unit tests + + - Artifacts: source code, unit tests, RCA and post-mortem artifacts + +- **`tester`** — Verification, security, and performance audit + + - Artifacts: test report, security report, performance baseline + +- **`release`** — Release preparation and PR handoff + + - Artifacts: release document, changelog, release PR + +Default artifact paths for each role are defined in ADR-021 and configured per-project +in each agent's `config.yaml`. Paths are overridable; the role ownership above is fixed. ## conceptual model diff --git a/docs/architecture/adr/010-artifact-flow.md b/docs/architecture/adr/010-artifact-flow.md index 3d0546d..e88dce9 100644 --- a/docs/architecture/adr/010-artifact-flow.md +++ b/docs/architecture/adr/010-artifact-flow.md @@ -15,6 +15,12 @@ but persistent and inspectable) to an external message bus (complex and heavywei **Roles communicate exclusively through files on disk.** +**Pipeline progression is stage-gated by explicit user approval.** + +**Handoff controls are reserved for happy-path continuation only.** + +**Release is the sign-off orchestrator and consolidates cross-role review outcomes.** + Each role: 1. Reads its required upstream artifacts (defined below) @@ -26,25 +32,74 @@ and stops — it does not proceed with partial context. ### artifact hand-off table -| Role | Reads | Writes | -| --------- | --------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------- | -| product | (nothing — initiates) | `docs/product/vision.md`, `docs/product/requirements.md`, `docs/product/roadmap.md` | -| architect | `docs/product/vision.md`, `docs/product/requirements.md` | `docs/architecture/architecture.md`, `docs/architecture/adr/*.md` | -| designer | `docs/product/vision.md`, `docs/product/requirements.md`, `docs/architecture/architecture.md`, `docs/architecture/adr/*.md` | `docs/design/design.md`, API specs | -| engineer | `docs/product/requirements.md`, `docs/design/design.md`, `docs/architecture/adr/*.md` | code, unit tests | -| tester | `docs/product/requirements.md`, source files, config | `docs/test-report.md`, `docs/security-report.md`, `docs/performance-baseline.md` | -| release | `docs/test-report.md`, `docs/security-report.md`, `docs/performance-baseline.md`, user sign-off | `docs/releases/{date}.md`, `CHANGELOG.md`, release PR | +This table describes the conceptual read/write relationships between roles. +Default artifact paths are defined in ADR-021 and configured per-project in each +agent's `config.yaml`. Paths are overridable; the hand-off relationships below are fixed. + +- **`product`** + + - Reads: *(nothing — initiates)* + - Writes: vision, requirements, roadmap + +- **`architect`** + + - Reads: product artifacts + - Writes: architecture overview, ADRs + +- **`designer`** + + - Reads: product artifacts, architecture artifacts + - Writes: design overview, API specs, UX artifacts (frontend scope only) + +- **`engineer`** + + - Reads: product artifacts, architecture artifacts, design artifacts + - Writes: source code, unit tests, RCA and post-mortem artifacts + +- **`tester`** + + - Reads: architecture artifacts, design artifacts, source files + - Writes: test report, security report, performance baseline + +- **`release`** + + - Reads: product artifacts, architecture artifacts, design artifacts, tester reports, user sign-off + - Writes: release document, changelog, release PR ### user gate moments -There are 4 explicit user gate moments: +There are explicit user gate moments after each stage output and before merge: + +| Gate | Trigger | Required | +| -------------------------------- | --------------------------------------- | ------------------------------------- | +| **1. Product approval** | After `product` updates scope artifacts | User approves before architect starts | +| **2. Architecture approval** | After `architect` updates artifacts | User approves before designer starts | +| **3. Design approval** | After `designer` updates artifacts | User approves before engineer starts | +| **4. Implementation checkpoint** | After `engineer` updates code/tests | User approves before tester starts | +| **5. Verification approval** | After `tester` reports are ready | User approves before release starts | +| **6. Merge approval** | Before `release` creates PR | User approves final merge | + +### handoff policy + +Handoffs are UI accelerators for the happy path, not orchestration logic. + +- Allowed pattern: `Go to next stage: ` (single forward continuation) +- Disallowed: back/side/escalation handoff buttons +- For `NOK` or blockers, user explicitly chooses the recovery path + +### release sign-off consolidation + +Release gathers explicit `OK`/`NOK` reviews from upstream role perspectives +(typically tester, architect, designer, and product) and records a unified +decision matrix before PR creation. + +The sign-off payload is standardized: -| Gate | Trigger | Required | -| ---------------------------- | ---------------------------------------------------- | ------------------------------------- | -| **1. Requirements approval** | After `product` writes requirements.md | User approves before architect starts | -| **2. Design approval** | After `architect` + `designer` write their artifacts | User approves before engineer starts | -| **3. Pre-prod sign-off** | After `tester` reports are ready | User approves before release starts | -| **4. Merge approval** | Before `release` creates PR | User approves final merge | +1. Verdict (`OK` or `NOK`) +1. Reviewed scope +1. Gaps or deviations +1. Impact/risk summary +1. Required next action and owner ## alternatives considered diff --git a/docs/architecture/adr/012-flat-templates-install-time-generation.md b/docs/architecture/adr/012-flat-templates-install-time-generation.md index 66ab3a9..41cbb86 100644 --- a/docs/architecture/adr/012-flat-templates-install-time-generation.md +++ b/docs/architecture/adr/012-flat-templates-install-time-generation.md @@ -30,9 +30,26 @@ src/vstack/_templates/ ├── skills// │ ├── config.yaml │ └── template.md -└── agents// - ├── config.yaml - └── template.md +├── agents// +│ ├── config.yaml +│ └── template.md +├── instructions// +│ ├── config.yaml +│ └── template.md +├── prompts// +│ ├── config.yaml +│ └── template.md +├── docs/ ← baseline doc stubs, seeded by vstack install +│ ├── product/ +│ ├── architecture/ +│ ├── design/ +│ ├── tests/ +│ └── releases/ +└── project/ ← .vstack/ project config templates, seeded by vstack install + ├── config.yaml + └── templates/ + ├── changes/RFC-template.md + └── issues/overview-template.md ``` ### 2. no direct edits to generated output @@ -40,23 +57,27 @@ src/vstack/_templates/ In the vstack source repo, generated output under `.github/` is install-time output and should not be edited directly. -### 3. install-time generation with `vstack install` +### 3. two-command generation model + +vstack uses two distinct commands with different lifecycles: -At install time (`vstack install` or `vstack install --target `), the -installer generates artifacts into `.github/`, including: +**`vstack install`** — first-run project setup (run once per project): -- `.github/skills//SKILL.md` -- `.github/agents/.agent.md` -- `.github/prompts/*.prompt.md` -- `.github/instructions/*.instructions.md` +- Creates `.vstack/` with `config.yaml` and `templates/` +- Seeds `docs/` baseline stubs (product, architecture, design, tests, releases) +- Calls `vstack init` internally to generate `.github/` artifacts +- All seeded files are project-owned and never overwritten by vstack after initial creation -The installer is conservative by default: +**`vstack init`** — idempotent artifact regeneration (run in CI and after vstack upgrades): -- existing files that are not tracked in `vstack.json` are preserved -- tracked files are updated only when their content still matches the last installed SHA-256 checksum -- `--adopt-name ` allows explicitly adopting one unmanaged file into tracking without overwriting it -- `--force-name ` can override preservation for one artifact -- `--force` is the explicit opt-in for overwriting local changes +- Generates `.github/` artifacts from current templates: + - `.github/skills//SKILL.md` + - `.github/agents/.agent.md` + - `.github/prompts/*.prompt.md` + - `.github/instructions/*.instructions.md` +- Conservative by default (see ADR-020) +- Adds new `.vstack/templates/` files from newer vstack versions, never overwrites existing ones +- Manifest (`vstack.json`) tracks only `.github/` artifacts, not project-owned files The same manifest checksum is also used for safe uninstall and status reporting: diff --git a/docs/architecture/adr/014-manifest-schema-versioning.md b/docs/architecture/adr/014-manifest-schema-versioning.md index 5ed7d98..e58c194 100644 --- a/docs/architecture/adr/014-manifest-schema-versioning.md +++ b/docs/architecture/adr/014-manifest-schema-versioning.md @@ -80,6 +80,28 @@ side-effect-free (NFR alignment). The `allow_legacy=True` parameter on `ManifestFile.read` is the single code-level bypass, used only by the upgrade command itself. +### manifest file location + +`vstack.json` lives at `.vstack/vstack.json`. This separates machine-generated +state (`.vstack/vstack.json`) from Copilot artifacts (`.github/`), and aligns with +the principle that `.vstack/` is the vstack project-scope directory (see ADR-019). + +The format distinction is intentional: `vstack.json` uses JSON (machine-generated, +not hand-edited), while `.vstack/config.yaml` uses YAML (human-authored). The +format difference signals ownership — JSON files are vstack-owned, YAML files are +project-owned. + +`vstack manifest upgrade` handles two migration paths: + +1. **Schema migration**: increments `manifest_version` and backfills missing fields + (existing behaviour, ADR-017). +1. **Location migration** (new in 3.0): moves `.github/vstack.json` to + `.vstack/vstack.json` when the old path is detected and the new path does not + yet exist. + +Older manifests at `.github/vstack.json` are detected and rejected with a clear +message pointing to `vstack manifest upgrade`. + ## consequences ### positive diff --git a/docs/architecture/adr/015-conservative-install-by-default.md b/docs/architecture/adr/015-conservative-install-by-default.md index 55eb828..a2a9e0a 100644 --- a/docs/architecture/adr/015-conservative-install-by-default.md +++ b/docs/architecture/adr/015-conservative-install-by-default.md @@ -3,7 +3,7 @@ > Maintained by: **architect** role **date:** 2026-04-26\ -**status:** accepted +**status:** superseded by [ADR-020](020-install-init-command-semantics.md) ## context diff --git a/docs/architecture/adr/017-checksum-backfill-on-upgrade.md b/docs/architecture/adr/017-checksum-backfill-on-upgrade.md index 2055885..f55e0c6 100644 --- a/docs/architecture/adr/017-checksum-backfill-on-upgrade.md +++ b/docs/architecture/adr/017-checksum-backfill-on-upgrade.md @@ -186,7 +186,7 @@ entries. ## impact on future orchestrated pipeline No direct impact. Checksum backfill is a manifest-maintenance operation. It would not -change if a multi-role orchestration layer is introduced in Option B. +change if a multi-role orchestration layer is introduced. ______________________________________________________________________ diff --git a/docs/architecture/adr/018-skill-genericity-boundary.md b/docs/architecture/adr/018-skill-genericity-boundary.md index 216fe34..d95d988 100644 --- a/docs/architecture/adr/018-skill-genericity-boundary.md +++ b/docs/architecture/adr/018-skill-genericity-boundary.md @@ -3,7 +3,7 @@ > Maintained by: **architect** role **date:** 2026-05-02\ -**status:** accepted +**status:** accepted — artifact paths made configurable via ADR-021 ## context @@ -12,8 +12,8 @@ sequencing that belongs to the agent layer, not the skill layer. Examples observed: -- `release-notes` references `docs/releases/{date}.md`, `docs/test-report.md`, - `docs/security-report.md` as required inputs. +- `release-notes` references `docs/releases/{date}.md`, `docs/reports/test-report.md`, + `docs/reports/security-report.md` as required inputs. - `pr` references `docs/releases/{date}.md` as the PR body source and enforces a vstack-specific release checklist. @@ -41,7 +41,7 @@ This ADR extends that boundary to the skill vs agent layer. ### rules 1. A skill must not reference named project artifact paths (e.g. `docs/releases/`, - `docs/test-report.md`). These belong in the agent template. + `docs/reports/test-report.md`). These belong in the agent template. 1. A skill must not enforce a role-sequencing gate (e.g. "verify tester sign-off before proceeding"). Gates belong in the agent template. 1. A skill may reference generic placeholders (e.g. `{release-notes-file}`, @@ -73,7 +73,7 @@ owns the artifact hand-off contract per ADR-010. 1. **Use a templating mechanism to inject artifact paths at install time** — considered: would allow per-consumer customisation. Not adopted now because it adds generator complexity. Revisit if consumer customisation becomes a first-class requirement - (see Option B pipeline roadmap item). + (see orchestrated pipeline roadmap item). ## rationale @@ -85,16 +85,16 @@ Agents are already the correct place for workflow-specific context: they declare scope, artifact contracts, and gate moments. Moving project-specific paths there makes the coupling explicit, inspectable, and maintainable in one place. -## impact on Option B pipeline +## impact on orchestrated pipeline -In Option B, each pipeline stage has declared input and output artifacts. This decision -aligns the current Option A model with that design: agents declare artifact contracts, -skills are stateless procedures. When Option B is implemented, the agent artifact -declarations become the pipeline stage contracts without requiring skill changes. +In the orchestrated pipeline, each pipeline stage has declared input and output artifacts. +This decision aligns direct execution with that design: agents declare artifact contracts, +skills are stateless procedures. When the orchestrated pipeline is implemented, the agent +artifact declarations become the pipeline stage contracts without requiring skill changes. ## references - [ADR-009: Role model](009-role-model.md) - [ADR-010: Artifact hand-off pipeline](010-artifact-flow.md) - [ADR-013: Policy vs procedure boundary](013-instructions-vs-skills-boundary.md) -- [ADR-004: Option A to B pipeline](004-option-a-to-b-pipeline.md) +- [ADR-004: Direct execution and orchestrated pipeline](004-option-a-to-b-pipeline.md) diff --git a/docs/architecture/adr/019-vstack-project-directory.md b/docs/architecture/adr/019-vstack-project-directory.md new file mode 100644 index 0000000..48c1b0b --- /dev/null +++ b/docs/architecture/adr/019-vstack-project-directory.md @@ -0,0 +1,156 @@ +# ADR-019: `.vstack/` Project-Scope Directory + +> Maintained by: **architect** role + +**date:** 2026-05-03\ +**status:** accepted + +## context + +vstack installs Copilot artifacts into `.github/` and tracks them via a manifest. +However, there was no designated location for project-level configuration, doc +baseline stubs, or role-scoped doc templates that belong to the consuming project +rather than to vstack's generated output. + +Previously, `vstack.json` lived at `.github/vstack.json`, mixing machine-generated +tracking state with Copilot artifacts. There was also no place for: + +- Install configuration (e.g. which artifact types to exclude). +- Doc structure stubs seeded once and then owned by the project. +- Role-scoped doc templates for architecture, design, and release artifacts. + +The question was: where should project-scoped, vstack-related state and +configuration live, and how should it be separated from generated Copilot artifacts? + +## decision + +Introduce `.vstack/` as the project-scope directory for all vstack-related state +and configuration that belongs to the project, not to the generated Copilot output. + +``` +.vstack/ +├── config.yaml ← human-authored install configuration (YAML) +├── vstack.json ← machine-generated manifest (JSON, moved from .github/) +├── templates/ ← doc templates, seeded once then project-owned +│ ├── architect/ +│ ├── designer/ +│ ├── engineer/ +│ ├── product/ +│ ├── release/ +│ └── tester/ +└── tmp/ ← scratch space, listed in .gitignore +``` + +`.vstack/` is created by `vstack install` (first-run setup) and is committed to +version control. `tmp/` is the only subdirectory excluded from git. + +### file ownership and update policy + +| File | Owner | vstack overwrites? | +| ---------------------- | --------- | ------------------------------------------------------------- | +| `.vstack/config.yaml` | project | Never | +| `.vstack/vstack.json` | vstack | Yes (machine-generated) | +| `.vstack/templates/**` | project | Additive only — new files added, existing files never touched | +| `.vstack/tmp/**` | ephemeral | Not tracked | + +### format convention + +Two formats are used intentionally, signalling ownership: + +- **JSON** (`vstack.json`) — machine-generated state. vstack owns it; do not + hand-edit. The JSON format discourages accidental manual modification. +- **YAML** (`config.yaml`) — human-authored configuration. The project team owns + it. YAML supports comments and is easier to read and maintain by hand. + +This is analogous to `package-lock.json` (machine-generated) alongside +`package.json` (human-authored) in the npm ecosystem. + +### `config.yaml` schema + +```yaml +# vstack install — first-run setup. Seeds this file if missing (never overwrites). +# Then runs init to generate .github/ artifacts from templates. +# Use once per project, or when onboarding a new machine. +# +# vstack init — idempotent regeneration. Reads this file on every run. +# Safe to re-run in CI after pip install --upgrade vstack. + +# Selective install exclusions — remove or comment out to install everything. +# +# exclude: +# skills: +# - terraform +# - terragrunt +# - helm +# - k8s +# instructions: all +# prompts: all + +# Root directory for generated agent artifact paths. +# Default: docs +# +# artifacts: +# root: docs +``` + +All fields are optional. An absent `exclude:` block means install everything. +Artifact type values are either a list of names to skip, or `all` to skip +the entire type. `agents` cannot be excluded — the 6-role chain is an atomic unit. + +The `artifacts: root:` key overrides the path prefix used in generated agent files +(e.g. links to architecture docs). Default is `docs`. + +### scope boundary + +`.vstack/` is repo-scoped only. Global installs (`vstack install --global`) write +Copilot artifacts directly to the VS Code user data directory and have no project +context — they do not create a `.vstack/` directory. + +## alternatives considered + +### Option A: Keep everything in `.github/` + +**Pros:** Single directory; no new convention. + +**Cons:** Mixes project-owned config with generated artifacts. Makes it harder to +determine what vstack regenerates vs what the project owns. `vstack.json` adjacent +to `agents/` and `skills/` creates confusion about what is source vs state. + +**Why rejected:** Ownership clarity is a first-class concern. Generated output +(`.github/`) and project state (`.vstack/`) serve different purposes and have +different update policies. + +### Option B: Use a dotfile (e.g. `.vstackrc` or `.vstack.yaml`) + +**Pros:** Familiar single-file pattern. + +**Cons:** A single file cannot accommodate both machine-generated state (`vstack.json`) +and project templates. Would require multiple dotfiles, which is more scattered than +a single directory. + +**Why rejected:** A directory naturally groups related concerns. + +### Option C: Use an existing directory (e.g. `.config/vstack/`) + +**Pros:** Follows XDG conventions. + +**Cons:** Less discoverable; `.vstack/` is self-explanatory and consistent with +tool naming conventions (`.terraform/`, `.nx/`, `.husky/`). + +**Why rejected:** `.vstack/` is more immediately recognisable to users of the tool. + +## rationale + +A dedicated `.vstack/` directory makes ownership explicit: everything in `.github/` +is generated by vstack and can be regenerated at any time; everything in `.vstack/` +(except `vstack.json`) is project-owned and preserved. The format difference between +`vstack.json` (JSON) and `config.yaml` (YAML) reinforces this boundary without +requiring documentation. + +## impact on future orchestrated pipeline + +The `.vstack/config.yaml` file provides install configuration that `vstack init` +reads to determine which artifact types to generate. Additional configuration +concepts (model preferences, artifact path overrides, template overlays) may be +added to this schema as they are implemented — only fields with an active code +afnemer are documented here. diff --git a/docs/architecture/adr/020-install-init-command-semantics.md b/docs/architecture/adr/020-install-init-command-semantics.md new file mode 100644 index 0000000..8bb8d63 --- /dev/null +++ b/docs/architecture/adr/020-install-init-command-semantics.md @@ -0,0 +1,132 @@ +# ADR-020: `install` and `init` Command Semantics + +> Maintained by: **architect** role + +**date:** 2026-05-03\ +**status:** accepted\ +**supersedes:** [ADR-015](015-conservative-install-by-default.md) + +## context + +vstack originally had a single `vstack install` command that both set up a project +and regenerated Copilot artifacts on subsequent runs. This conflated two distinct +concerns: + +1. **First-run project setup** — creating `.vstack/`, seeding `docs/` stubs, + writing `config.yaml`, and generating `.github/` artifacts for the first time. +1. **Ongoing artifact regeneration** — idempotently regenerating `.github/` after + a vstack upgrade or template change. + +This mismatch was confusing: the same command behaved differently depending on +whether `.vstack/` existed. CI pipelines ran `vstack install`, which implied +"installing" something on every pipeline run — not the expected mental model. + +The conventional software model separates these concerns clearly: + +- `install` = first-time setup, run once (analogous to `npm install`, `terraform init`) +- `init` = idempotent, safe to re-run (analogous to `git init`, `terraform init`) + +vstack had these semantics reversed. + +## decision + +**`vstack install`** is the first-run project setup command. It: + +- Creates `.vstack/` with `config.yaml`, `vstack.json`, and `templates/` +- Seeds `docs/` baseline stubs (additive — never overwrites existing files) +- Calls `vstack init` internally to generate `.github/` artifacts +- Is intended to be run once per project +- Is safe to re-run: already-existing files in `.vstack/` and `docs/` are skipped + +**`vstack init`** is the idempotent artifact regeneration command. It: + +- Generates or updates `.github/` artifacts from current vstack templates +- Is the command to run in CI pipelines and after `pip install --upgrade vstack` +- Adds new `.vstack/templates/` files from newer vstack versions (additive only) +- Is conservative by default for `.github/` artifacts (inherits ADR-015 policy): + - Untracked files in `.github/` are never overwritten + - Tracked files with local modifications are skipped and reported + - Tracked clean files are replaced + - `--force` overrides all preservation for explicit upgrade + +**`vstack manifest upgrade`** handles breaking migrations (schema changes, file +location changes) and remains a separate, explicit command. + +### conservative policy for `.github/` (inherited from ADR-015) + +| Flag | Behaviour | +| --------------------- | ------------------------------------------------------------------------------ | +| _(default)_ | Skip untracked files; skip modified tracked files; replace clean tracked files | +| `--force` | Overwrite all target files | +| `--force-name ` | Overwrite one named artifact | +| `--adopt-name ` | Take ownership of an untracked file without overwriting | +| `--update` | Overwrite clean tracked files; skip locally modified files | + +### additive policy for `.vstack/templates/` and `docs/` + +Files seeded by `vstack install` into `.vstack/templates/` and `docs/` are +project-owned after creation. vstack will never overwrite them. On subsequent +`vstack init` runs, only missing files are added (additive-only). Projects can +freely modify seeded files. + +### `vstack manifest upgrade` migration path (3.0) + +In addition to existing schema migration (ADR-017), `vstack manifest upgrade` in +3.0 also handles location migration: + +- Detects `.github/vstack.json` (legacy location) +- Moves it to `.vstack/vstack.json` +- Updates internal paths accordingly + +Other `vstack` commands fail fast when `.github/vstack.json` is detected without a +corresponding `.vstack/vstack.json`, directing users to `vstack manifest upgrade`. + +## alternatives considered + +### Option A: Keep `vstack install` for both setup and CI regeneration + +**Pros:** Single command; no migration burden. + +**Cons:** Semantically incorrect — running "install" in CI on every build does not +match developer expectations. Forces consumers to explain why they run `install` +repeatedly. Conflates setup and maintenance into one command. + +**Why rejected:** Mental model clarity matters for a developer tool. The confusion +was a recurring source of questions. + +### Option B: Introduce `vstack sync` or `vstack update` for CI + +**Pros:** Avoids overloading `install` or `init`. + +**Cons:** Neither `sync` nor `update` is as universally understood as `init`. +`init` already carries the "idempotent setup" meaning from `git init`, +`terraform init`, and similar tools. + +**Why rejected:** `init` is the established convention; inventing a new verb adds +vocabulary without benefit. + +### Option C: Single `vstack init` command for everything (no `install`) + +**Pros:** Minimal surface area; one command to learn. + +**Cons:** No semantic distinction between first-run and ongoing use. Users cannot +tell from the command whether it will create new directories and files or only +regenerate existing artifacts. + +**Why rejected:** The distinction between "set up a project" and "keep artifacts +current" is meaningful and worth exposing. + +## rationale + +Aligning with the convention established by `git init`, `terraform init`, and +`npm install` reduces cognitive load. `install` as a first-run wizard matches how +developers think about onboarding a new tool; `init` as an idempotent regenerator +matches how developers think about keeping generated files current. The separation +also makes CI configuration self-documenting: `vstack init` in a workflow step +clearly communicates "regenerate artifacts". + +## impact on future orchestrated pipeline + +The orchestrated pipeline (ADR-004) would invoke `vstack init` to ensure +Copilot artifacts are current before executing role stages, not `vstack install`. +This is consistent with the idempotent, side-effect-free intent of `init`. diff --git a/docs/architecture/adr/021-config-driven-artifact-paths.md b/docs/architecture/adr/021-config-driven-artifact-paths.md new file mode 100644 index 0000000..41d930a --- /dev/null +++ b/docs/architecture/adr/021-config-driven-artifact-paths.md @@ -0,0 +1,126 @@ +# ADR-021: Config-Driven Artifact Paths in Agent Config + +> Maintained by: **architect** role + +**date:** 2026-05-03\ +**status:** accepted + +## context + +Each vstack agent role owns a well-defined set of documentation artifacts. These +paths — `docs/architecture/overview.md`, `docs/product/vision.md`, and so on — +were previously documented only in agent `template.md` bodies and in +`docs/design/agents.md`. They existed in two places in prose form: the agent +instructions and the design documentation. + +This created two problems: + +1. **No machine-readable source of truth.** Tooling, validators, and future + orchestrated pipeline stages (ADR-004) had no structured way to discover which + agent produces or consumes which paths without parsing Markdown. + +1. **Drift risk.** When an artifact path changes, the update had to be applied + independently to the template body, the design docs, and any tooling that + relied on the path. There was no single authoritative record. + +The manifest schema (ADR-014) tracks which files were installed; it does not +describe which files an agent conceptually owns. A separate schema field is +needed to capture ownership at the agent config level. + +## decision + +Agent `config.yaml` files gain an optional `artifacts:` block: + +```yaml +artifacts: + dir: architecture # subdirectory within the global docs root (no root prefix) + input: # paths this agent reads as context + - product/**/*.md + output: # files this agent produces + - overview.md # string: resolved as // + - path: ux.md # dict: allows an optional notes annotation + notes: frontend/fullstack scope only + - path: ./src/**/* # ./ prefix: verbatim path, dir prefix not applied +``` + +Rules: + +- `dir` is the subdirectory within the global `ARTIFACTS_DOCS_ROOT` (`docs`) that + this agent writes to. It is a subdirectory name only — no root prefix. Required + when `output` items are relative paths without a `./` prefix; optional otherwise. +- `input` is a list of glob patterns for files this agent reads. Paths are resolved + as `/` (e.g. `product/**/*.md` → `docs/product/**/*.md`). +- `output` is a list of items, each either a plain string or a dict with `path` and + an optional `notes` field. Path resolution: + - No `./` prefix, `dir` set → `//` + - No `./` prefix, no `dir` → `` verbatim + - `./` prefix → strip `./`, use remainder verbatim (`./src/**/*` → `src/**/*`) +- `ARTIFACTS_DOCS_ROOT` is a global constant (`src/vstack/constants.py`, default + `docs`). It is applied at render time. Individual agents must not embed the root + prefix; only the subdirectory is set in `dir`. +- The `artifacts:` block is **vstack-internal only** — it is not emitted to the + generated `.agent.md` frontmatter. +- At install time, `AgentGenerator` resolves the block into four placeholder tokens + (`{{AGENT_ARTIFACTS_INPUT}}`, `{{AGENT_ARTIFACTS_OUTPUT}}`, + `{{AGENT_ARTIFACTS_INPUT_COMMENTS}}`, `{{AGENT_ARTIFACTS_OUTPUT_COMMENTS}}`) + that are substituted into the template body. + +### per-role values + +| Agent | dir | input | output | +| ----------- | -------------- | ----------------------------------------------------------- | ------------------------------------------------------------------------------------------------- | +| `product` | `product` | — | `vision.md`, `requirements.md`, `roadmap.md`, `changes/*.md`, `issues/*.md` | +| `architect` | `architecture` | `product/**/*.md` | `overview.md`, `adr/*.md` | +| `designer` | `design` | `architecture/**/*.md` | `overview.md`, `ux.md` *(frontend/fullstack)*, `**/*.md` *(when scope warrants)* | +| `engineer` | — | `product/**/*.md`, `architecture/**/*.md`, `design/**/*.md` | `./src/**/*`, `./tests/**/*`, `./issues/{id}-{slug}-rca.md`, `./issues/{id}-{slug}-postmortem.md` | +| `tester` | `reports` | `architecture/**/*.md`, `design/**/*.md` | `**/*.md`, `./tests/**/*` | +| `release` | `releases` | `**/*.md` | `*.md` *(release notes and sign-off record)* | + +`engineer` has no `dir` because its outputs are code files under `src/` and +`tests/`, addressed with `./` verbatim paths rather than documentation +subdirectories. + +## alternatives considered + +### 1. Encode paths only in template bodies + +Paths remain in `template.md` prose. No schema change required. + +Rejected: paths stay human-only, untestable, and invisible to any future +automation or orchestrated pipeline tooling. + +### 2. Separate paths manifest file + +A standalone `paths.yaml` or `artifact-registry.yaml` at the repo root lists all +role-to-path mappings. + +Rejected: splits ownership away from the agent config. A single `config.yaml` +per agent keeps all agent-level facts co-located. + +### 3. Embed paths in the generated `.agent.md` frontmatter + +Emit `artifacts:` into the VS Code agent file. + +Rejected: VS Code does not define `artifacts:` as a supported frontmatter field. +Emitting unknown fields risks warnings or future parse errors. This schema is +vstack-internal. + +## rationale + +Placing `artifacts:` in `config.yaml` is consistent with the existing pattern: +`version`, `handoffs`, and other vstack-internal fields already live in +`config.yaml` without being emitted. The field is co-located with the agent it +describes, machine-readable, and immediately useful for orchestrated pipeline validation without requiring a separate registry file. + +## impact on orchestrated pipeline + +ADR-004 describes a future sequential multi-agent pipeline where each stage's +output feeds the next. The `artifacts:` schema directly enables this: + +- A pipeline orchestrator can read `input` to know what context to inject. +- It can read `output` to know which files to collect and pass to the next stage. +- `dir` and `ARTIFACTS_DOCS_ROOT` together provide the base path for file + resolution without parsing agent prose. + +No changes to the orchestrated pipeline design are needed; this ADR makes `config.yaml` the +structured input source that orchestrated pipeline tooling will consume. diff --git a/docs/architecture/adr/022-selective-exclude-filter.md b/docs/architecture/adr/022-selective-exclude-filter.md new file mode 100644 index 0000000..8bcac07 --- /dev/null +++ b/docs/architecture/adr/022-selective-exclude-filter.md @@ -0,0 +1,101 @@ +# ADR-022: Selective exclude filter in `.vstack/config.yaml` + +> Maintained by: **architect** role + +**date:** 2026-05-06\ +**status:** accepted + +## context + +vstack installs Copilot artifacts — agents, skills, instructions, and prompts — into `.github/` +at install or init time. Before this change, every run regenerated all artifact types +unconditionally. Teams adopting vstack in an existing repo often want only a subset of artifact +types: a platform team may need agents and instructions but has no use for skills or prompts; a +team that already maintains its own prompt library does not want vstack's prompts landing in their +repository. + +The existing `--only` CLI flag allows callers to restrict a single run to specific artifact types, +but it is transient. A team running `vstack init` in CI must repeat the flag on every invocation. +There was no mechanism for a project to encode its artifact preferences once and have them honored +automatically on every subsequent install. + +## decision + +An `exclude:` block is added to `.vstack/config.yaml`. It supports two modes per artifact type: + +- **Full-type exclusion** — setting a type's value to `all` (e.g. `skills: all`) removes every + artifact of that type from the effective install set. +- **Name-level exclusion** — setting a type's value to a list (e.g. `instructions: [security, testing]`) + installs all artifacts of that type except the named ones. + +Example configuration: + +```yaml +exclude: + skills: all + prompts: all + instructions: [security, testing] +``` + +Agents cannot be excluded. The six-role agent chain is treated as an atomic unit; vstack's +execution model depends on all six roles being present, and allowing partial agent installs would +produce a broken workflow for the consumer. + +Config keys use plural forms (`skills`, `agents`, `instructions`, `prompts`) for readability and +are mapped to singular internal type names at parse time via a `_CONFIG_TYPE_ALIAS` table in +`interface.py`. The implementation is split across two layers: full-type exclusions are resolved +at the `CommandLineInterface` level by filtering `effective_only` before dispatch, while +name-level exclusions are carried in `CommandContext.excluded_names` and applied per-artifact +inside the `init` command's generate loop. This keeps the type-routing logic central and the +per-artifact skip logic local to generation. + +When the config file is absent or the `exclude:` key is not present, the filter is a no-op and +all artifacts are installed, preserving full backward compatibility. When `--only` is also +supplied, type exclusions subtract from the explicit `--only` set; they narrow the effective set +but never override an explicit inclusion. + +## alternatives considered + +**`--only` CLI flag alone.** The flag already exists and handles ad-hoc type filtering. It was +not sufficient here because project-level preferences must survive across all invocations — in CI, +in local onboarding, and in automated upgrade flows — without requiring every caller to repeat the +flag. Encoding the preference in config is the correct level of abstraction. + +**Per-type boolean flags in config** (`install_skills: false`). This approach is more verbose, +requires a separate key per type, and does not compose naturally with name-level exclusions +without additional design. The `exclude:` block with `all`/list values is more compact, expresses +both modes under a single key, and leaves room for future glob-based partial exclusion without a +config format change. + +**Allowlist instead of denylist** (`include: [agents, instructions]`). An allowlist requires +teams to update their config whenever vstack adds a new artifact type. A denylist is +forward-compatible by default: new types are installed unless explicitly excluded, which matches +the expected behavior for most consumers upgrading vstack. + +## rationale + +The `exclude:` filter is the minimal solution for the two most common adoption patterns: teams +that want agents and instructions only, and teams that already have their own skills or prompts +and do not want vstack's versions to land in their repository. It introduces no new concepts +beyond what is already present in the config schema and composes cleanly with `--only` without +changing install semantics for teams that do not configure it. + +Keeping agents non-excludable is a deliberate constraint rather than an oversight. The six-role +model (ADR-009) is the structural unit of vstack; partially removing roles would leave consumers +with a workflow that references agents that do not exist. If a team genuinely does not need all +six roles, the correct solution is a future scoped-install mode, not silent omission of individual +agents. + +Splitting the implementation between `CommandLineInterface` (type filtering) and the generate +loop (name filtering) reflects the natural ownership boundary: the CLI layer owns which types +enter the pipeline, and the generator layer owns which named artifacts within a type are emitted. +This avoids coupling artifact-level skip logic to the routing layer and keeps each layer testable +in isolation. + +## impact on orchestrated pipeline + +Low. The filter operates entirely before artifact generation, at context construction time. In a +multi-agent sequential pipeline (ADR-004), each stage receives `CommandContext` as its +input; `excluded_names` and `effective_only` travel with the context unchanged. No stage-level +changes are required to honor the filter. The constraint that agents cannot be excluded aligns +naturally with the orchestrated pipeline's dependency on all six roles being available as pipeline stages. diff --git a/docs/architecture/architecture.md b/docs/architecture/overview.md similarity index 62% rename from docs/architecture/architecture.md rename to docs/architecture/overview.md index d092539..d791b84 100644 --- a/docs/architecture/architecture.md +++ b/docs/architecture/overview.md @@ -29,18 +29,24 @@ vstack/ │ ├── manifest/ ← Manifest, ManifestFile, ArtifactEntry, checksums │ ├── cli/ ← interface, registry, service, per-command handlers, helpers │ └── _templates/ ← source templates for all artifact types +│ ├── skills/, agents/, instructions/, prompts/ +│ ├── docs/ ← baseline doc stubs (seeded by vstack install) +│ └── project/ ← .vstack/ config and artifact starter templates ├── docs/ │ ├── architecture/ ← architecture docs + ADRs │ ├── design/ ← design, workflow, skills, instructions │ └── product/ ← roadmap, requirements, vision ├── tests/ │ └── vstack/ -├── .github/ ← generated output (never edit directly) +├── .vstack/ ← project-scope vstack state (committed) +│ ├── config.yaml ← human-authored project config (YAML) +│ ├── vstack.json ← machine-generated manifest (JSON) +│ └── templates/ ← project-owned artifact starter templates (seeded by vstack install) +├── .github/ ← generated Copilot artifacts (never edit directly) │ ├── skills//SKILL.md │ ├── agents/.agent.md │ ├── instructions/.instructions.md -│ ├── prompts/.prompt.md -│ └── vstack.json +│ └── prompts/.prompt.md └── README.md ``` @@ -84,29 +90,33 @@ Key resolvers defined inline in the generator: vstack uses 6 fixed agent roles. Each role has defined skill access and artifact ownership. See `docs/architecture/adr/009-role-model.md` for the decision record. -| Role | Artifact ownership | -| --------- | ----------------------------------------------------------------------------------- | -| product | `docs/product/vision.md`, `docs/product/requirements.md`, `docs/product/roadmap.md` | -| architect | `docs/architecture/architecture.md`, `docs/architecture/adr/*.md` | -| designer | `docs/design/design.md`, `docs/design/ux.md` (frontend scope), API specs | -| engineer | code, unit tests | -| tester | `docs/test-report.md`, `docs/security-report.md`, `docs/performance-baseline.md` | -| release | `docs/releases/{date}.md`, `CHANGELOG.md`, release PR | +| Role | Artifact ownership | +| --------- | -------------------------------------------------------------------------------------------------------- | +| product | `docs/product/vision.md`, `docs/product/requirements.md`, `docs/product/roadmap.md` | +| architect | `docs/architecture/overview.md`, `docs/architecture/adr/*.md` | +| designer | `docs/design/` (overview.md, ux.md, agents.md, skills.md, instructions.md, workflow.md, cicd.md) | +| engineer | code, unit tests | +| tester | `docs/reports/test-report.md`, `docs/reports/security-report.md`, `docs/reports/performance-baseline.md` | +| release | `docs/releases/YYYYMMDDNN.md`, `CHANGELOG.md`, release PR | -### 5. manifest (`vstack.json`) +### 5. manifest (`.vstack/vstack.json`) -Generated at install time in the target directory. Tracks every artifact installed -by `vstack install` (skills, agents, instructions, and prompts), including a per-file -SHA-256 checksum, version, and algorithm so that: +Generated at install time at `.vstack/vstack.json`. Tracks every `.github/` artifact +installed by `vstack init` (skills, agents, instructions, and prompts), including a +per-file SHA-256 checksum, version, and algorithm so that: - `vstack uninstall` removes exactly the files it installed. -- `install --update` detects local modifications before rewriting. +- `init --update` detects local modifications before rewriting. - `verify` / `status` report checksum drift and ownership state. -- `manifest upgrade` migrates legacy schema to the current version. +- `manifest upgrade` migrates legacy schema and file location to the current version. + +The manifest uses JSON format (machine-generated, not hand-edited). Project +configuration uses YAML (`config.yaml`). The format difference signals ownership. +See ADR-019. The manifest schema is versioned (`manifest_version` field). Operations that require the current schema fail fast with an upgrade hint rather than silently misbehaving. -See ADR-014 and ADR-015. +See ADR-014 and ADR-020. Writes are atomic: content is staged to a sibling `.tmp` file and promoted with `os.replace` so a crash or `KeyboardInterrupt` cannot produce a partially-written @@ -144,14 +154,14 @@ The CLI layer translates argparse input into domain operations through a small s focused components. See `docs/design/design.md` for the full component table and dispatch flow. -| Component | Responsibility | -| ------------------------ | ----------------------------------------------------------------------------------- | -| `CommandLineInterface` | Facade: parser construction, service creation, target/scope resolution, dispatch | -| `CommandService` | Shared coordinator: generators, path labelling, manifest access, artifact state | -| `build_command_registry` | Maps command names to `BaseCommand` instances | -| `BaseCommand` | ABC contract: all handlers implement `run(*, context: CommandContext) → int` | -| Per-command modules | `install`, `verify`, `status`, `uninstall`, `validate`, `manifest` — one class each | -| `helpers.py` | Shared install/uninstall utilities (name normalization, manifest preservation) | +| Component | Responsibility | +| ------------------------ | ------------------------------------------------------------------------------------------- | +| `CommandLineInterface` | Facade: parser construction, service creation, target/scope resolution, dispatch | +| `CommandService` | Shared coordinator: generators, path labelling, manifest access, artifact state | +| `build_command_registry` | Maps command names to `BaseCommand` instances | +| `BaseCommand` | ABC contract: all handlers implement `run(*, context: CommandContext) → int` | +| Per-command modules | `install`, `init`, `verify`, `status`, `uninstall`, `validate`, `manifest` — one class each | +| `helpers.py` | Shared install/uninstall utilities (name normalization, manifest preservation) | ______________________________________________________________________ @@ -184,23 +194,29 @@ flowchart LR C --> D[Writes docs, code, or reports to disk] ``` -### possible future model — orchestrated role pipeline +### target operating model — stage-gated role pipeline -Each role makes its own model call. Output artifacts are passed to the next role. +Each role is a separate model call. Output artifacts from one role become the input context +for the next, and progression only happens after explicit user approval at each stage gate. ```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 + P[product] --> GP{User approves Product output} + GP --> A[architect] + A --> GA{User approves Architecture output} + GA --> D[designer] + D --> GD{User approves Design output} + GD --> E[engineer] + E --> GE{User approves Implementation checkpoint} + GE --> T[tester] + T --> GT{User approves Verification output} + GT --> R[release] + R --> GR{User final merge approval} + GR --> PR[PR opened] ``` -See `docs/architecture/adr/004-option-a-to-b-pipeline.md` and `docs/design/workflow.md` for pipeline detail. +See `docs/architecture/adr/004-option-a-to-b-pipeline.md`, `docs/architecture/adr/010-artifact-flow.md`, +and `docs/design/workflow.md` for pipeline and gate detail. ______________________________________________________________________ @@ -209,21 +225,27 @@ ______________________________________________________________________ All significant architectural decisions are recorded in `docs/architecture/adr/`. See individual files for context, decision, alternatives, and rationale. -| ADR | Title | Status | -| --- | ---------------------------------------------------- | -------- | -| 001 | VS Code-native variant | accepted | -| 002 | Artifact naming and compatibility policy | accepted | -| 003 | Backend-first verify | accepted | -| 004 | Option A to B pipeline | accepted | -| 005 | VS Code prompt format | accepted | -| 006 | No runtime dependency on external binaries | accepted | -| 007 | Python runtime | accepted | -| 008 | Agents over prompts | accepted | -| 009 | 6-role agent model | accepted | -| 010 | Artifact flow | accepted | -| 011 | Skill restructure | accepted | -| 012 | Flat templates and install-time generation | accepted | -| 013 | Policy vs procedure boundary for instructions/skills | accepted | -| 014 | Manifest schema versioning and explicit upgrade gate | accepted | -| 015 | Conservative install-by-default | accepted | -| 016 | Atomic manifest writes | accepted | +| ADR | Title | Status | Notes | +| --- | ---------------------------------------------------- | ---------- | --------------------------------------- | +| 001 | VS Code-native variant | accepted | | +| 002 | Artifact naming and compatibility policy | accepted | | +| 003 | Backend-first verify | accepted | | +| 004 | Direct execution and orchestrated pipeline | accepted | | +| 005 | VS Code prompt format | accepted | | +| 006 | No runtime dependency on external binaries | accepted | | +| 007 | Python runtime | accepted | | +| 008 | Agents over prompts | accepted | | +| 009 | 6-role agent model | accepted | | +| 010 | Artifact flow | accepted | | +| 011 | Skill restructure | accepted | | +| 012 | Flat templates and install-time generation | accepted | | +| 013 | Policy vs procedure boundary for instructions/skills | accepted | | +| 014 | Manifest schema versioning and explicit upgrade gate | accepted | | +| 015 | Conservative install-by-default | superseded | Superseded by ADR-020 | +| 016 | Atomic manifest writes | accepted | | +| 017 | Checksum backfill on upgrade | accepted | | +| 018 | Skill genericity boundary | accepted | | +| 019 | `.vstack/` project-scope directory | accepted | Introduced `.vstack/` directory | +| 020 | `install` and `init` command semantics | accepted | Breaking change; supersedes ADR-015 | +| 021 | Config-driven artifact paths in agent config | accepted | Machine-readable artifact ownership | +| 022 | Selective exclude filter in `.vstack/config.yaml` | accepted | Agents cannot be excluded (atomic unit) | diff --git a/docs/design/agents.md b/docs/design/agents.md index a9be8f5..fce46b1 100644 --- a/docs/design/agents.md +++ b/docs/design/agents.md @@ -1,7 +1,7 @@ # vstack — agents > Maintained by: **designer** role\ -> Last updated: 2026-04-01\ +> Last updated: 2026-05-03\ > VS Code docs: [custom agents](https://code.visualstudio.com/docs/copilot/customization/custom-agents) · [agents overview](https://code.visualstudio.com/docs/copilot/agents/overview) ## what are agents? @@ -61,14 +61,76 @@ Style rule: long `description` and `handoffs.prompt` values should use YAML bloc ### vstack-internal only (not emitted) -| Field | Notes | -| --------- | ------------------------------------------------------------------------------ | -| `version` | Semantic version for vstack change tracking — never reaches the generated file | +| Field | Notes | +| ----------- | ------------------------------------------------------------------------------ | +| `version` | Semantic version for vstack change tracking — never reaches the generated file | +| `artifacts` | Declares artifact ownership for this agent — see [artifacts](#artifacts) below | Frontmatter multiline rendering is configured in generator code (`ArtifactTypeConfig.preserve_multiline_frontmatter`), not per-agent `config.yaml`. ______________________________________________________________________ +## artifacts + +The optional `artifacts:` block declares which paths an agent reads and writes. +This field is **vstack-internal** — it is not emitted to the generated `.agent.md` +frontmatter; instead it drives the rendered `## artifacts you use` section in the +template body. +See [ADR-021](../architecture/adr/021-config-driven-artifact-paths.md) for rationale. + +```yaml +artifacts: + dir: architecture # subdirectory within the global docs root (no root prefix) + input: # paths this agent reads (glob patterns, relative to docs root) + - product/**/*.md + output: # files this agent produces + - overview.md # simple string: resolved as // + - path: ux.md # dict form: required for entries with notes + notes: frontend/fullstack scope only + - path: ./src/**/* # ./ prefix: verbatim path, dir prefix not applied +``` + +### path resolution rules + +| Form | Resolution | +| ----------------------------------------------------- | ------------------------------------------------------------------- | +| `input` item | `/` (e.g. `docs/product/**/*.md`) | +| `output` string or `path` — no `./` prefix, `dir` set | `//` | +| `output` string or `path` — no `./` prefix, no `dir` | `` verbatim | +| `output` `path` with `./` prefix | strip `./`, use remainder verbatim (e.g. `./src/**/*` → `src/**/*`) | + +`ARTIFACTS_DOCS_ROOT` defaults to `docs`. It is a global constant in +`src/vstack/constants.py` and can be overridden per project via `artifacts.root` +in `.vstack/config.yaml`. Individual agent configs must never embed the root +prefix; set only the subdirectory in `dir`. + +### field reference + +| Field | Required | Type | Notes | +| ----------------- | -------- | ------------------------ | -------------------------------------------------------------------- | +| `dir` | no | string | Subdirectory this agent writes to, relative to `ARTIFACTS_DOCS_ROOT` | +| `input` | no | list of strings | Glob patterns for files the agent reads as context | +| `output` | no | list of strings or dicts | Paths the agent produces; use dict form to add `notes` | +| `input_comments` | no | string | Optional free-text appended below the input table | +| `output_comments` | no | string | Optional free-text appended below the output table | + +### generated template tokens + +The `## artifacts you use` section in each `template.md` uses four placeholder +tokens that are resolved by `AgentGenerator` at install time: + +| Token | Rendered as | +| ------------------------------------- | --------------------------------------------------- | +| `{{AGENT_ARTIFACTS_INPUT}}` | Markdown table of input artifacts, or empty string | +| `{{AGENT_ARTIFACTS_OUTPUT}}` | Markdown table of output artifacts, or empty string | +| `{{AGENT_ARTIFACTS_INPUT_COMMENTS}}` | Value of `input_comments`, or empty string | +| `{{AGENT_ARTIFACTS_OUTPUT_COMMENTS}}` | Value of `output_comments`, or empty string | + +Tables use a single `Artifact` column when no entry has notes, and two columns +(`Artifact`, `Notes`) when any entry has a non-empty `notes` value. + +______________________________________________________________________ + ## tools Use the following tool names in the `tools` list: @@ -94,13 +156,23 @@ Handoffs create guided sequential workflows. After a response completes, VS Code `handoffs` is fully supported in `AGENT_SCHEMA` and is emitted by the generator. Define handoffs in `config.yaml` and they will appear in the generated `.agent.md` frontmatter. +### handoff policy + +Handoffs are UI accelerators for the happy path only, not orchestration logic: + +- Each non-terminal role defines **exactly one** forward handoff with label `Go to next stage: `. +- The `release` role is terminal — it has **no handoffs**. Opening a PR is a release action, not a handoff. +- Back, side, and escalation handoff buttons are not allowed. Non-happy paths remain explicit user decisions. + +See [workflow.md](./workflow.md#handoff-button-convention) for the full stage-gated model and gate moment definitions. + Structure: ```yaml handoffs: - label: Start implementation agent: engineer - prompt: Design is complete in docs/design/design.md. Please implement. + prompt: Design is complete in docs/design/overview.md. Please implement. send: false # true = auto-submit the prompt model: "" # optional override; use only a verified model ID ``` @@ -125,10 +197,9 @@ All role templates in `src/vstack/_templates/agents//template.md` must fol 1. `## communication style` 1. `## workflow and handoffs` 1. Role-specific deep-dive sections (e.g. `how you work`, `scope detection`, `artifact checklist`, `verification tracks`) -1. `## baseline and optional delta` (if applicable) 1. `## success criteria` 1. `## failure and escalation rules` -1. `## artifacts you own` or `## artifacts you touch` +1. `## artifacts you use` 1. `## completion checklist` 1. `## skills you use` @@ -137,7 +208,6 @@ Rules: - Keep the canonical sections present and in this order for every role. - Role-specific sections are allowed, but they must not replace canonical sections. - Keep role boundaries explicit; do not let one role absorb another role's ownership. -- Prefer baseline-first language and treat `docs/delta/{id}/` as temporary. Minimal shape: @@ -184,11 +254,14 @@ You are a **** acting as the **<role> role**. <one-line purpose>. - … -## artifacts you own +## artifacts you use + +{{AGENT_ARTIFACTS_INPUT}} + +{{AGENT_ARTIFACTS_OUTPUT}} -| artifact | purpose | -|----------|---------| -| ... | ... | +Agents do not write to artifacts owned by other roles. If you discover something +that requires changes to upstream artifacts, flag it and trigger a reverse handoff. ## completion checklist @@ -229,6 +302,13 @@ tools: - todo - agent agents: ["*"] +artifacts: + target: docs/architecture + input: + - docs/product/*.md + output: + - overview.md + - adr/*.md target: vscode user-invocable: true ``` diff --git a/docs/design/design.md b/docs/design/overview.md similarity index 98% rename from docs/design/design.md rename to docs/design/overview.md index d6f157a..36b828c 100644 --- a/docs/design/design.md +++ b/docs/design/overview.md @@ -1,7 +1,7 @@ # vstack — design > Maintained by: **designer** role\ -> Last updated: 2026-04-26 +> Last updated: 2026-05-03 ## overview @@ -9,7 +9,7 @@ This document is the concrete design baseline for vstack — a `platform` artifa standalone CLI tool and SDK that installs structured role artifacts into a project's `.github/` directory for use with GitHub Copilot Agent Mode. -It translates the architecture blueprint in `docs/architecture/architecture.md` into +It translates the architecture blueprint in `docs/architecture/overview.md` into implementable interfaces, data schemas, state models, validation rules, and error contracts. Implementation follows this document; architecture decisions are recorded in `docs/architecture/adr/`. @@ -322,6 +322,13 @@ ______________________________________________________________________ ## 3. frontmatter schemas +The tables below are the authoritative quick-reference for implementers. For full authoring +guidance, field semantics, examples, and template structure, see the dedicated docs: + +- Agents → [agents.md](./agents.md) +- Skills → [skills.md](./skills.md) +- Instructions → [instructions.md](./instructions.md) + ### 3.1 skill frontmatter (`config.yaml`) | Field | Type | Required | Constraints | diff --git a/docs/design/skills.md b/docs/design/skills.md index 359af0e..298ac62 100644 --- a/docs/design/skills.md +++ b/docs/design/skills.md @@ -49,9 +49,9 @@ ______________________________________________________________________ | --------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------- | ------------------------------------------- | | `vision` | CEO/founder-mode plan review. Rethink from first principles, validate ambition, find the 10x solution. | product | `docs/product/vision.md` | | `requirements` | Collaborative requirements gathering. Clarifies what must be built, defines success criteria and NFRs. | product | `docs/product/requirements.md` | -| `architecture` | Engineering-lead plan review. Lock in service boundaries, data models, API contracts, test strategy. | architect | `docs/architecture/architecture.md` | +| `architecture` | Engineering-lead plan review. Lock in service boundaries, data models, API contracts, test strategy. | architect | `docs/architecture/overview.md` | | `adr` | Architecture Decision Record writing. Documents a decision with context, alternatives, and rationale. | architect | `docs/architecture/adr/NNN-*.md` | -| `design` | Build a complete API or service design from scratch. Produces OpenAPI specs, error conventions, naming standards. | designer | `docs/design/design.md` / `openapi.yaml` | +| `design` | Build a complete API or service design from scratch. Produces OpenAPI specs, error conventions, naming standards. | designer | `docs/design/overview.md` / `openapi.yaml` | | `consult` | DX triage and focused review. Routes to one path (API DX, CLI/tool DX, or developer workflow DX) and routes non-DX requests to specialized skills. | designer | focused DX report or routing recommendation | | `concise` | Runtime response-style controller. Switches response density (`normal`, `compact`, `ultra`) and reports active mode via `status` without reinstall. | all roles | session style state + status output | | `conventional-commit` | Prepare and validate Conventional Commit messages from current changes, with type/scope selection and commit-intent checks before commit. | engineer, release | compliant commit message(s) + commit(s) | @@ -66,7 +66,7 @@ ______________________________________________________________________ | `cicd` | Write GitHub Actions CI/CD workflow configuration. Covers build, test, lint, security scan, container publish, deploy. | release | GitHub Actions workflow | | `container` | Write and review Dockerfile, docker-compose, and container config. Covers multi-stage builds, non-root users, layer optimisation. | engineer | Dockerfile + compose | | `threat-model` | Threat modeling with STRIDE-first identification, optional DREAD prioritization, and selective PASTA depth for critical systems. | architect, tester, engineer, designer | `docs/architecture/threat-model.md` | -| `release-notes` | Prepare release artifacts: write release notes, own CHANGELOG updates, produce `docs/releases/{date}.md`. | release | CHANGELOG + release doc | +| `release-notes` | Prepare release artifacts: write release notes, own CHANGELOG updates, produce `docs/releases/YYYY-MM-DD.md`. | release | CHANGELOG + release doc | | `pr` | Commit, push, and open a pull request from the current branch to main. | release | commit + PR | | `gh-release` | Create or update a GitHub Release using gh CLI with tag validation, draft/publish mode, and release notes file integration. | release | GitHub Release | | `docs` | Post-release documentation alignment for README/API docs/migrations and related artifacts (no CHANGELOG ownership). | product, architect, designer, engineer, tester, release | updated docs artifacts | diff --git a/docs/design/workflow.md b/docs/design/workflow.md index 214fcf2..d60467a 100644 --- a/docs/design/workflow.md +++ b/docs/design/workflow.md @@ -1,18 +1,15 @@ # vstack — workflow > Maintained by: **designer** role\ -> Last updated: 2026-04-21 +> Last updated: 2026-05-03 ## overview This document describes how vstack workflows execute today (single-call execution) -and a possible future orchestrated role pipeline. +and the target operating model: a stage-gated role pipeline. -For a precise GitHub Actions CI/CD and release pipeline specification, see -`docs/design/cicd.md`. - -It also documents the repository-level GitHub Actions automation used for quality, -security, commit policy, and releases. +For the full GitHub Actions CI/CD and release pipeline specification, including the +human and Dependabot sequences with step-by-step checklists, see `docs/design/cicd.md`. For authoring boundaries between reusable guidance mechanisms: @@ -37,30 +34,8 @@ and easy to reason about. | `.github/workflows/release.yml` | Push to `main` | Run release-please to maintain release PRs and create tags/releases when merged. | | `.github/workflows/publish.yml` | GitHub release published | Build package artifacts from the release tag and publish to PyPI. | -### 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 and PRs. -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. - -### release versioning model - -`release.yml` uses release-please as the source of truth for version calculation, -CHANGELOG updates, and GitHub release notes based on conventional commits. - -`publish.yml` only builds and publishes artifacts for already created release tags. - -Repository tag policy remains strict `X.Y.Z` (no `v` prefix). +For trigger conditions, execution sequences, commit policy details, and release versioning +rules, see `docs/design/cicd.md`. ______________________________________________________________________ @@ -86,29 +61,44 @@ flowchart LR ______________________________________________________________________ -## possible future model — orchestrated role pipeline +## stage-gated role pipeline (target operating model) -Each role becomes a separate model call. Output artifacts from one role become -the input context for the next. +Each role is a separate model call. Output artifacts from one role become the +input context for the next role, and progression only happens after explicit +user approval. ```mermaid flowchart TD - P[product<br>vision.md<br>requirements.md<br>roadmap.md] --> A[architect<br>architecture.md<br>adr/*.md] - A --> D[designer<br>design.md] - D --> G1{User gate 1<br>requirements and design} - D -. backend-only path .-> G1 - G1 --> E[engineer<br>code and unit tests] - E --> T[tester<br>test-report.md<br>security-report.md<br>performance-baseline.md] - T --> G2{User gate 2<br>pre-prod sign-off} - G2 --> G3{User gate 3<br>final merge approval} - G3 --> R[release<br>releases/{date}.md<br>CHANGELOG.md<br>PR] + P[product] --> GP{User approves Product output} + GP --> A[architect] + A --> GA{User approves Architecture output} + GA --> D[designer] + D --> GD{User approves Design output} + GD --> E[engineer] + E --> GE{User approves Implementation checkpoint} + GE --> T[tester] + T --> GT{User approves Verification output} + GT --> R[release] + R --> GR{User final merge approval} + GR --> PR[PR opened] ``` **Characteristics:** - Each role is scoped to its domain - Each role reads its inputs from disk (artifacts from upstream roles) -- User gates are explicit pauses for human review +- User approval is required after each stage output +- Handoffs are only for happy-path continuation + +### flow principles + +1. **User-gated progression:** every stage output is reviewed by the user before the next stage starts. +1. **All roles in every pipeline:** every use case runs through all six roles. Roles that are not affected by a change assess impact and pass through explicitly rather than being skipped. +1. **Happy-path handoffs only:** handoff buttons are limited to one forward action named `Go to next stage: <stage>`. +1. **No automatic backtracking:** non-happy paths (`NOK`, blockers, missing artifacts) do not use handoff buttons; the user decides the next action. +1. **Subagent delegation mid-role:** engineer may invoke architect or designer as subagents to clarify constraints or contracts during implementation without going back to a full gate cycle. +1. **Release owns sign-off orchestration:** release remains the final orchestrator and gathers `OK`/`NOK` review outcomes from prior role perspectives. +1. **Deterministic sign-off contract:** every sign-off review returns the same structure: verdict, reviewed scope, gaps, impact, and required next action. ______________________________________________________________________ @@ -125,32 +115,190 @@ If an upstream artifact is missing, the role reports what it needs before procee ### required reads per role -| Role | Must read before starting | -| --------- | --------------------------------------------------------------------------------------------------------------------------- | -| product | (none — initiates pipeline) | -| architect | `docs/product/vision.md`, `docs/product/requirements.md` | -| designer | `docs/product/vision.md`, `docs/product/requirements.md`, `docs/architecture/architecture.md`, `docs/architecture/adr/*.md` | -| engineer | `docs/product/requirements.md`, `docs/design/design.md`, `docs/architecture/architecture.md`, `docs/architecture/adr/*.md` | -| tester | `docs/product/requirements.md`, relevant source files | -| release | `docs/test-report.md`, `docs/security-report.md`, user sign-off | +Default artifact paths are defined in ADR-021 and configured per-project in each +agent's `config.yaml`. + +- **`product`** — *(none — initiates pipeline)* +- **`architect`** — product artifacts +- **`designer`** — product artifacts, architecture artifacts +- **`engineer`** — product artifacts, architecture artifacts, design artifacts +- **`tester`** — architecture artifacts, design artifacts, relevant source files +- **`release`** — product artifacts, architecture artifacts, design artifacts, tester reports, user sign-off ______________________________________________________________________ ## user gate moments -There are **4 explicit user gate moments** where the pipeline pauses for human input: +There are **6 explicit user gate moments** where the pipeline pauses for human input: -| Gate | When | Who signs off | -| ---------------------------- | ------------------------------------------------------------ | ------------- | -| **1. Requirements approval** | After product writes requirements.md | User | -| **2. Design approval** | After architect writes architecture + designer writes design | User | -| **3. Pre-prod sign-off** | After tester reports are ready | User | -| **4. Merge approval** | Before release creates PR | User | +| Gate | When | Who signs off | +| -------------------------------- | ------------------------------------- | ------------- | +| **1. Product approval** | After product updates scope artifacts | User | +| **2. Architecture approval** | After architect updates architecture | User | +| **3. Design approval** | After designer updates design | User | +| **4. Implementation checkpoint** | After engineer implements changes | User | +| **5. Verification approval** | After tester reports are ready | User | +| **6. Final merge approval** | After release readiness is complete | User | Gates prevent automated pipelines from deploying without human review. In the current model, the user implicitly gates by choosing which skill to invoke next. In the orchestrated model, the orchestrator pauses and waits for explicit confirmation. +### handoff button convention + +For role UIs that expose handoffs, use exactly one continuation button per stage: + +- `Go to next stage: Architecture` +- `Go to next stage: Design` +- `Go to next stage: Engineering` +- `Go to next stage: Verification` +- `Go to next stage: Release readiness` + +Release is the final role stage; opening the PR is a release action, not a +handoff to another role. + +Do not add back, side, or escalation handoff buttons. Those paths remain +explicit user decisions. + +______________________________________________________________________ + +## use-case flow examples + +The following examples apply the same stage-gated model to common scenarios. + +### use case 1 — new project + +```mermaid +sequenceDiagram + participant U as User + participant P as Product + participant A as Architect + participant D as Designer + participant E as Engineer + participant T as Tester + participant R as Release + + U->>P: Define vision, scope, success criteria + P-->>U: Product artifacts ready + U->>U: Gate 1 approve + U->>A: Go to next stage: Architecture + A-->>U: Architecture and ADR updates + U->>U: Gate 2 approve + U->>D: Go to next stage: Design + D-->>U: Design and contracts + U->>U: Gate 3 approve + U->>E: Go to next stage: Engineering + E-->>U: Implementation and tests + U->>U: Gate 4 approve + U->>T: Go to next stage: Verification + T-->>U: Test and security reports + U->>U: Gate 5 approve + U->>R: Go to next stage: Release readiness + R-->>U: Consolidated sign-off matrix + release artifacts + U->>U: Gate 6 final merge approval +``` + +### use case 2 — update existing project (change) + +```mermaid +sequenceDiagram + participant U as User + participant P as Product + participant A as Architect + participant D as Designer + participant E as Engineer + participant T as Tester + participant R as Release + + U->>P: Request change and impact boundaries + P-->>U: Updated requirements and scope changes + U->>U: Gate 1 approve + U->>A: Go to next stage: Architecture + A-->>U: Architecture constraints for this change + U->>U: Gate 2 approve + U->>D: Go to next stage: Design + D-->>U: Design contracts and edge cases for this change + U->>U: Gate 3 approve + U->>E: Go to next stage: Engineering + E-->>U: Incremental implementation + U->>U: Gate 4 approve + U->>T: Go to next stage: Verification + T-->>U: Regression + targeted verification verdict + U->>U: Gate 5 approve + U->>R: Go to next stage: Release readiness + R-->>U: Sign-off matrix based on changed scope + U->>U: Gate 6 final merge approval +``` + +### use case 3 — incident fix + +All roles remain in the pipeline. Architect and designer each assess whether +their domain is affected and either contribute or pass through explicitly. +Engineer can invoke architect or designer as subagents to clarify constraints +or contracts mid-implementation. + +```mermaid +sequenceDiagram + participant U as User + participant P as Product + participant A as Architect + participant D as Designer + participant E as Engineer + participant T as Tester + participant R as Release + + U->>P: Declare incident scope and urgency + P-->>U: Incident acceptance criteria + U->>U: Gate 1 approve + U->>A: Go to next stage: Architecture + A-->>U: Architecture impact assessed — constraints or pass-through + U->>U: Gate 2 approve + U->>D: Go to next stage: Design + D-->>U: Design impact assessed — contract updates or pass-through + U->>U: Gate 3 approve + U->>E: Go to next stage: Engineering + E-->>U: Hotfix implementation (may invoke architect/designer as subagents) + U->>U: Gate 4 implementation checkpoint + U->>T: Go to next stage: Verification + T-->>U: Focused regression and safety verdict + U->>U: Gate 5 verification approval + U->>R: Go to next stage: Release readiness + R-->>U: Final sign-off matrix and PR readiness + U->>U: Gate 6 final merge approval +``` + +### use case 4 — reverse engineering and baseline reconstruction + +```mermaid +sequenceDiagram + participant U as User + participant P as Product + participant A as Architect + participant D as Designer + participant E as Engineer + participant T as Tester + participant R as Release + + U->>P: Define discovery goals and constraints + P-->>U: Discovery requirements + U->>U: Gate 1 approve + U->>A: Go to next stage: Architecture + A-->>U: As-is architecture map and risks + U->>U: Gate 2 approve + U->>D: Go to next stage: Design + D-->>U: As-is contracts and interaction model + U->>U: Gate 3 approve + U->>E: Go to next stage: Engineering + E-->>U: Instrumentation or documentation improvements + U->>U: Gate 4 approve + U->>T: Go to next stage: Verification + T-->>U: Evidence that reconstructed baseline matches behavior + U->>U: Gate 5 approve + U->>R: Go to next stage: Release readiness + R-->>U: Consolidated sign-off and publication readiness + U->>U: Gate 6 final merge approval +``` + ______________________________________________________________________ ## skill execution within a role diff --git a/docs/product/roadmap.md b/docs/product/roadmap.md index d49b952..8f8e277 100644 --- a/docs/product/roadmap.md +++ b/docs/product/roadmap.md @@ -1,45 +1,42 @@ # vstack — roadmap > Maintained by: **product** role\ -> Last updated: 2026-05-02 +> Last updated: 2026-05-06 ______________________________________________________________________ ## feature status table -| Feature | Status | Notes | -| ---------------------------------------- | ----------- | ------------------------------------------------------------------------------------------------------- | -| foundation | shipped | Core template-driven install model is in place | -| backend-first verification | shipped | Verify/inspect focus on contracts, observability, security | -| VS Code agent migration | shipped | Native agent output format implemented | -| role model + doc restructure | shipped | 6-role model and docs baseline established | -| new skill scaffolding | shipped | 42-skill set with canonical naming | -| agent skill wiring | shipped | Role-to-skill mapping and handoffs are present | -| CLI modularisation (v2.0.0) | shipped | 12 focused CLI modules; BaseCommand + CommandContext contract | -| manifest package (v2.0.0) | shipped | Dedicated `manifest/` package; atomic writes (ADR-016) | -| mypy type checking (v2.0.0) | shipped | Full mypy coverage enforced in CI; 100% test coverage gate | -| manifest schema versioning (v2.0.0) | shipped | `manifest_version: 2`; upgrade path via `manifest upgrade` (ADR-014) | -| checksum backfill (v2.0.0) | shipped | `manifest upgrade --backfill` adds SHA-256 for VSTACK-META-tagged files (ADR-017) | -| conservative install (v2.0.0) | shipped | Untracked files never overwritten; checksum-gated update (ADR-015) | -| dry-run install | shipped | `vstack install --dry-run` previews actions; type/name selectors in summary | -| workflow contract source-of-truth | candidate | Defer until current template expansion is complete; then add machine-readable role workflow contract | -| agent hooks support | candidate | Generate `.github/hooks/<name>.json` from vstack templates; enforce quality gates at session boundaries | -| Copilot code review support | candidate | Add templates and policy defaults for requesting/configuring Copilot code review | -| GitHub tasks MCP-first mode | candidate | Add task profile for issues/PR/branch operations with MCP-first execution and safe fallback | -| Copilot Spaces context pack | candidate | Add structured context packaging and refresh workflow for Copilot Spaces | -| content exclusion baseline | candidate | Ship policy templates/checklists for Copilot content exclusion at repo/org/enterprise scope | -| MCP governance baseline | candidate | Standardize MCP registry allowlist and server-access policy defaults | -| Copilot Memory governance | candidate | Add memory enablement, review, and curation policy guidance for teams | -| usage-based billing guardrails | candidate | Add budget/allowance/monitoring playbooks for Copilot metered usage | -| Copilot admin observability pack | candidate | Define operational KPI and reporting cadence for adoption and usage dashboards | -| VS Code customization starter pack | candidate | Add installable templates for custom agents, instructions, prompts, and agent customization workflow | -| template overlays + selective install | candidate | Combine upstream and company templates with source-priority rules and install by artifact type | -| optional orchestrated role pipeline | candidate | Optional future model, only if coordination bottlenecks appear | -| multi-IDE support (IntelliJ first) | candidate | Not planned before v1 stabilization | -| heavy agent runtime framework | not planned | Keeps runtime lightweight and transparent | -| cloud control plane dependency | not planned | Keeps operation local/offline-capable | -| VS Code extension packaging | not planned | Not required for current install model | -| browser automation as default dependency | not planned | Backend/microservice-first remains default | +| Feature | Version | Status | Notes | +| ---------------------------------------- | ------- | ----------- | ----------------------------------------------------------------------------------------------------------- | +| foundation | v1.0.0 | shipped | Core template-driven install model is in place | +| backend-first verification | v1.0.0 | shipped | Verify/inspect focus on contracts, observability, security | +| VS Code agent migration | v1.x | shipped | Native `.github/agents/*.agent.md` output format implemented | +| role model + doc restructure | v1.1.0 | shipped | 6-role model, agent templates, and docs baseline established | +| new skill scaffolding | v2.2.0 | shipped | 42-skill set with canonical naming | +| agent skill wiring | v2.2.0 | shipped | Role-to-skill mapping, handoffs, and concise modes wired into all agents | +| CLI modularisation | v2.0.0 | shipped | 12 focused CLI modules; BaseCommand + CommandContext contract | +| manifest package | v2.0.0 | shipped | Dedicated `manifest/` package; atomic writes (ADR-016) | +| mypy type checking | v2.0.0 | shipped | Full mypy coverage enforced in CI; 100% test coverage gate | +| manifest schema versioning | v2.0.0 | shipped | `manifest_version: 2`; upgrade path via `manifest upgrade` (ADR-014) | +| checksum backfill | v2.0.0 | shipped | `manifest upgrade --backfill` adds SHA-256 for VSTACK-META-tagged files (ADR-017) | +| conservative install | v2.0.0 | shipped | Untracked files never overwritten; checksum-gated update (ADR-015, superseded by ADR-020) | +| dry-run install | v2.1.0 | shipped | `vstack install --dry-run` previews actions; type/name selectors in summary | +| project-scope directory | v3.0.0 | shipped | `.vstack/` directory: `config.yaml`, manifest, delta templates (ADR-019) | +| install/init command semantics | v3.0.0 | shipped | `install` = first-run setup; `init` = idempotent CI regeneration (ADR-020, breaking change) | +| manifest relocation | v3.0.0 | shipped | `vstack.json` moves from `.github/` to `.vstack/`; migration via `manifest upgrade` (ADR-014) | +| selective install | v3.0.0 | shipped | Per-type and per-name exclusions via `exclude:` in `.vstack/config.yaml`; agents always installed (ADR-022) | +| agent hooks support | t.b.d. | candidate | Generate `.github/hooks/<name>.json` from vstack templates; enforce quality gates at session boundaries | +| new skills (next batch) | t.b.d. | candidate | `spaces`: set up Copilot Spaces; `copilot-admin`: manage Copilot settings via `gh api` | +| team customization layer | t.b.d. | candidate | Custompacks on top of vstack defaults; agents non-removable, skills fully overridable; overlay merge model | +| workflow contract source-of-truth | t.b.d. | candidate | Central contract file + generator validation of role I/O chains; prereq for orchestrated pipeline | +| optional orchestrated role pipeline | t.b.d. | candidate | Optional future model, only if coordination bottlenecks appear | +| multi-IDE support (IntelliJ first) | t.b.d. | candidate | Not planned before current model stabilizes | +| heavy agent runtime framework | — | not planned | Keeps runtime lightweight and transparent | +| cloud control plane dependency | — | not planned | Keeps operation local/offline-capable | +| VS Code extension packaging | — | not planned | Not required for current install model | +| browser automation as default dependency | — | not planned | Backend/microservice-first remains default | +| install target directory override | — | not planned | Won't implement unless a concrete tool incompatibility with `.github/` arises | ______________________________________________________________________ @@ -47,54 +44,53 @@ ______________________________________________________________________ Legend: shipped = implemented and available; candidate = optional future feature (not committed); not planned = evaluated and intentionally excluded for now. -### foundation [shipped] +______________________________________________________________________ + +### foundation [shipped — v1.0.0] -- template-driven generation with source under `src/vstack/_templates/` and install output under `.github/` -- 42 backend-oriented skills -- generated install manifest (`.github/vstack.json`) tracking installed artifacts +- Template-driven generation with source under `src/vstack/_templates/` and install output under `.github/` +- Generated install manifest (`.vstack/vstack.json`) tracking installed artifacts - VS Code prompt file (`.prompt.md`) support -- global install workflow (`vstack install --global`) +- Global install workflow (`vstack install --global`) -### backend-first verification [shipped] +### backend-first verification [shipped — v1.0.0] - verify + inspect skills emphasizing contract tests, observability, security -- skill renaming to canonical backend names (vision, architecture, verify, etc.) +- Skill renaming to canonical backend names (vision, architecture, verify, etc.) - `derive-from` removed from all templates -### VS Code agent migration [shipped] +### VS Code agent migration [shipped — v1.x] -- migrated from `.prompt.md` → `.github/agents/*.agent.md` format -- generator produces agents output with YAML frontmatter -- tool mapping (template tool names → VS Code tool IDs) -- removed upstream compatibility markers from generated artifacts +- Migrated from `.prompt.md` → `.github/agents/*.agent.md` format +- Generator produces agents output with YAML frontmatter +- Tool mapping (template tool names → VS Code tool IDs) +- Removed upstream compatibility markers from generated artifacts -### role model + doc restructure [shipped] +### role model + doc restructure [shipped — v1.1.0] - 6-role model defined: product, architect, designer, engineer, tester, release (guardian merged into tester) -- artifact ownership documented per role -- all docs renamed to lowercase -- docs/architecture/adr/ structure established -- docs rewritten to match agent-output format +- Artifact ownership documented per role; all docs renamed to lowercase +- `docs/architecture/adr/` structure established +- Docs rewritten to match agent-output format; per-role concise modes wired into all agents -### new skill scaffolding [shipped] +### new skill scaffolding [shipped — v2.2.0] -39 skills across 6 roles. Representative additions: +42 skills across 6 roles. Representative additions: - `requirements`, `adr`, `analyse` (new) -- `gh-issues`, `codeql`, `dependabot`, `secret-scan` +- `gh-issues`, `codeql`, `dependabot`, `secret-scan`, `gh-release` - `gdpr`, `terraform`, `terragrunt`, `cloudformation`, `aws-cli` +- `k8s`, `helm`, `rancher`, `threat-model`, `conventional-commit` - Renames: `experience` → `consult`, `design-consult` → `design`, `docs-release` → `docs`, `discovery` → `explore` - All templates: WHO→HOW (removed role persona preamble, added out-of-scope sections) -- `guardrails` retained as a per-project installable safety skill - -### agent skill wiring [shipped] -Role templates now reference the intended canonical skills: +### agent skill wiring [shipped — v2.2.0] - Product + architect include `requirements` and `adr` - Architect + engineer include `analyse` and `explore` - Designer includes `consult` - Agent configs define role-to-role handoff buttons for pipeline flow +- `artifacts.input` / `artifacts.output` fields in all agent `config.yaml` files ______________________________________________________________________ @@ -113,49 +109,56 @@ ______________________________________________________________________ - `manifest_version: 2` schema with `hash_algorithm` and per-entry `checksum_algorithm` - Upgrade path: `vstack manifest upgrade [--backfill]` - Checksum backfill for VSTACK-META-tagged files (ADR-017) +- mypy type checking added as a quality gate; 100% test coverage enforced in CI ### conservative install [shipped — v2.0.0] - Untracked files are never overwritten by default (ADR-015) - Checksum-gated `--update` mode: only rewrites clean tracked files - `--force-name` / `--adopt-name` accept `type/name` selectors (e.g. `agent/engineer`) -- `--dry-run` previews all actions with a summary and preserved-selectors list -______________________________________________________________________ +### dry-run install [shipped — v2.1.0] -### optional orchestrated role pipeline [candidate] +- `vstack install --dry-run` previews all planned actions without writing any files +- Summary shows which artifacts would be installed, updated, preserved, or skipped +- Type/name selectors (`--only`) compose with dry-run for scoped previews -Possible future workflow with explicit orchestration (only if real coordination bottlenecks appear): +______________________________________________________________________ -- Each role makes its own model call -- Artifacts pass between roles via disk files -- User gates pause the pipeline at defined checkpoints -- Orchestrator role (product) manages pipeline state -- Parallel execution possible for multiple tester passes +### project-scope directory [shipped — v3.0.0] -See `docs/design/workflow.md` for current execution and the orchestrated future model. +- `.vstack/` directory introduced as the project-scope state container (ADR-019) +- `config.yaml` — human-authored project configuration (YAML) +- `vstack.json` — machine-generated install manifest (JSON), relocated from `.github/` +- `templates/` — project-owned artifact starter templates, seeded by `vstack install` +- `.vstack/.gitignore` seeded on every install run -### workflow contract source-of-truth [candidate] +### install/init command semantics [shipped — v3.0.0] -Deferred until the current templates expansion is complete. +- `vstack install` — first-run setup: seeds `.vstack/`, generates all artifacts, writes manifest (ADR-020) +- `vstack init` — idempotent CI regeneration: rewrites artifacts from templates, updates manifest +- Breaking change: previous `install` behavior is now split across both commands -Planned direction: +### manifest relocation [shipped — v3.0.0] -- Keep one machine-readable workflow contract describing role inputs, outputs, gates, and handoffs. -- Use that contract to keep agent workflow sections and checks aligned. -- Keep `docs/design/workflow.md` as the human-readable explanation layer. +- `vstack.json` moved from `.github/` to `.vstack/` to separate generated config from generated artifacts +- Migration via `vstack manifest upgrade` for existing installs +- Format difference (JSON vs YAML) signals machine-generated vs human-authored ownership (ADR-019) -This reduces drift risk between agent behavior and workflow documentation while keeping skills and instructions generic. +### selective install [shipped — v3.0.0] -### multi-IDE support [candidate] +- Per-type exclusion via `exclude: <type>: all` in `.vstack/config.yaml` removes an entire artifact type +- Per-name exclusion via `exclude: <type>: [name, …]` installs all but the listed names +- Agents are always installed; the six-role chain is an atomic unit (ADR-022) +- Composes with `--only`: type exclusions subtract from the effective install set -IntelliJ is the first candidate beyond VS Code. Not planned until after v1 stabilization. +______________________________________________________________________ -### agent hooks support [candidate] +### agent hooks support [candidate — t.b.d.] -GitHub Copilot agents support a repository-level hooks mechanism: shell commands defined in `.github/hooks/<name>.json` -that execute at key points during an agent session — `sessionStart`, `sessionEnd`, `userPromptSubmitted`, -`preToolUse`, `postToolUse`, and `errorOccurred`. +GitHub Copilot agents support a repository-level hooks mechanism: shell commands defined in +`.github/hooks/<name>.json` that execute at key points during an agent session — +`sessionStart`, `sessionEnd`, `userPromptSubmitted`, `preToolUse`, `postToolUse`, and `errorOccurred`. vstack is well-positioned to provide curated, installable hook templates for common quality-gate patterns: @@ -171,175 +174,108 @@ Planned direction: - Templates live in `src/vstack/_templates/hooks/<name>/hook.json` (source of truth) - Generated output written to `.github/hooks/<name>.json` at install time - Register hooks in `vstack.json` manifest and track them with checksums like other artifact types -- Note: the per-agent `hooks` frontmatter field (already supported) is separate — it scopes hooks to one agent; repository hooks apply to all agent sessions +- Note: the per-agent `hooks` frontmatter field (already supported) is separate — it scopes hooks to one + agent; repository hooks apply to all agent sessions Ref: [GitHub — Customize agent workflows with hooks](https://docs.github.com/en/copilot/how-tos/copilot-on-github/customize-copilot/customize-cloud-agent/use-hooks) -### Copilot code review support [candidate] - -Relevance: - -- Copilot code review is now a dedicated workflow with setup and configuration needs. -- Teams need clear defaults for when review is advisory versus blocking. -- Runner and policy setup should be documented as reusable project artifacts. - -Planned direction: - -- Add templates/checklists for enabling and configuring Copilot code review. -- Add policy defaults for review severity handling and escalation paths. -- Add runner guidance for repository/org-level rollout. - -Ref: [GitHub — Code review](https://docs.github.com/en/copilot/how-tos/use-copilot-agents/request-a-code-review) - -### GitHub tasks MCP-first mode [candidate] - -Relevance: - -- Copilot can perform GitHub tasks directly (issues, PRs, branches) with MCP integration. -- Teams need predictable, safe defaults for operational GitHub actions in agent workflows. -- MCP-first task execution can reduce glue scripting while preserving auditable actions. - -Planned direction: - -- Add a GitHub-tasks profile/skill with MCP-first behavior. -- Add safe fallback to `gh` CLI when MCP capability is unavailable. -- Add guardrails for sensitive actions (merge/close/delete) with explicit confirmation rules. +### new skills (next batch) [candidate — t.b.d.] -Ref: [GitHub — Copilot for GitHub tasks](https://docs.github.com/en/copilot/how-tos/copilot-on-github/copilot-for-github-tasks) +Two new skills planned for the next skill expansion: -### Copilot Spaces context pack [candidate] +#### `spaces` -Relevance: +Guides setup and maintenance of a Copilot Space for a project. -- Copilot Spaces provides curated context for higher-quality responses. -- Teams need a repeatable way to map project artifacts into Space-friendly context bundles. -- Without a packaging pattern, context quality drifts across repositories and teams. - -Planned direction: - -- Add a context-pack template that maps core docs/artifacts into a stable Space feed. -- Add refresh procedures so context remains synchronized with baseline docs and releases. -- Add quality checks for stale or missing context entries. +- Identify which project artifacts belong in the Space (architecture docs, design docs, README, ADRs, skills) +- Step-by-step setup via GitHub UI or `gh api` +- Refresh procedure when baseline docs change (after `vstack install` or a release) +- Quality check: detect stale or missing context entries Ref: [GitHub — Copilot Spaces](https://docs.github.com/en/copilot/how-tos/copilot-on-github/customize-copilot/copilot-spaces) -### content exclusion baseline [candidate] - -Relevance: - -- Copilot content exclusion is now a first-class admin control at repository, organization, and enterprise scope. -- Teams need consistent patterns for excluding sensitive paths (for example: secrets, generated artifacts, legal-restricted data). -- A policy baseline prevents ad hoc exclusions and avoids accidental over-exclusion that harms developer experience. - -Planned direction: - -- Add installable content-exclusion policy templates and review checklist artifacts. -- Provide path-pattern conventions and anti-patterns for repository and org scope. -- Add validation guidance and rollout checks to avoid silent misconfiguration. +#### `copilot-admin` -Ref: [GitHub — Excluding content from GitHub Copilot](https://docs.github.com/en/copilot/how-tos/configure-content-exclusion/exclude-content-from-copilot) +Operational skill for Copilot administrators. Covers all `gh api` operations across the Copilot admin surface: -### MCP governance baseline [candidate] +- **Content exclusion** — query and set excluded paths at repo/org/enterprise scope; path-pattern conventions and anti-patterns +- **MCP governance** — list and manage allowed MCP servers; define allowlist/denylist policy defaults +- **Memory governance** — enable/disable memory per org or repo; view and delete repository memories +- **Usage and billing** — query seat usage, premium-request consumption, and alert thresholds; monthly review checklist +- **Observability** — pull adoption and usage metrics via API; define operational KPIs and escalation triggers -Relevance: - -- MCP server usage now has organization/enterprise governance controls. -- Without governance defaults, teams can connect inconsistent or untrusted MCP servers. -- A baseline improves security posture and keeps tool access predictable across repositories. - -Planned direction: - -- Add a governance-focused instruction/template pack for MCP registry and access policy. -- Define default allowlist/denylist patterns and review ownership. -- Include onboarding checks for newly proposed MCP servers. - -Ref: [GitHub — Managing MCP usage in your company](https://docs.github.com/en/copilot/how-tos/administer-copilot/manage-mcp-usage) - -### Copilot Memory governance [candidate] - -Relevance: - -- Copilot Memory affects cloud agent, code review, and CLI behavior quality. -- Feature is preview and policy-sensitive, so teams need explicit enablement and curation rules. -- Memory hygiene avoids stale, misleading, or sensitive memories degrading output quality. - -Planned direction: - -- Add memory policy templates (enablement defaults, owners, and review cadence). -- Add curation guidance for viewing/deleting repository memories. -- Add short operational guidance for teams with mixed org/enterprise policy inheritance. +Ref: [GitHub — Administer GitHub Copilot for your team](https://docs.github.com/en/copilot/how-tos/administer-copilot) -Ref: [GitHub — Managing and curating Copilot Memory](https://docs.github.com/en/copilot/how-tos/use-copilot-agents/copilot-memory) +### team customization layer [candidate — t.b.d.] -### usage-based billing guardrails [candidate] +Teams want to put their own layer on top of vstack — tuning agents to company context, +replacing generic skills with company-specific ones, and bundling those changes as a +reusable custompack that travels with the project. -Relevance: +Rules: -- Copilot billing is moving to usage-based models; teams need predictable spend controls. -- Without guardrails, premium-request usage can spike unexpectedly. -- Operational usage visibility is required for sustainable adoption. +- **Agents** — can be customized (description, tools, handoffs, prompt body); cannot be removed + (the six-role chain is an atomic unit, ADR-022) +- **Skills** — can be overridden entirely with a company version, or supplemented with new skills +- **Instructions and prompts** — fully under team control; vstack defaults are a starting point +- **Custompacks** — a named bundle of overrides and additions tracked in the manifest alongside first-party artifacts Planned direction: -- Add spend-control playbooks (budgets, allowance policies, and alert thresholds). -- Add monthly usage review checklist and optimization guidance for model/task selection. -- Add references for organization vs enterprise ownership boundaries. - -Ref: [GitHub — Monitoring your GitHub Copilot usage and entitlements](https://docs.github.com/en/copilot/how-tos/manage-and-track-spending/monitor-premium-requests) +- Define a `custompack` artifact type: a directory of templates with the same structure as + `src/vstack/_templates/` that a team maintains in their own repo +- `vstack install --pack <path>` installs the custompack after the base set; team artifacts + win on conflict for skills, instructions, and prompts; agents are merged, not replaced +- Track custompack artifacts in `vstack.json` with a `source: custompack` marker so + `vstack status` and `uninstall` can distinguish first-party from team-owned files +- Add a `vstack pack init` scaffold command to create a well-structured custompack starter -### Copilot admin observability pack [candidate] - -Relevance: - -- Copilot adoption at scale needs explicit operational KPIs and recurring review loops. -- Admin dashboards and reports exist, but teams need standardized interpretation and actions. -- Consistent observability improves rollout quality and policy compliance. - -Planned direction: +Overlay model (template source priority): -- Add an admin operations pack for usage/adoption KPI definitions and cadence. -- Add a standard reporting checklist for org and enterprise owners. -- Add escalation triggers for unusual usage patterns and policy drift. +- Multiple template sources resolved in priority order: upstream vstack first, custompack second +- Conflict resolution is deterministic: custompack wins for skills, instructions, and prompts; + agents are merged (custompack fields override, core structure preserved) +- Source-aware checksums in `vstack.json` so updates can be applied safely per origin +- Longer term: `vstack install --profile company` for named install profiles -Ref: [GitHub — Administer GitHub Copilot for your team](https://docs.github.com/en/copilot/how-tos/administer-copilot) - -### VS Code customization starter pack [candidate] +Ref: [GitHub — Customize Copilot for your project](https://docs.github.com/en/copilot/how-tos/copilot-on-github/customize-copilot/customize-copilot-overview) -Relevance: +### workflow contract source-of-truth [candidate — t.b.d.] -- Teams repeatedly ask how to create custom agents, custom instructions, prompts, and skills in VS Code. -- vstack already generates these artifact types, but onboarding the customization model can be clearer. -- A starter pack lowers adoption friction and keeps customization patterns consistent. +Partially realized: each agent's `config.yaml` already declares `artifacts.input`, `artifacts.output`, +and `handoffs` (target role, label, prompt). Role boundaries and artifact ownership are machine-readable +per agent today. -Planned direction: +What remains: -- Add a dedicated starter package with guided templates for custom agents, custom instructions, prompts, and skills. -- Include step-by-step examples for common workflows (domain agent, policy instruction, task prompt). -- Add a focused skill/instruction pair that teaches and validates Copilot customization patterns inside VS Code. +- A **central contract file** that aggregates all role I/O and gate definitions in one place, so an + orchestrator or validator can inspect the full pipeline without reading six separate files. +- **Generator-level validation** that input/output chains are consistent across roles (e.g. role B's + declared inputs exist in role A's declared outputs). +- **`docs/design/workflow.md`** stays as the human-readable explanation layer; the contract file + becomes the machine-readable source of truth it is derived from. -Ref: [GitHub — Customize Copilot for your project](https://docs.github.com/en/copilot/how-tos/copilot-on-github/customize-copilot/customize-copilot-overview) +This item is a prerequisite for the optional orchestrated role pipeline. It has no value in the +current single-call execution model beyond what the per-agent configs already provide. -### template overlays + selective install [candidate] +### optional orchestrated role pipeline [candidate — t.b.d.] -Relevance: +Possible future workflow with explicit orchestration (only if real coordination bottlenecks appear): -- Teams often need a company-specific Copilot stack rather than the default vstack set. -- A practical model is: keep upstream vstack as baseline, then layer private templates and extensions on top. -- Adoption is blocked when teams cannot control which artifact types are installed (`agents`, `skills`, `prompts`, `instructions`) or which source wins on conflicts. +- Each role makes its own model call +- Artifacts pass between roles via disk files +- User gates pause the pipeline at defined checkpoints +- Orchestrator role (product) manages pipeline state +- Parallel execution possible for multiple tester passes -Planned direction: +See `docs/design/workflow.md` for current execution and the orchestrated future model. -- Add support for multiple template sources in priority order (for example: upstream vstack first, company repo second). -- Define deterministic conflict resolution rules: `prefer-local`, `prefer-upstream`, and explicit `replace`/`extend` behavior per artifact. -- Add install selectors for coarse-grained artifact types (`--types agents,skills,prompts,instructions`) and optional fine-grained name selectors. -- Add install profiles in config/manifest (for example: `baseline`, `company`, `minimal`) to make repeat installs deterministic. -- Keep checksum/manifests source-aware so updates can be applied safely per origin. +### multi-IDE support [candidate — t.b.d.] -Initial UX target: +IntelliJ is the first candidate beyond VS Code. Not planned until after the current model stabilizes. +Requires separate template sets, different generator targets, and different frontmatter schemas. -- `vstack install --source upstream=... --source company=... --prefer company` -- `vstack install --types agents,skills` -- `vstack install --profile company` +______________________________________________________________________ ### heavy agent runtime framework [not planned] @@ -357,6 +293,12 @@ Not required for the current install model (`vstack install`) and kept out to re Kept optional; backend/microservice verification remains the default priority. +### install target directory override [not planned] + +Configuring the output root (e.g. `.cursor/` instead of `.github/`) is not implemented. +All mainstream AI editors read `.github/` without issue. This will only be reconsidered +if a concrete tool incompatibility with `.github/` is reported. + ______________________________________________________________________ ## decisions captured diff --git a/docs/releases/2026-04-26.md b/docs/releases/2026-04-26.md index 5e20b59..aa2b4a6 100644 --- a/docs/releases/2026-04-26.md +++ b/docs/releases/2026-04-26.md @@ -45,14 +45,14 @@ for retroactively adding checksums to legacy manifest entries. ## Artifacts reviewed -| Artifact | Status | -| ----------------------------------- | ----------------------------------------- | -| `docs/product/requirements.md` | ✓ | -| `docs/architecture/architecture.md` | ✓ | -| `docs/design/design.md` | ✓ | -| `docs/test-report.md` | ✓ updated (342/342, 100%) | -| `docs/security-report.md` | ✓ PASS with notes (no blockers) | -| `docs/performance-baseline.md` | ✓ present (not in scope for this release) | +| Artifact | Status | +| ------------------------------- | ----------------------------------------- | +| `docs/product/requirements.md` | ✓ | +| `docs/architecture/overview.md` | ✓ | +| `docs/design/design.md` | ✓ | +| `docs/test-report.md` | ✓ updated (342/342, 100%) | +| `docs/security-report.md` | ✓ PASS with notes (no blockers) | +| `docs/performance-baseline.md` | ✓ present (not in scope for this release) | ## Sign-offs diff --git a/docs/releases/2026-05-06.md b/docs/releases/2026-05-06.md new file mode 100644 index 0000000..2fc9f76 --- /dev/null +++ b/docs/releases/2026-05-06.md @@ -0,0 +1,123 @@ +# Release 2026-05-06 — v3.0.0 + +## Summary + +vstack 3.0.0 is a breaking release that introduces a dedicated `.vstack/` project-scope +directory and moves the install manifest from `.github/vstack.json` to +`.vstack/vstack.json`. It also adds a new `vstack init` command for idempotent artifact +regeneration (complementing `vstack install` as a first-run setup command), and delivers a +comprehensive alignment pass across all six agent templates and fifteen skill templates. + +Existing projects must run `vstack manifest upgrade` after upgrading to migrate the manifest. + +## Breaking changes + +- **Manifest relocated**: `vstack.json` moves from `.github/vstack.json` to + `.vstack/vstack.json`. Run `vstack manifest upgrade` to migrate. Any CI step or + tooling that reads `.github/vstack.json` directly must be updated. +- **`.vstack/` directory introduced**: `vstack install` now creates `.vstack/` on first + run and commits `config.yaml`, `vstack.json`, and role-scoped doc templates there + (ADR-019). + +## What's new + +- **`vstack init` command**: idempotent artifact regeneration command, intended for CI + pipelines and `pip install --upgrade` workflows. `vstack install` now delegates artifact + generation to `vstack init` internally, keeping first-run setup clearly separated from + ongoing regeneration (ADR-020). +- **`.vstack/config.yaml` — `exclude:` filter**: per-type and per-name exclusion of + installed artifacts. Set a type to `all` to skip every artifact of that type, or to a + list of names to skip individual artifacts. Agents can never be excluded — the six-role + chain is treated as an atomic unit. Config is honored on every `vstack install` and + `vstack init` run without repeating CLI flags (ADR-022). +- **`.vstack/config.yaml` — `artifacts.root` override**: projects with a non-standard + documentation root (e.g. `documentation/` instead of `docs/`) can now set + `artifacts: root: documentation` in `.vstack/config.yaml`. The override is read at + install/init time and passed through to `AgentGenerator` for path resolution (ADR-021). +- **`.vstack/.gitignore` seeded on every install run**: `vstack install` and `vstack init` + always write `.vstack/.gitignore` (ignoring `tmp/`). The file is regenerated + unconditionally to ensure it is present even in projects upgraded from older versions. +- **Agent `artifacts:` section generated from `config.yaml`**: each agent template can now + declare an `artifacts:` block in its `config.yaml`. `AgentGenerator` resolves this block + into `{{AGENT_ARTIFACTS_INPUT}}` and `{{AGENT_ARTIFACTS_OUTPUT}}` tokens injected into + the agent body, giving agents a machine-readable ownership record (ADR-021). +- **Agent passthrough behavior**: architect and designer now confirm explicitly when + a change does not affect their domain before passing through to the next stage. All + roles remain in every pipeline — passthrough is an explicit outcome, not an omission. +- **Mid-implementation subagent delegation**: the engineer role can now invoke `@architect` + or `@designer` as subagents to clarify constraints or contracts without triggering a + full gate cycle. +- **Broadened handoff triggers**: passthrough conditions in product, architect, and + designer handoff prompts are no longer restricted to incidents — any change outside a + role's domain can trigger a passthrough. + +## Improvements + +- **docs/delta concept removed**: delta workflow templates and WIP delta lines removed + from all six agent configs, ten skill templates, and all related documentation. The + install model no longer assumes a `docs/delta/` directory. +- **ADR-009 and ADR-010 path-agnostic**: primary artifact tables in the role model + and artifact flow ADRs now use conceptual domain names rather than hardcoded paths. + Default paths remain in ADR-021 and are configurable per project. +- **ADR-021 corrected**: per-role values updated — engineer target changed from `docs` + to `issues`, tester target to `docs/reports`, input globs made fully recursive. +- **Use case 3 corrected in workflow documentation**: all roles are now present in + every pipeline use case. Designer was previously absent from bug/incident flows. +- **release-notes skill genericized**: evidence check step no longer references + hardcoded project paths; invoking agent determines required artifacts. +- **threat-model skill**: output ownership clarified — `architect` role owns the + deliverable; default path is a configurable default, not a fixed constraint. +- **product/template.md**: `explore` and `analyse` are now correctly described as + skills invoked inline (`@#explore`, `@#analyse`), not as role handoff targets. +- **ADR-018 status updated**: noted that artifact paths are now configurable via ADR-021. +- **ADR-019–022 added**: architecture decisions for `.vstack/` project directory, install/init + command semantics, config-driven artifact paths, and selective exclude filter are all + recorded and accepted. +- **Roadmap**: version column added; `delta workflow templates (v3.0.0)` row removed; + v3.0.0 candidates rationalised — `selective install` marked shipped; governance skills + (`copilot-admin`, `spaces`) consolidated into a single `new skills (next batch)` row; + `template overlays` split into its own candidate row. +- **Terminology update**: "Option A/B" replaced with "direct execution / orchestrated + pipeline" across ADR-004 and all referencing docs. Terminology is now self-explanatory + without cross-referencing the ADR. +- **gh-issues skill**: updated to MCP-first guidance with `gh` CLI as the fallback, + reflecting current VS Code tooling capabilities. +- **Read/write relationship tables** in ADR-009, ADR-010, and `docs/design/workflow.md` + converted from wide tables to per-role lists for readability. + +## Internal + +- All six agent templates regenerated to reflect config and template changes. +- Fifteen skill templates regenerated. +- Test suite: 428 tests, 100% coverage (2,058 measured lines), py311–py314 all PASS. +- mypy strict: 51 files, 0 errors. +- ruff: clean. +- bandit: 1 LOW informational (B404 `subprocess` import advisory — unchanged from prior). +- pip-audit: 1 advisory (pip dev tooling only — unchanged from prior). + +## Artifacts reviewed + +| Artifact | Status | +| -------------------------------------- | ---------------- | +| `docs/product/requirements.md` | ✓ | +| `docs/product/roadmap.md` | ✓ | +| `docs/product/vision.md` | ✓ | +| `docs/architecture/overview.md` | ✓ | +| `docs/architecture/adr/019–022` | ✓ | +| `docs/design/overview.md` | ✓ | +| `docs/design/agents.md` | ✓ | +| `docs/design/workflow.md` | ✓ | +| `docs/reports/test-report.md` | ✓ 428/428 | +| `docs/reports/security-report.md` | ✓ PASS/notes | +| `docs/reports/performance-baseline.md` | ✓ no regressions | + +## Sign-offs + +| Role | Sign-off | +| --------- | ------------------------------------------------------------------------------------------------------------------------------ | +| product | ✓ requirements aligned; roadmap version column added; v3.0.0 features all marked shipped; candidates rationalised | +| architect | ✓ ADR-019–022 present and accepted; `.vstack/` structure sound; install/init semantics clear; exclude filter design documented | +| designer | ✓ agent config docs updated (2026-05-03); workflow docs consistent; `artifacts:` block schema documented in agents.md | +| engineer | ✓ implementation complete; 428 tests; 100% coverage (2,058 lines); mypy strict 0 errors; ruff clean | +| tester | ✓ all gates PASS; security: 1 LOW advisory (B404 informational, non-blocking); no performance regressions | +| release | ✓ all sign-offs collected; release notes current; CHANGELOG updated; PR ready | diff --git a/docs/performance-baseline.md b/docs/reports/performance-baseline.md similarity index 87% rename from docs/performance-baseline.md rename to docs/reports/performance-baseline.md index 31d078e..99470cb 100644 --- a/docs/performance-baseline.md +++ b/docs/reports/performance-baseline.md @@ -1,8 +1,8 @@ # Performance Baseline -**Branch:** `feat/improved_cli`\ -**Date:** 2026-04-26\ -**Scope:** CLI hot-path operations — parser build, target resolution, registry build; post-backfill feature addition\ +**Branch:** `feat/workflow_update`\ +**Date:** 2026-05-06\ +**Scope:** CLI hot-path operations — parser build, target resolution, registry build; `.vstack/` project-scope directory (ADR-019); `vstack install`/`vstack init` command semantics (ADR-020); manifest relocation (ADR-021); selective install with `exclude:` filter (ADR-022); `artifacts.root` config override; `.vstack/.gitignore` seeding; agent `artifacts:` section generation; ADR terminology update (Option A/B → direct execution/orchestrated pipeline); roadmap cleanup and gh-issues skill MCP-first guidance\ **Method:** `timeit.repeat` micro-benchmarks (Python 3.13.12, Linux) ______________________________________________________________________ @@ -71,7 +71,7 @@ ______________________________________________________________________ ## Test Suite Wall Time ``` -342 tests passed in 4.24s (pytest, with coverage) +428 tests passed in 5.90s (pytest, with coverage) ``` Acceptable. No slow test outliers observed. diff --git a/docs/security-report.md b/docs/reports/security-report.md similarity index 91% rename from docs/security-report.md rename to docs/reports/security-report.md index f59f5ad..c10be67 100644 --- a/docs/security-report.md +++ b/docs/reports/security-report.md @@ -1,8 +1,8 @@ # Security Report -**Branch:** `feat/improved_cli`\ -**Date:** 2026-04-26\ -**Scope:** Full source tree — static analysis (bandit) + dependency audit (pip-audit); security fixes for S-001 (assert guards) and S-002 (subprocess nosec)\ +**Branch:** `feat/workflow_update`\ +**Date:** 2026-05-06\ +**Scope:** Full source tree — `.vstack/` project-scope directory (ADR-019); `vstack install`/`vstack init` command semantics (ADR-020); manifest relocation from `.github/` to `.vstack/` (ADR-021); selective install with `exclude:` filter (ADR-022); `artifacts.root` config override; `.vstack/.gitignore` seeding; agent `artifacts:` section generation; ADR terminology update (Option A/B → direct execution/orchestrated pipeline); roadmap cleanup and gh-issues skill MCP-first guidance. Static analysis (bandit) + dependency audit (pip-audit); security fixes for S-001 (assert guards) and S-002 (subprocess nosec)\ **Method:** OWASP Top 10 + STRIDE (static analysis on a local CLI tool; no network surface, no auth surface, no DB) ______________________________________________________________________ diff --git a/docs/test-report.md b/docs/reports/test-report.md similarity index 59% rename from docs/test-report.md rename to docs/reports/test-report.md index 8bf6578..1bee490 100644 --- a/docs/test-report.md +++ b/docs/reports/test-report.md @@ -1,21 +1,21 @@ # Test Report -**Branch:** `feat/improved_cli`\ -**Date:** 2026-04-26\ -**Scope:** Full repository — CLI refactor (`catalog`, `report`, `registry`, `parser`, `interface`, `service`, `helpers`); manifest checksum backfill feature (`manifest/store.py`, `cli/service.py`, `cli/parser.py`, `cli/manifest.py`); security fixes (`cli/report.py` assert guards, `constants.py` nosec); **full test suite restructure** (per-module test files, TestClass layout, 342 tests) +**Branch:** `feat/workflow_update`\ +**Date:** 2026-05-06\ +**Scope:** Full repository — `.vstack/` project-scope directory (ADR-019); `vstack install`/`vstack init` command semantics (ADR-020); manifest relocation from `.github/` to `.vstack/` (ADR-021); selective install with `exclude:` filter (ADR-022); `artifacts.root` config override; `.vstack/.gitignore` seeding; agent `artifacts:` section generation; ADR terminology update (Option A/B → direct execution/orchestrated pipeline); roadmap cleanup (version column, candidate rationalisation); gh-issues skill MCP-first guidance. CLI refactor, manifest backfill, and test suite structure remain in scope as prior context. ______________________________________________________________________ ## Verdict -| Dimension | Result | -| ------------- | ------------------------------------------- | -| Functional | **PASS** — 342/342 tests green | -| Lint / Style | **PASS** — ruff clean | -| Type checking | **PASS** — mypy clean (106 files, 0 errors) | -| Coverage | **PASS** — 100.00% (fail-under=100) | -| Security | See `docs/security-report.md` | -| Performance | See `docs/performance-baseline.md` | +| Dimension | Result | +| ------------- | ------------------------------------------ | +| Functional | **PASS** — 428/428 tests green | +| Lint / Style | **PASS** — ruff clean | +| Type checking | **PASS** — mypy clean (51 files, 0 errors) | +| Coverage | **PASS** — 100.00% (fail-under=100) | +| Security | See `docs/reports/security-report.md` | +| Performance | See `docs/reports/performance-baseline.md` | > **Ship readiness: READY** — all verification gates currently pass. @@ -27,7 +27,7 @@ ______________________________________________________________________ platform: linux, Python 3.13.12-final-0 runner: pytest 9.0.3 + pytest-cov 7.1.0 command: pytest -q -342 passed in 4.24s +428 passed in 5.90s ``` All tests pass. No flaky, skipped, or xfail tests observed. @@ -36,7 +36,7 @@ ______________________________________________________________________ ## Coverage Summary -Total: 100.00% — 0 missed statements across 1,828 measured +Total: 100.00% — 0 missed statements across 2,058 measured `fail-under=100` is configured in `pyproject.toml`. This gate is **passing**. @@ -46,7 +46,7 @@ All modules are now at 100% statement coverage. Blockage coverage findings were resolved by adding targeted unit tests for: -- per-module test files replacing the `test_commands.py` / `test_coverage_blockers.py` catch-all (test count: 288 → 342) +- per-module test files replacing the `test_commands.py` / `test_coverage_blockers.py` catch-all (test count: 288 → 428) - manifest subcommand dispatch and missing-action path - service wrappers and `manifest_upgrade` success/error branches - parser config guard rails (`scope_help` / `only_help` validation) @@ -63,7 +63,7 @@ ______________________________________________________________________ ``` ruff check src tests → All checks passed! -python -m mypy src tests → Success: no issues found in 106 source files +python -m mypy src tests → Success: no issues found in 51 source files ``` No lint or type findings. diff --git a/src/vstack/_templates/agents/architect/artifacts/adr/NNN-template.md b/src/vstack/_templates/agents/architect/artifacts/adr/NNN-template.md new file mode 100644 index 0000000..ac015e0 --- /dev/null +++ b/src/vstack/_templates/agents/architect/artifacts/adr/NNN-template.md @@ -0,0 +1,27 @@ +# ADR-NNN: {title} + +<!-- One file per decision. Suggested name: NNN-{slug}.md (e.g. 001-service-boundaries.md) +Deviate from this structure as needed. --> + +> **date:** YYYY-MM-DD\ +> **status:** proposed | accepted | superseded by ADR-NNN + +## context + +<!-- What situation or problem led to this decision? --> + +## decision + +<!-- What was decided? State it clearly and positively. --> + +## alternatives considered + +<!-- What other options were evaluated? Why were they not chosen? --> + +## rationale + +<!-- Why is this decision the right one given the context? --> + +## consequences + +<!-- What is easier or harder as a result? What does this enable or constrain? --> diff --git a/src/vstack/_templates/agents/architect/artifacts/overview.md b/src/vstack/_templates/agents/architect/artifacts/overview.md new file mode 100644 index 0000000..4bf345b --- /dev/null +++ b/src/vstack/_templates/agents/architect/artifacts/overview.md @@ -0,0 +1,41 @@ +# Architecture Overview + +<!-- Describes the system architecture. Deviate from this structure as needed. --> + +> Maintained by: **architect** role\ +> Last updated: YYYY-MM-DD + +## overview + +<!-- What system is this? What problem does it solve? +Include a system style declaration: e.g. `**System style:** microservice | monolith | platform | library` --> + +## system structure + +<!-- Repository or deployment layout. Use a plain text tree for directory structure. --> + +```text +{system}/ +├── ... +``` + +## components + +<!-- High-level component breakdown. Use a Mermaid diagram for flows; text tree for structure. --> + +## principles + +<!-- Key architectural constraints and non-negotiable decisions. +These constrain all downstream design choices. --> + +## data flow + +<!-- How data moves through the system. --> + +## key decisions + +<!-- Links or summaries of significant ADRs. --> + +## open questions + +<!-- Unresolved architectural concerns. --> diff --git a/src/vstack/_templates/agents/architect/config.yaml b/src/vstack/_templates/agents/architect/config.yaml index 0e15fe1..2f67021 100644 --- a/src/vstack/_templates/agents/architect/config.yaml +++ b/src/vstack/_templates/agents/architect/config.yaml @@ -1,13 +1,11 @@ name: architect -version: 20260502015 +version: 20260503022 description: > Senior software architect. Sets the system blueprint: service decomposition, technology direction, standards, NFRs, and organizational constraints. Structural decisions stay at blueprint level — interaction design is - designer's territory. Reads docs/product/vision.md and - docs/product/requirements.md; produces docs/architecture/architecture.md and - docs/architecture/adr/*.md. Baseline-first on branch, optional - docs/delta/{id} for complex drafts. + designer's territory. Reads product artifacts; produces architecture overview + and ADRs. Baseline-first on branch. argument-hint: "[design architecture | write ADR | review architecture | check implementation alignment]" tools: - read @@ -23,11 +21,20 @@ model: - Claude Opus 4.7 (copilot) agents: ["*"] handoffs: - - label: Continue to design + - label: "Go to next stage: Design" agent: designer prompt: > - Translate docs/architecture/architecture.md into docs/design/design.md - with concrete interfaces and contracts. + Architecture outputs are approved. Assess the current state and produce + design specifications as needed. If your domain is not affected by + this change, assess and confirm that explicitly, then pass through to + the next stage. +artifacts: + dir: architecture + input: + - product/**/*.md + output: + - overview.md + - adr/*.md target: vscode user-invocable: true diff --git a/src/vstack/_templates/agents/architect/template.md b/src/vstack/_templates/agents/architect/template.md index 29d07d3..141a1e9 100644 --- a/src/vstack/_templates/agents/architect/template.md +++ b/src/vstack/_templates/agents/architect/template.md @@ -56,12 +56,26 @@ Signal readiness before downstream work proceeds: Handoffs you own: - To designer: system style, boundaries, NFRs, failure modes, and constrained tradeoffs. +- Pass-through: if the architecture is not affected by this change, confirm that explicitly before passing through. - Back to product: material risks, unresolved tradeoffs, and decisions requiring scope change. +## assess current state + +Before producing any output, scan your configured input artifacts to determine +what work is needed: + +1. Read your input artifacts. +1. Identify artifacts that require action: + - Issues or change requests with status `open` or `draft` that touch architecture. + - Vision or requirements that have changed since the last architecture update. + - ADRs with status `proposed` that require a decision. +1. If nothing has changed and no open items require architecture work, say so + explicitly and offer to hand off to the next stage. + ## how you work -1. Read `docs/product/vision.md` and `docs/product/requirements.md`. If either is missing, stop and request product clarification. -1. **Declare system style** in `docs/architecture/architecture.md`: +1. Assess current state (see above) before touching any output artifact. +1. **Declare system style** in the architecture overview: - `backend-only` — API, service, library, CLI, data pipeline - `frontend-only` — UI, static site, design system - `fullstack` — API + UI tightly coupled @@ -70,7 +84,7 @@ Handoffs you own: 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 or update the architecture overview via `@#architecture`. 1. Write ADRs via `@#adr` for each significant structural decision. 1. Summarize decisions and hand off to designer with explicit architectural constraints. @@ -85,12 +99,14 @@ Handoffs you own: - Conflicting constraints or unresolvable tradeoffs: escalate to user with options. - Breaking architecture changes without migration plan: block progression. -## artifacts you own +## artifacts you use + +{{AGENT_ARTIFACTS_INPUT}} + +{{AGENT_ARTIFACTS_OUTPUT}} -| Artifact | Role | -| ----------------------------------- | ------- | -| `docs/architecture/architecture.md` | creator | -| `docs/architecture/adr/NNN-*.md` | creator | +Agents do not write to artifacts owned by other roles. If you discover something +that requires changes to upstream artifacts, flag it and trigger a reverse handoff. ## completion checklist diff --git a/src/vstack/_templates/agents/designer/artifacts/overview.md b/src/vstack/_templates/agents/designer/artifacts/overview.md new file mode 100644 index 0000000..37fdc1f --- /dev/null +++ b/src/vstack/_templates/agents/designer/artifacts/overview.md @@ -0,0 +1,39 @@ +# Design Overview + +<!-- Translates the architecture blueprint into implementable interfaces, data schemas, + state models, and error contracts. Deviate from this structure as needed. --> + +> Maintained by: **designer** role\ +> Last updated: YYYY-MM-DD + +## overview + +<!-- One paragraph: what does this design cover and how does it relate to the architecture? + Reference docs/architecture/overview.md rather than restating it. --> + +## domain model + +<!-- Key entities, their states, fields, and relationships. Include a state machine + if entities have a lifecycle (e.g. draft → approved → archived). --> + +## interfaces + +<!-- Component interfaces, module boundaries, API/CLI contracts, request/response shapes, + status codes, and error conventions. Link to OpenAPI spec if applicable. --> + +## data flows + +<!-- Sequence of operations for the primary use cases. Use Mermaid sequence diagrams + when the interaction between components matters more than the step list. --> + +## error contract + +<!-- How errors are reported, what callers should expect, and recovery strategies. --> + +## design principles + +<!-- Guiding constraints and tradeoffs that shaped this design. --> + +## open questions + +<!-- Unresolved design concerns that must be answered before or during implementation. --> diff --git a/src/vstack/_templates/agents/designer/config.yaml b/src/vstack/_templates/agents/designer/config.yaml index a77756a..bdd0bf9 100644 --- a/src/vstack/_templates/agents/designer/config.yaml +++ b/src/vstack/_templates/agents/designer/config.yaml @@ -1,12 +1,10 @@ name: designer -version: 20260502016 +version: 20260503024 description: > Senior interaction designer. Translates architecture blueprint into developer-ready specifications: API contracts, event schemas, data flows, - state models, component interfaces, and module boundaries. Reads - docs/architecture/architecture.md; produces docs/design/design.md and (if - user-facing) docs/design/ux.md. Baseline-first on branch, optional - docs/delta/{id} for complex drafts. + state models, component interfaces, and module boundaries. Reads architecture + artifacts; produces design overview. Baseline-first on branch. argument-hint: "[write design | API contracts | event and data flows | state models | interaction review]" tools: - read @@ -21,11 +19,24 @@ model: - GPT-5.3-Codex (copilot) agents: ["*"] handoffs: - - label: Continue to engineering + - label: "Go to next stage: Engineering" agent: engineer prompt: > - Implement docs/design/design.md and add/update tests for the defined - interfaces and flows. + Design outputs are approved. Assess the current state and implement + code and tests as needed. If your domain is not affected by this + change, assess and confirm that explicitly, then pass through to + engineering. If working on an issue, document findings in RCA or + post-mortem artifacts as relevant. +artifacts: + dir: design + input: + - architecture/**/*.md + output: + - path: overview.md + - path: ux.md + notes: frontend/fullstack scope only + - path: "**/*.md" + notes: additional detail docs per component, model, system, or domain (when scope warrants it) target: vscode user-invocable: true diff --git a/src/vstack/_templates/agents/designer/template.md b/src/vstack/_templates/agents/designer/template.md index b514868..f264149 100644 --- a/src/vstack/_templates/agents/designer/template.md +++ b/src/vstack/_templates/agents/designer/template.md @@ -7,7 +7,7 @@ You are a **senior interaction designer** acting as the **designer role**. You t ## responsibilities - Own contract-level and interaction-level design: API contracts, event schemas, data flows, state models, component interfaces, module boundaries. -- If user-facing scope: also own `docs/design/ux.md` — user flows, component hierarchy, interaction patterns. +- If user-facing scope: also own the UX design artifact — user flows, component hierarchy, interaction patterns. - Flag design gaps or architectural inconsistencies to architect. ## scope and boundaries @@ -48,7 +48,7 @@ You are a **senior interaction designer** acting as the **designer role**. You t ## scope detection -Read `docs/architecture/architecture.md` to determine the system style, then apply the relevant design disciplines: +Read the architecture overview to determine the system style, then apply the relevant design disciplines: | System style | Design tasks | | -------------------------------------- | -------------------------------------------------------------------- | @@ -71,12 +71,26 @@ Signal readiness before implementation proceeds: Handoffs you own: - To engineer: actionable contracts, state models, validation rules, and edge-case behavior. +- Pass-through: if the design is not affected by this change, confirm that explicitly before passing through. - Back to architect: design findings that require structural changes. +## assess current state + +Before producing any output, scan your configured input artifacts to determine +what work is needed: + +1. Read your input artifacts. +1. Identify artifacts that require action: + - Architecture overview or ADRs updated since the last design revision. + - Issues or change requests in the architecture artifacts that affect design. + - Design overview missing or inconsistent with current architecture. +1. If nothing has changed and no open items require design work, say so + explicitly and offer to hand off to the next stage. + ## how you work -1. Read `docs/architecture/architecture.md`, `docs/architecture/adr/*.md`, `docs/product/vision.md`, `docs/product/requirements.md`. -1. If `docs/architecture/architecture.md` is missing or too vague to design from, stop and hand off to architect. +1. Assess current state (see above) before touching any output artifact. +1. If the architecture overview 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 @@ -84,12 +98,14 @@ Handoffs you own: - Define state models where applicable (states, transitions, triggers, terminal states) - Define error cases and how they are communicated to callers 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. If user-facing scope: design UX flows and write the UX design artifact. +1. Write or update the design overview (always). 1. Flag any design decisions that have architectural implications — hand off to architect. ## success criteria +- Design overview covers implementation contracts, schemas, and CLI specs. +- If user-facing scope: UX design artifact covers user flows, component hierarchy, and interaction patterns. - Design docs are actionable without guesswork. - API/interface contracts and error cases are explicit. @@ -99,12 +115,14 @@ Handoffs you own: - Contract conflicts with architecture: escalate before implementation. - Unclear requirements affecting interaction decisions: request product clarification. -## artifacts you own +## artifacts you use + +{{AGENT_ARTIFACTS_INPUT}} + +{{AGENT_ARTIFACTS_OUTPUT}} -| Artifact | Role | -| ----------------------- | --------------------------------------- | -| `docs/design/design.md` | creator | -| `docs/design/ux.md` | creator (frontend/fullstack scope only) | +Agents do not write to artifacts owned by other roles. If you discover something +that requires changes to upstream artifacts, flag it and trigger a reverse handoff. ## completion checklist diff --git a/src/vstack/_templates/agents/engineer/artifacts/issues/postmortem.md b/src/vstack/_templates/agents/engineer/artifacts/issues/postmortem.md new file mode 100644 index 0000000..7039841 --- /dev/null +++ b/src/vstack/_templates/agents/engineer/artifacts/issues/postmortem.md @@ -0,0 +1,42 @@ +# {id}: {title} — Post-Mortem + +<!-- One file per incident. Suggested name: {id}-{slug}-postmortem.md (e.g. 001-login-timeout-postmortem.md) +Deviate from this structure as needed. --> + +> **date:** YYYY-MM-DD\ +> **severity:** P{1–4}\ +> **status:** draft | review | closed\ +> **issue:** [{id}](%7Bid%7D-%7Bslug%7D.md)\ +> **rca:** [{id}-{slug}-rca.md](%7Bid%7D-%7Bslug%7D-rca.md) + +## summary + +<!-- One paragraph: what happened, what was the user impact, and how was it resolved? --> + +## timeline + +| Time (UTC) | Event | +| ---------- | ----- | +| HH:MM | | + +## impact + +<!-- Who was affected, for how long, and what was the observable effect? --> + +## root cause + +<!-- Brief restatement of the root cause from the RCA. Link to the RCA for full analysis. --> + +## resolution + +<!-- What was done to restore service? --> + +## action items + +| Item | Owner | Due | Status | +| ---- | ----- | --- | ------ | +| | | | | + +## lessons learned + +<!-- What would we do differently? What systemic improvements does this suggest? --> diff --git a/src/vstack/_templates/agents/engineer/artifacts/issues/rca.md b/src/vstack/_templates/agents/engineer/artifacts/issues/rca.md new file mode 100644 index 0000000..ce48e5a --- /dev/null +++ b/src/vstack/_templates/agents/engineer/artifacts/issues/rca.md @@ -0,0 +1,39 @@ +# {id}: {title} — Root Cause Analysis + +<!-- One file per incident. Suggested name: {id}-{slug}-rca.md (e.g. 001-login-timeout-rca.md) +Deviate from this structure as needed. --> + +> **date:** YYYY-MM-DD\ +> **severity:** P{1–4}\ +> **status:** draft | in-progress | resolved\ +> **issue:** [{id}](%7Bid%7D-%7Bslug%7D.md) + +## what happened + +<!-- Factual description of the incident. --> + +## root cause + +<!-- The underlying cause, not the proximate trigger. --> + +## contributing factors + +<!-- What conditions made this possible? --> + +## detection + +<!-- How was this discovered? How long did it take? --> + +## resolution + +<!-- What was done to restore service? --> + +## action items + +| Item | Owner | Due | +| ---- | ----- | --- | +| | | | + +## lessons learned + +<!-- What would we do differently? --> diff --git a/src/vstack/_templates/agents/engineer/config.yaml b/src/vstack/_templates/agents/engineer/config.yaml index dbdd673..ef5d39f 100644 --- a/src/vstack/_templates/agents/engineer/config.yaml +++ b/src/vstack/_templates/agents/engineer/config.yaml @@ -1,11 +1,10 @@ name: engineer -version: 20260502017 +version: 20260503024 description: > Senior software engineer. Implements features, bug fixes, and unit tests - based on docs/design/design.md, docs/architecture/architecture.md, and ADRs. - Reviews code for correctness and architectural alignment. Debugs issues - root-cause first. Baseline-first on branch, optional docs/delta/{id} for - complex context only. + based on the approved design, architecture, and ADRs. Reviews code for + correctness and architectural alignment. Debugs issues root-cause first. + Baseline-first on branch. argument-hint: "[implement feature | fix bug | refactor area | review code | debug issue | update tests]" tools: - read @@ -21,11 +20,26 @@ model: - Claude Sonnet 4.6 (copilot) agents: ["*"] handoffs: - - label: Run verification + - label: "Go to next stage: Verification" agent: tester prompt: > - Run verification and produce/update test, security, and performance - reports. + Implementation is approved. Assess the current state and verify the + implementation as needed — run tests, security checks, and performance + analysis. If this is an issue (bug, problem, or incident), also produce + or update an RCA and, if stakeholder impact is significant, a + post-mortem. +artifacts: + input: + - product/**/*.md + - architecture/**/*.md + - design/**/*.md + output: + - path: ./src/**/* + - path: ./tests/**/* + - path: ./issues/{id}-{slug}-rca.md + notes: when working on an issue + - path: ./issues/{id}-{slug}-postmortem.md + notes: when stakeholder impact is significant target: vscode user-invocable: true diff --git a/src/vstack/_templates/agents/engineer/template.md b/src/vstack/_templates/agents/engineer/template.md index c10e33c..babef0b 100644 --- a/src/vstack/_templates/agents/engineer/template.md +++ b/src/vstack/_templates/agents/engineer/template.md @@ -7,7 +7,7 @@ You are a **senior software engineer** acting as the **engineer role**. You buil ## responsibilities - Own implementation quality: features, bug fixes, refactors, and code-level correctness. -- Deliver code aligned with `docs/product/requirements.md`, `docs/design/design.md`, `docs/architecture/architecture.md`, and `docs/architecture/adr/*.md`. +- Deliver code aligned with approved input artifacts. - Write and maintain unit tests alongside implementation. ## scope and boundaries @@ -56,21 +56,37 @@ Signal readiness before downstream verification: Handoffs you own: - To tester: verification targets, risk areas, and changed behavior summary. -- Back to architect/designer/product: blockers caused by missing or conflicting contracts. +- Mid-implementation subagents: invoke `@architect` or `@designer` to clarify constraints or contracts without triggering a full gate cycle. Integrate their output before continuing. +- Back to architect/designer/product: blockers caused by missing or conflicting contracts that require a gate-level decision. ## parallel delegation For `fullstack` or `integration` system styles, split work across specialized subagents: -- Identify independent workstreams from `docs/design/design.md` (for example: frontend, backend, integration layer). +- Identify independent workstreams from the design overview (for example: frontend, backend, integration layer). - Delegate each workstream to a separate `@engineer` subagent with a scoped task description. - Collect and integrate results before handing off to tester. Only delegate when workstreams are genuinely independent. +## assess current state + +Before writing any code, scan your configured input artifacts to determine +what work is needed: + +1. Read your input artifacts. +1. Identify artifacts that require action: + - Issues with status `open` or `in-progress`. + - Change requests or requirements not yet reflected in code. + - Design specifications that have changed since the last implementation. +1. For issues (bugs, problems, incidents): check whether an RCA exists. If not, + plan to produce one after the fix. +1. If nothing requires implementation work, say so explicitly and offer to hand + off to the next stage. + ## how you work -1. Read upstream artifacts before touching code. +1. Assess current state (see above) before touching any code. 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. @@ -90,12 +106,14 @@ Only delegate when workstreams are genuinely independent. - High-risk defects discovered: escalate immediately with mitigation options. - Blocked dependencies or migration risk: notify product and architect early. -## artifacts you own +## artifacts you use + +{{AGENT_ARTIFACTS_INPUT}} + +{{AGENT_ARTIFACTS_OUTPUT}} -| Artifact | Role | -| ----------- | ------- | -| source code | creator | -| unit tests | creator | +Agents do not write to artifacts owned by other roles. If you discover something +that requires changes to upstream artifacts, flag it and trigger a reverse handoff. ## completion checklist @@ -121,7 +139,9 @@ Only delegate when workstreams are genuinely independent. - `@#refactor` — structured refactoring without behavior change - `@#openapi` — OpenAPI 3.1 spec writing and review - `@#dependency` — dependency health audit -- `@#incident` — incident analysis and post-mortem writing +- `@#incident` — incident analysis and coordination (delegates to rca + postmortem) +- `@#rca` — root cause analysis document writing +- `@#postmortem` — blameless post-mortem document writing - `@#dependabot` — configure automated dependency updates - `@#secret-scan` — configure GitHub secret scanning and push protection - `@#gdpr` — GDPR engineering practices for data models, APIs, logging, and retention diff --git a/src/vstack/_templates/agents/product/artifacts/changes/change-request.md b/src/vstack/_templates/agents/product/artifacts/changes/change-request.md new file mode 100644 index 0000000..592ec44 --- /dev/null +++ b/src/vstack/_templates/agents/product/artifacts/changes/change-request.md @@ -0,0 +1,42 @@ +# Change Request: {title} + +<!-- One file per proposed change. Suggested name: {prefix}-{id}-{slug}.md (e.g. RFC-001-auth-redesign.md) +Deviate from this structure as needed. --> + +> **date:** YYYY-MM-DD\ +> **status:** draft | review | approved | rejected\ +> **author:** @handle\ +> **stakeholders:** @handle, @handle + +## summary + +<!-- One paragraph: what change is proposed and why. --> + +## rationale + +<!-- Why is this change needed now? What user or system problem does this solve? +What driver, event, or opportunity makes this the right time? --> + +## as-is + +<!-- Describe the current state: how things work today, what the pain points are, and what constraints exist. --> + +## to-be + +<!-- Describe the desired future state: how things will work after this change, and what improves. --> + +## proposed approach + +<!-- How will the change be implemented? Include key design decisions, phasing, or migration steps if relevant. --> + +## alternatives considered + +<!-- What other approaches were evaluated and why were they rejected? --> + +## risks and dependencies + +<!-- What could go wrong? What must be true before this can proceed? Who or what does this depend on? --> + +## success criteria + +<!-- How do we know this change has been successfully delivered? --> diff --git a/src/vstack/_templates/agents/product/artifacts/issues/issue.md b/src/vstack/_templates/agents/product/artifacts/issues/issue.md new file mode 100644 index 0000000..446c4fc --- /dev/null +++ b/src/vstack/_templates/agents/product/artifacts/issues/issue.md @@ -0,0 +1,38 @@ +# Issue: {title} + +<!-- One file per issue. Suggested name: {id}-{slug}.md (e.g. 001-login-timeout.md) +Deviate from this structure as needed. --> + +> **date:** YYYY-MM-DD\ +> **status:** open | in-progress | resolved\ +> **author:** @handle\ +> **kind:** bug | problem | incident + +## summary + +<!-- One paragraph: what is observed and what is the impact. --> + +## steps to reproduce + +<!-- For bugs: minimal steps to reproduce. For incidents: timeline of events. --> + +## expected behaviour + +<!-- What should happen? --> + +## actual behaviour + +<!-- What happens instead? --> + +## environment + +<!-- For bugs: Python version, OS, relevant package versions, runtime context. +For incidents: affected environment (prod/staging), region, deployment version. --> + +## workaround + +<!-- Is there a known workaround while this is open? If none, state "none known". --> + +## next steps + +<!-- Immediate actions and who owns them. --> diff --git a/src/vstack/_templates/agents/product/artifacts/requirements.md b/src/vstack/_templates/agents/product/artifacts/requirements.md new file mode 100644 index 0000000..66196f7 --- /dev/null +++ b/src/vstack/_templates/agents/product/artifacts/requirements.md @@ -0,0 +1,42 @@ +# Requirements + +<!-- Product requirements document. Deviate from this structure as needed. --> + +> Maintained by: **product** role\ +> Last updated: YYYY-MM-DD + +______________________________________________________________________ + +## context + +<!-- One paragraph: what is this product, who uses it, and what problem does it solve? +Reference vision.md rather than restating it in full. --> + +______________________________________________________________________ + +## functional requirements + +| ID | Requirement | Status | +| ---- | ------------ | ------ | +| FR-1 | {capability} | draft | + +### FR-1 — {capability} + +<!-- What the system must do. Use "must", "should", "may" to signal priority. +Add a row to the index and a matching FR-N section for each requirement. --> + +______________________________________________________________________ + +## non-functional requirements + +| ID | Requirement | +| ----- | ----------- | +| NFR-1 | | + +______________________________________________________________________ + +## success criteria + +<!-- Conditions that must be true for the product to be considered successful. --> + +1. {criterion} diff --git a/src/vstack/_templates/agents/product/artifacts/roadmap.md b/src/vstack/_templates/agents/product/artifacts/roadmap.md new file mode 100644 index 0000000..344ab79 --- /dev/null +++ b/src/vstack/_templates/agents/product/artifacts/roadmap.md @@ -0,0 +1,73 @@ +# Roadmap + +<!-- Product roadmap. Deviate from this structure as needed. +Status values: shipped | planned | candidate | not planned --> + +> Maintained by: **product** role\ +> Last updated: YYYY-MM-DD + +______________________________________________________________________ + +## status overview + +<!-- One table covering all items. Add rows as features are identified. --> + +| feature | status | notes | +| ------- | ------ | ----- | +| | | | + +Legend: **shipped** = done; **planned** = committed for next release; **candidate** = optional, not committed; **not planned** = evaluated and intentionally excluded. + +______________________________________________________________________ + +## shipped + +<!-- One subsection per logical feature group. Use bullets for what was built. +Reference ADRs or design docs where relevant. Add [v{version}] in the heading if applicable. --> + +### {feature group} [shipped — v{version}] + +<!-- Optional: one sentence of context if the table entry alone is not self-explanatory. --> + +- what was built +- what changed and why it matters +- any ADR or design doc reference + +______________________________________________________________________ + +## planned + +<!-- Committed for the next release. Each subsection describes the goal, +key design decisions, and any open questions or blockers. --> + +### {feature or milestone} [planned — v{version}] + +<!-- Why is this being built now? What is the expected outcome? --> + +Planned direction: + +- key deliverable +- key deliverable +- any dependency or prerequisite + +______________________________________________________________________ + +## candidates + +<!-- Optional future features. Not committed. Include enough context to evaluate later — +rationale for why this is on the radar, rough shape, and what would trigger prioritisation. --> + +### {candidate feature} [candidate] + +<!-- What is this and why is it on the radar? +What would need to be true for this to become planned? --> + +______________________________________________________________________ + +## not planned + +<!-- Evaluated and deliberately excluded. Record the reason so it is not re-debated. --> + +| item | reason | +| ---- | ------ | +| | | diff --git a/src/vstack/_templates/agents/product/artifacts/vision.md b/src/vstack/_templates/agents/product/artifacts/vision.md new file mode 100644 index 0000000..833a89c --- /dev/null +++ b/src/vstack/_templates/agents/product/artifacts/vision.md @@ -0,0 +1,38 @@ +# Vision + +<!-- Product vision document. Deviate from this structure as needed. --> + +> Maintained by: **product** role\ +> Last updated: YYYY-MM-DD + +## what is {product} + +<!-- One to three paragraphs: what the system is, what it does, and what makes it different. +Write as prose, not bullets. --> + +______________________________________________________________________ + +## why it exists + +<!-- What problem does this solve and for whom? What gap or pain makes this worth building? +Write as prose — describe the situation before this product exists. --> + +______________________________________________________________________ + +## who it is for + +<!-- Who are the primary users? What do they need, what frustrates them today, and what do they not want? +Be specific: a vague user description produces vague product decisions. --> + +______________________________________________________________________ + +## what success looks like + +<!-- What does the world look like when this has worked? What can users do that they could not before? +Prefer observable outcomes over vanity metrics. --> + +______________________________________________________________________ + +## out of scope + +<!-- What are we explicitly not doing? Be deliberate — this shapes what the product does not become. --> diff --git a/src/vstack/_templates/agents/product/config.yaml b/src/vstack/_templates/agents/product/config.yaml index 93f1851..ecf5688 100644 --- a/src/vstack/_templates/agents/product/config.yaml +++ b/src/vstack/_templates/agents/product/config.yaml @@ -1,11 +1,10 @@ name: product -version: 20260502018 +version: 20260503021 description: > Senior product manager. Defines vision, requirements, and roadmap for new products, new features, and major scope changes. Baseline-first on branch: - update docs/product directly and orchestrate role-owned baseline updates in - docs/architecture and docs/design. Optional docs/delta/{intake-id} is for - complex temporary drafts only. + update product artifacts directly and orchestrate role-owned baseline updates + in architecture and design. Baseline-first on branch. argument-hint: "[vision | requirements | scope review | acceptance review | release readiness check]" tools: - read @@ -21,12 +20,21 @@ model: - Claude Opus 4.7 (copilot) agents: ["*"] handoffs: - - label: Continue to architecture + - label: "Go to next stage: Architecture" agent: architect prompt: > - Use docs/product/vision.md and docs/product/requirements.md to - produce/update docs/architecture/architecture.md and - docs/architecture/adr/*.md. + Product outputs are approved. Assess the current state and produce or + update the architecture as needed. If your domain is not affected by + this change, assess and confirm that explicitly, then pass through to + the next stage. +artifacts: + dir: product + output: + - vision.md + - requirements.md + - roadmap.md + - changes/*.md + - issues/*.md target: vscode user-invocable: true diff --git a/src/vstack/_templates/agents/product/template.md b/src/vstack/_templates/agents/product/template.md index abd0050..4008026 100644 --- a/src/vstack/_templates/agents/product/template.md +++ b/src/vstack/_templates/agents/product/template.md @@ -58,17 +58,17 @@ You pause the pipeline at key moments and wait for explicit user confirmation: Handoffs you own: -- To architect/designer/engineer: clear scope, acceptance criteria, and known constraints. -- To release: explicit acceptance decision, unresolved risks, and blocked items (if any). +- Happy path only: one forward continuation to architect after user approval. +- For non-happy paths (`NOK`, blockers, missing artifacts), do not use handoff buttons; ask user to choose the recovery path. ## 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. -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` -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. **Choose flow** (skills are invoked inline; roles receive a handoff after user approval): + - Brownfield discovery: `@#requirements` → `@#explore` → `@#analyse` → handoff to `architect` + - New feature: `@#requirements` → handoff to `architect` → `designer` → `engineer` → `tester` → `release` + - Existing behavior change: `@#requirements` → `@#debug` → handoff to `architect` (light) → `engineer` → `tester` → `release` +1. **Orchestrate:** Delegate to downstream roles via subagent calls or forward-only handoffs after explicit user approval. 1. **Gate:** Confirm with user at each transition before proceeding. 1. **Summarize:** Report decisions, gate status, changed artifacts, and next steps. @@ -84,14 +84,14 @@ Handoffs you own: - If tester reports unresolved blockers: do not release. - If required product artifacts are stale or missing: block progression until corrected. -## artifacts you own +## artifacts you use -| Artifact | Role | -| ------------------------------------ | ------- | -| `docs/product/vision.md` | creator | -| `docs/product/requirements.md` | creator | -| `docs/product/roadmap.md` | creator | -| gate decisions and acceptance record | creator | +{{AGENT_ARTIFACTS_INPUT}} + +{{AGENT_ARTIFACTS_OUTPUT}} + +Agents do not write to artifacts owned by other roles. If you discover something +that requires changes to upstream artifacts, flag it and trigger a reverse handoff. ## completion checklist diff --git a/src/vstack/_templates/agents/release/artifacts/release-summary.md b/src/vstack/_templates/agents/release/artifacts/release-summary.md new file mode 100644 index 0000000..da5cc83 --- /dev/null +++ b/src/vstack/_templates/agents/release/artifacts/release-summary.md @@ -0,0 +1,48 @@ +# Release Summary — YYYY-MM-DD + +<!-- One file per release. Suggested name: YYYY-MM-DD.md (e.g. 2026-05-03.md) +Summary and sign-off record for a release. Release notes are generated separately +by the release-notes skill or CI workflow. Deviate from this structure as needed. --> + +> **date:** YYYY-MM-DD\ +> **version:** vX.Y.Z — assign when tagging\ +> **status:** draft | published + +## summary + +<!-- One paragraph: what does this release deliver and who is affected? --> + +## breaking changes + +<!-- List breaking changes. Remove this section if none. --> + +## what's new + +<!-- List new features and capabilities. --> + +## fixed + +<!-- List bug fixes. --> + +## internal + +<!-- Non-user-facing changes: refactors, test improvements, CI, documentation, ADRs. --> + +## upgrade notes + +<!-- Migration steps required. Remove this section if none. --> + +## artifacts reviewed + +<!-- List each role artifact that was reviewed or updated for this release. --> + +| Artifact | Status | +| -------- | ----------- | +| | ✓ / updated | + +## sign-offs + +| Role | Sign-off | +| ------- | -------- | +| Tester | | +| Product | | diff --git a/src/vstack/_templates/agents/release/config.yaml b/src/vstack/_templates/agents/release/config.yaml index 07e6917..84b9420 100644 --- a/src/vstack/_templates/agents/release/config.yaml +++ b/src/vstack/_templates/agents/release/config.yaml @@ -1,11 +1,10 @@ name: release -version: 20260502019 +version: 20260503020 description: > Senior platform and release engineer. Acts as release gatekeeper: verifies - baseline artifacts are complete (docs/product, docs/architecture, - docs/design, tester reports), collects explicit sign-offs from tester and - product, then produces a dated release document and creates the PR. Ensures - optional docs/delta/{id} content is consolidated and cleaned up before merge. + baseline artifacts are complete across all roles, collects explicit cross-role + sign-off reviews, then produces a dated release document and creates the PR. + Ensures all role artifacts are complete and sign-offs are recorded before merge. argument-hint: "[release readiness | compile release notes | collect sign-offs | open release PR]" tools: - read @@ -19,12 +18,13 @@ model: - Claude Sonnet 4.6 (copilot) - GPT-5.3-Codex (copilot) agents: ["*"] -handoffs: - - label: Product sign-off - agent: product - prompt: > - Review release deliverables against requirements and provide final - OK/NOK sign-off. +artifacts: + dir: releases + input: + - "**/*.md" + output: + - path: "*.md" + notes: includes release notes and sign-off record target: vscode user-invocable: true diff --git a/src/vstack/_templates/agents/release/template.md b/src/vstack/_templates/agents/release/template.md index 75d8ab1..d644644 100644 --- a/src/vstack/_templates/agents/release/template.md +++ b/src/vstack/_templates/agents/release/template.md @@ -7,8 +7,8 @@ You are a **senior platform and release engineer** acting as the **release role* ## responsibilities - Own release gating, artifact checks, and PR creation. -- Collect explicit sign-offs from tester and product. -- Produce `docs/releases/{date}.md`, update `CHANGELOG.md`, and open the release PR. +- Collect explicit sign-off reviews from upstream role perspectives (typically tester, architect, designer, and product). +- Produce the release document, update the changelog, and open the release PR. ## scope and boundaries @@ -25,9 +25,9 @@ You are a **senior platform and release engineer** acting as the **release role* ## working principles - Evidence-first release decisions. -- Explicit sign-offs from tester and product. +- Explicit cross-role sign-off reviews. - Deterministic, auditable release documentation. -- Both tester and product must be OK before PR creation. +- Required sign-off perspectives must be explicitly recorded before PR creation. - If any blocker exists, stop and route to owning role. - Prefer clear release notes over minimal notes. @@ -51,29 +51,28 @@ 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. -1. **Ready for PR creation** — tester and product both return explicit OK. +1. **Ready for PR creation** — required sign-off perspectives return explicit OK. -Handoffs you own: - -- To tester/product: explicit sign-off request with current artifact set and scope. -- Back to owning role: NOK reason, blocker owner, and required next action. -- To normal review flow: release PR with dated release notes and changelog updates. +Release does not expose cross-role handoff buttons for escalation paths. +For non-happy paths (`NOK`, blockers, missing artifacts), report blocker details +and wait for explicit user routing decisions. ## how you work -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. Baseline artifacts to check: the requirements doc, architecture overview, design overview, test report, security report, and changelog. Use your input artifacts (see `## artifacts you use`) to locate them. +1. Validate required-for-scope artifacts: require the performance baseline only when performance validation is in scope; require observability evidence in the test report (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. Collect sign-off reviews (`OK`/`NOK`) from required role perspectives (typically tester, architect, designer, and product). +1. Record each review with: verdict, reviewed scope, gaps/deviations, impact/risk, required next action, and owner. +1. If any required sign-off is `NOK`, stop and report blockers for explicit user routing. +1. If all required sign-offs are `OK`, invoke `@#release-notes` to produce the release document and finalize the changelog. 1. Invoke `@#pr` to push and open the PR with release notes as the body. ## success criteria +- Output artifacts are produced, accurate, and up to date (see output artifacts). - Required-for-scope artifacts are present and current before sign-off. -- Tester and product sign-offs are explicit and recorded. +- Required sign-off reviews are explicit and recorded with verdict and rationale. - Release notes and changelog accurately reflect shipped scope. ## failure and escalation rules @@ -82,14 +81,14 @@ Handoffs you own: - Any NOK sign-off: stop and hand back with rationale. - Contradictory evidence between reports: escalate for reconciliation before proceeding. -## artifacts you own +## artifacts you use + +{{AGENT_ARTIFACTS_INPUT}} + +{{AGENT_ARTIFACTS_OUTPUT}} -| Artifact | Role | -| -------------------------------- | ------- | -| `docs/releases/{date}.md` | creator | -| `CHANGELOG.md` updates | creator | -| release PR | creator | -| sign-off record (tester/product) | creator | +Agents do not write to artifacts owned by other roles. If you discover something +that requires changes to upstream artifacts, flag it and trigger a reverse handoff. ## completion checklist @@ -100,7 +99,7 @@ Handoffs you own: ## skills you use - `@#concise` — runtime response-style mode (`normal|compact|ultra|status`) -- `@#release-notes` — write `docs/releases/{date}.md` and update `CHANGELOG.md` +- `@#release-notes` — produce the release document and update the changelog - `@#conventional-commit` — produce compliant Conventional Commit messages before PR - `@#pr` — commit, push, and open pull request - `@#gh-release` — create or update GitHub Release with `gh` CLI diff --git a/src/vstack/_templates/agents/tester/artifacts/performance-baseline.md b/src/vstack/_templates/agents/tester/artifacts/performance-baseline.md new file mode 100644 index 0000000..9f6631d --- /dev/null +++ b/src/vstack/_templates/agents/tester/artifacts/performance-baseline.md @@ -0,0 +1,25 @@ +# Performance Baseline + +<!-- Update after each performance benchmark. Deviate from this structure as needed. --> + +> Maintained by: **tester** role\ +> Last updated: YYYY-MM-DD\ +> Scope: {what was benchmarked} + +## baseline + +| Operation / Endpoint | P50 | P95 | P99 | Threshold | Status | +| -------------------- | --- | --- | --- | --------- | ----------- | +| | | | | | PASS / FAIL | + +## methodology + +<!-- How was this measured? Tool, environment, sample size, warmup runs. --> + +## regressions + +<!-- Any operations that exceed the threshold or regressed since last baseline. --> + +## notes + +<!-- Advisory items, follow-up actions. --> diff --git a/src/vstack/_templates/agents/tester/artifacts/security-report.md b/src/vstack/_templates/agents/tester/artifacts/security-report.md new file mode 100644 index 0000000..5394356 --- /dev/null +++ b/src/vstack/_templates/agents/tester/artifacts/security-report.md @@ -0,0 +1,26 @@ +# Security Report + +<!-- Update after each security scan. Deviate from this structure as needed. --> + +> Maintained by: **tester** role\ +> Last updated: YYYY-MM-DD\ +> Scope: {what was scanned} — method: {static analysis | OWASP | STRIDE | ...} + +## verdict + +| Category | Findings | Blocking | +| ----------------- | ---------------------------- | -------- | +| Static analysis | {N} LOW / {N} MED / {N} HIGH | Yes / No | +| Dependency CVEs | {N} | Yes / No | +| Secrets in source | None | — | +| Injection risk | None identified | — | + +<!-- Ship readiness: PASS / PASS with notes / FAIL --> + +## findings + +<!-- One subsection per finding. Include ID, severity, file/line, description, resolution. --> + +## notes + +<!-- Advisory items, deferred findings, follow-up actions. --> diff --git a/src/vstack/_templates/agents/tester/artifacts/test-report.md b/src/vstack/_templates/agents/tester/artifacts/test-report.md new file mode 100644 index 0000000..7564cea --- /dev/null +++ b/src/vstack/_templates/agents/tester/artifacts/test-report.md @@ -0,0 +1,32 @@ +# Test Report + +<!-- Update after each test run. Deviate from this structure as needed. --> + +> Maintained by: **tester** role\ +> Last updated: YYYY-MM-DD\ +> Scope: {what was tested} + +## verdict + +| Dimension | Result | +| ------------- | --------------------------------- | +| Functional | PASS / FAIL — {N}/{N} tests green | +| Lint / Style | PASS / FAIL | +| Type checking | PASS / FAIL | +| Coverage | PASS / FAIL — {N}% | +| Security | See security-report.md | +| Performance | See performance-baseline.md | + +<!-- Ship readiness: READY / NOT READY --> + +## test execution + +<!-- Platform, Python version, runner, command, duration. --> + +## coverage summary + +<!-- Total coverage, missed statements, notable gaps. --> + +## findings + +<!-- Notable failures, flaky tests, or follow-up items. --> diff --git a/src/vstack/_templates/agents/tester/config.yaml b/src/vstack/_templates/agents/tester/config.yaml index 6d08f7b..433853a 100644 --- a/src/vstack/_templates/agents/tester/config.yaml +++ b/src/vstack/_templates/agents/tester/config.yaml @@ -1,11 +1,9 @@ name: tester -version: 20260502020 +version: 20260503026 description: > Senior QA, security, and reliability engineer. Runs functional, security, - and performance tests. Produces docs/test-report.md, docs/security-report.md, - and docs/performance-baseline.md based on docs/architecture/architecture.md - and requirements. Baseline-first on branch, optional docs/delta/{id} notes - for complex efforts only. + and performance tests. Produces verification reports based on the approved + architecture and requirements. Baseline-first on branch. argument-hint: "[verify changes | write tests | security review | performance review | smoke test service]" tools: - read @@ -21,16 +19,20 @@ model: - GPT-5.3-Codex (copilot) agents: ["*"] handoffs: - - label: Send fixes to engineer - agent: engineer - prompt: > - Resolve blocking findings from tester reports and update tests where - needed. - - label: Continue to release + - label: "Go to next stage: Release readiness" agent: release prompt: > - If findings are resolved, run release readiness checks and prepare - release artifacts. + Verification outputs are approved. Assess the current state and prepare + the release as needed. Create and/or update the relevant artifacts if needed, + as well as any sign-offs. +artifacts: + dir: reports + input: + - architecture/**/*.md + - design/**/*.md + output: + - "**/*.md" + - ./tests/**/* target: vscode user-invocable: true diff --git a/src/vstack/_templates/agents/tester/template.md b/src/vstack/_templates/agents/tester/template.md index 0358055..6be8546 100644 --- a/src/vstack/_templates/agents/tester/template.md +++ b/src/vstack/_templates/agents/tester/template.md @@ -8,7 +8,7 @@ You are a **senior QA, security, and reliability engineer** acting as the **test - Own verification evidence and release-readiness findings. - Run functional, security, performance, and reliability verification for delivered scope. -- Produce `docs/test-report.md` and `docs/security-report.md`; add `docs/performance-baseline.md` when performance validation is in scope. +- Produce output reports (see output artifacts); include the performance baseline when performance validation is in scope. - Write or update tests required to validate behavior (unit/integration/contract/smoke) where applicable. ## scope and boundaries @@ -56,17 +56,31 @@ Signal readiness before release proceeds: Handoffs you own: -- To engineer: reproducible defects with severity, impact, and recommended fix direction. -- To product/release: explicit go/no-go verdict with residual risk summary. +- Happy path only: one forward continuation to release readiness after user approval. +- For non-happy paths (`NOK`, blockers, missing artifacts), do not use handoff buttons; provide blocker details and let the user choose the recovery path. + +## assess current state + +Before running any checks, scan your configured input artifacts to determine +what work is needed: + +1. Read your input artifacts. +1. Identify artifacts that require action: + - Implementation changes since the last test report. + - New components or contracts not yet covered in the test report. + - Security or performance findings that are unresolved. + - Reports that are stale relative to the current architecture or design. +1. If all reports are current and no new verification is required, say so + explicitly and offer to hand off to the next stage. ## how you work -1. Read `docs/product/requirements.md`, `docs/architecture/architecture.md`, and relevant design/implementation context. +1. Assess current state (see above) before running any checks. 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. Write your baseline reports (see output artifacts); include the performance baseline when performance validation is in scope. Include observability evidence in the test report unless a dedicated observability report is used. 1. Publish verdict and hand off blockers or release-readiness status. ## success criteria @@ -81,14 +95,14 @@ Handoffs you own: - Security-critical issue found: escalate immediately and block release. - Missing or stale required-for-scope artifacts: stop and report owners. -## artifacts you own +## artifacts you use + +{{AGENT_ARTIFACTS_INPUT}} + +{{AGENT_ARTIFACTS_OUTPUT}} -| 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 | +Agents do not write to artifacts owned by other roles. If you discover something +that requires changes to upstream artifacts, flag it and trigger a reverse handoff. ## completion checklist diff --git a/src/vstack/_templates/project/.vstack/config.yaml b/src/vstack/_templates/project/.vstack/config.yaml new file mode 100644 index 0000000..840037a --- /dev/null +++ b/src/vstack/_templates/project/.vstack/config.yaml @@ -0,0 +1,28 @@ +# vstack project configuration +# +# vstack install — first-run setup. Seeds this file if missing (never overwrites). +# Then runs init to generate .github/ artifacts from templates. +# Use once per project, or when onboarding a new machine. +# +# vstack init — idempotent regeneration. Reads this file on every run. +# Safe to re-run in CI after pip install --upgrade vstack. +# +# This file is committed to git. It expresses stable project preferences — +# editing it is a deliberate choice that affects all future vstack init runs. + +# Selective install exclusions — remove or comment out to install everything. +# +# exclude: +# skills: +# - terraform +# - terragrunt +# - helm +# - k8s +# instructions: all +# prompts: all + +# Root directory for generated agent artifact paths. +# Default: docs +# +# artifacts: +# root: docs diff --git a/src/vstack/_templates/skills/adr/template.md b/src/vstack/_templates/skills/adr/template.md index d04e6d7..d3da9cf 100644 --- a/src/vstack/_templates/skills/adr/template.md +++ b/src/vstack/_templates/skills/adr/template.md @@ -18,7 +18,7 @@ Read existing ADRs and architecture docs: ```bash ls docs/architecture/adr/ 2>/dev/null | sort | head -20 || true -cat docs/architecture/architecture.md 2>/dev/null | head -40 || true +cat docs/architecture/overview.md 2>/dev/null | head -40 || true # Find highest existing ADR number ls docs/architecture/adr/*.md 2>/dev/null | grep -oE '[0-9]+' | sort -n | tail -1 || echo "0" ``` diff --git a/src/vstack/_templates/skills/architecture/template.md b/src/vstack/_templates/skills/architecture/template.md index f4500f0..7002944 100644 --- a/src/vstack/_templates/skills/architecture/template.md +++ b/src/vstack/_templates/skills/architecture/template.md @@ -15,11 +15,10 @@ recommendations, and produce a final verdict. ## Deliverable and artifact policy -- Primary deliverable: `docs/architecture/architecture.md` +- Primary deliverable: `docs/architecture/overview.md` - Additional deliverables when needed: `docs/architecture/adr/NNN-*.md` for significant structural decisions. -- Baseline-first default: write final architecture decisions directly to `docs/architecture/architecture.md` on the feature branch. -- Optional WIP area for complex/uncertain efforts: `docs/delta/{id}/ARCHITECTURE_DELTA.md` -- Before merge: consolidate any required content from optional delta notes back into baseline architecture docs and keep ADRs in `docs/architecture/adr/`. +- Baseline-first default: write final architecture decisions directly to `docs/architecture/overview.md` on the feature branch. +- Before merge: confirm the architecture overview reflects final decisions and keep ADRs in `docs/architecture/adr/`. ## Review philosophy @@ -208,4 +207,4 @@ For each significant structural decision made during this review (technology cho - Write an ADR via `@#adr`. - Cross-reference related ADRs. -- Update `docs/architecture/architecture.md` to reflect the final decisions. +- Update `docs/architecture/overview.md` to reflect the final decisions. diff --git a/src/vstack/_templates/skills/cicd/template.md b/src/vstack/_templates/skills/cicd/template.md index 733ff36..2fb4504 100644 --- a/src/vstack/_templates/skills/cicd/template.md +++ b/src/vstack/_templates/skills/cicd/template.md @@ -153,7 +153,7 @@ Adapt the deploy trigger to match the target platform (Fly.io, Render, Railway, ## Step 5: Branch protection (document, don't automate) -Record in `docs/architecture/architecture.md` or a README section: +Record in `docs/architecture/overview.md` or a README section: ```text Branch protection rules for `main`: diff --git a/src/vstack/_templates/skills/design/template.md b/src/vstack/_templates/skills/design/template.md index 8c51c93..dcca342 100644 --- a/src/vstack/_templates/skills/design/template.md +++ b/src/vstack/_templates/skills/design/template.md @@ -14,11 +14,10 @@ becomes the source of truth for implementation. ## Deliverable and artifact policy -- Primary deliverable: `docs/design/design.md` +- Primary deliverable: `docs/design/overview.md` - Additional deliverable when user-facing scope: `docs/design/ux.md` - Baseline-first default: write final design decisions directly to `docs/design/*.md` on the feature branch. -- Optional WIP area for complex/uncertain efforts: `docs/delta/{id}/DESIGN_DELTA.md` -- Before merge: consolidate any delta draft into baseline design docs. +- Before merge: confirm design docs on the feature branch are complete before merge. ## Step 0: Understand the Domain @@ -183,7 +182,7 @@ Authorization model: RBAC with per-resource checks ## Step 7: Produce the Design Document -Output a complete design document to `docs/design/design.md` or `openapi.yaml`: +Output a complete design document to `docs/design/overview.md` or `openapi.yaml`: ```markdown # API Design — [Service Name] diff --git a/src/vstack/_templates/skills/docs/template.md b/src/vstack/_templates/skills/docs/template.md index 2ae2e6b..a2dd769 100644 --- a/src/vstack/_templates/skills/docs/template.md +++ b/src/vstack/_templates/skills/docs/template.md @@ -18,8 +18,7 @@ change source code. - Primary deliverables: updated baseline documentation artifacts (for example `README.md`, API docs, migration guides) - Baseline-first default: write final documentation updates directly to baseline docs on the feature branch. -- Optional WIP area for complex/uncertain efforts: `docs/delta/{id}/DOCS_DELTA.md` -- Before merge: consolidate any documentation delta notes into baseline docs. +- Before merge: confirm documentation updates are complete and consistent before merge. ## Step 0: Scope the Release diff --git a/src/vstack/_templates/skills/gh-issues/template.md b/src/vstack/_templates/skills/gh-issues/template.md index 14ebe31..44ec393 100644 --- a/src/vstack/_templates/skills/gh-issues/template.md +++ b/src/vstack/_templates/skills/gh-issues/template.md @@ -2,7 +2,10 @@ # gh-issues — GitHub Issue Management -Create, update, and manage GitHub issues using the `gh` CLI. +Create, update, and manage GitHub issues. Prefer the GitHub MCP server tools when +available in the current agent session (`create_issue`, `update_issue`, `list_issues`, +etc.). Fall back to `gh` CLI when MCP tools are unavailable or when a required +operation is not exposed by the MCP server. ## Out of scope diff --git a/src/vstack/_templates/skills/incident/config.yaml b/src/vstack/_templates/skills/incident/config.yaml index b59ccbf..bc3af18 100644 --- a/src/vstack/_templates/skills/incident/config.yaml +++ b/src/vstack/_templates/skills/incident/config.yaml @@ -1,12 +1,12 @@ name: incident -version: 20260421017 +version: 20260503002 description: | - Incident analysis and post-mortem writing. Guides a structured investigation - from timeline reconstruction through root cause identification to a blameless - post-mortem document with action items. Use when asked to "write a post-mortem", - "incident review", "root cause analysis for this outage", "what went wrong?", - or "blameless post-mortem". Produces a docs/postmortems/{date}-{slug}.md. -argument-hint: '[incident or outage to analyse and document]' + Incident analysis and coordination. Guides timeline reconstruction, contributing + factor identification, and action item definition. Delegates technical root cause + analysis to `rca` and stakeholder documentation to `postmortem`. Use when asked + to "incident review", "analyse this outage", "what went wrong?", or to coordinate + a full incident response retrospective. +argument-hint: '[incident or outage to analyse]' license: MIT compatibility: Requires a skills-compatible agent with repository file access and terminal command execution when needed. diff --git a/src/vstack/_templates/skills/incident/template.md b/src/vstack/_templates/skills/incident/template.md index 20f17c5..c007896 100644 --- a/src/vstack/_templates/skills/incident/template.md +++ b/src/vstack/_templates/skills/incident/template.md @@ -1,12 +1,15 @@ {{SKILL_CONTEXT}} -# incident — Incident Analysis & Post-Mortem +# incident — Incident Analysis & Coordination -Guide a structured incident investigation and produce a blameless post-mortem -document. The goal is learning and prevention — not blame. +Guide a structured incident retrospective: reconstruct the timeline, identify +contributing factors, define action items, then delegate to `rca` and `postmortem` +for the written artifacts. The goal is learning and prevention — not blame. ## Out of scope +- Technical root cause write-up (use `rca`) +- Post-mortem document (use `postmortem`) - Live incident response / on-call triage (this skill is for retrospective analysis) - Root-cause debugging of code bugs (use `debug`) - Security audit of vulnerabilities (use `security`) @@ -185,78 +188,17 @@ Process (improve how we handle incidents): [ ] [specific action] — owner: [name/team] — due: [date/sprint] ``` -## Step 6: Produce the Post-Mortem Document +## Step 6: Produce Artifacts -Write the post-mortem to `docs/postmortems/YYYY-MM-DD-<slug>.md`: +With the analysis complete, delegate writing to the dedicated skills: -```markdown -# Post-Mortem: [Short Title] +1. Invoke `@#rca` to produce the technical root cause document. + The engineer role owns this artifact. Pass: incident ID, issue reference (if known), + and the confirmed output path. -**Date:** YYYY-MM-DD -**Severity:** [P0 | P1 | P2] -**Duration:** N hours N minutes -**Status:** [Draft | In Review | Closed] -**Author(s):** [names] - ---- - -## Summary - -[2–3 sentences: what happened, what was the impact, how was it resolved. -Written for a non-technical audience.] - -## Impact - -| Dimension | Details | -|---|---| -| Duration | N hours N minutes (HH:MM–HH:MM UTC) | -| Users affected | [N users / N% of traffic] | -| Error rate | [N%] | -| SLA breach | [yes / no] | -| Data loss | [none / description] | - -## Timeline - -| Time (UTC) | Event | -|---|---| -| HH:MM | [event] | -| HH:MM | [event] | -| ... | ... | - -## Root Cause - -[One paragraph. Specific, systemic, blameless.] - -## Contributing Factors - -- [factor 1] -- [factor 2] -- [factor 3] - -## What Went Well - -- [thing 1 — e.g. alert fired within 2 minutes] -- [thing 2 — e.g. rollback completed in 4 minutes] -- [thing 3] - -## What Went Poorly - -- [thing 1 — e.g. no runbook for this failure mode] -- [thing 2] - -## Action Items - -| # | Action | Category | Owner | Due | -|---|---|---|---|---| -| 1 | [action] | Prevention | [owner] | [date] | -| 2 | [action] | Detection | [owner] | [date] | -| 3 | [action] | Response | [owner] | [date] | - -## Lessons Learned - -[2–4 sentences summarizing the key takeaways for the team and organization. -What does this incident teach us about our system, processes, or culture?] -``` +1. Invoke `@#postmortem` to produce the stakeholder-facing post-mortem. + Pass: incident ID, RCA path, and the confirmed output path + (default: `docs/postmortems/{id}-{slug}-postmortem.md`). ## Output @@ -272,6 +214,5 @@ Root cause: [one-line summary] Contributing factors: [N identified] Action items: [N total — N prevention, N detection, N response] -Post-mortem written: docs/postmortems/YYYY-MM-DD-<slug>.md -Status: [Draft — ready for team review] +Next: invoke @#rca and @#postmortem to produce written artifacts. ``` diff --git a/src/vstack/_templates/skills/inspect/template.md b/src/vstack/_templates/skills/inspect/template.md index e3f6939..1e638f6 100644 --- a/src/vstack/_templates/skills/inspect/template.md +++ b/src/vstack/_templates/skills/inspect/template.md @@ -18,10 +18,9 @@ Use `verify` when a fix loop is required. ## Deliverable and artifact policy -- Primary deliverable: `docs/test-report.md` -- Baseline-first default: write final findings directly to `docs/test-report.md` on the feature branch. -- Optional WIP area for complex/uncertain efforts: `docs/delta/{id}/TESTING_DELTA.md` -- Before merge: consolidate any blocking findings and final verdict into baseline reports. +- Primary deliverable: `docs/reports/test-report.md` +- Baseline-first default: write final findings directly to `docs/reports/test-report.md` on the feature branch. +- Before merge: confirm blocking findings and final verdict are written to baseline reports. ## Step 0: Scope diff --git a/src/vstack/_templates/skills/onboard/template.md b/src/vstack/_templates/skills/onboard/template.md index 02d89e2..3d68aad 100644 --- a/src/vstack/_templates/skills/onboard/template.md +++ b/src/vstack/_templates/skills/onboard/template.md @@ -210,7 +210,7 @@ cp .env.example .env ## Architecture Overview \[2–4 sentences describing the main components and how they interact. -Link to docs/architecture/architecture.md for details.\] +Link to docs/architecture/overview.md for details.\] ## Good First Issues diff --git a/src/vstack/_templates/skills/performance/template.md b/src/vstack/_templates/skills/performance/template.md index 7610c8f..8239c44 100644 --- a/src/vstack/_templates/skills/performance/template.md +++ b/src/vstack/_templates/skills/performance/template.md @@ -16,9 +16,8 @@ optimizations. Measure first; never optimize without evidence. ## Deliverable and artifact policy -- Primary deliverable: `docs/performance-baseline.md` -- Baseline-first default: write benchmark outcomes and regression verdicts directly to `docs/performance-baseline.md` on the feature branch. -- Optional WIP area for complex/uncertain efforts: `docs/delta/{id}/PERFORMANCE_DELTA.md` +- Primary deliverable: `docs/reports/performance-baseline.md` +- Baseline-first default: write benchmark outcomes and regression verdicts directly to `docs/reports/performance-baseline.md` on the feature branch. - Before merge: consolidate thresholds, measurements, and recommendations into the baseline performance report. ## Setup diff --git a/src/vstack/_templates/skills/postmortem/config.yaml b/src/vstack/_templates/skills/postmortem/config.yaml new file mode 100644 index 0000000..5d89989 --- /dev/null +++ b/src/vstack/_templates/skills/postmortem/config.yaml @@ -0,0 +1,16 @@ +name: postmortem +version: 20260503001 +description: | + Blameless post-mortem writing for incidents. Produces a stakeholder-facing + post-mortem document linked to the triggering issue and RCA. Use when asked + to "write a post-mortem", "blameless post-mortem", or "incident post-mortem". + Called by the incident skill; the RCA should be available before invoking this. +argument-hint: '[incident to write a post-mortem for]' + +license: MIT +compatibility: Requires a skills-compatible agent with repository file access and terminal command execution when needed. +metadata: + owner: vstack + maturity: stable +user-invocable: true +disable-model-invocation: false diff --git a/src/vstack/_templates/skills/postmortem/template.md b/src/vstack/_templates/skills/postmortem/template.md new file mode 100644 index 0000000..9d2ced4 --- /dev/null +++ b/src/vstack/_templates/skills/postmortem/template.md @@ -0,0 +1,151 @@ +{{SKILL_CONTEXT}} + +# postmortem — Blameless Post-Mortem + +Produce a blameless, stakeholder-facing post-mortem document for a resolved +incident. The post-mortem summarises impact, timeline, root cause, and action +items. It is distinct from the RCA: the RCA is the technical analysis; the +post-mortem is the organisational record. + +## Out of scope + +- Technical root cause investigation (use `rca` first) +- Live incident triage (use `incident`) +- Code-level debugging (use `debug`) + +**Golden rule: Incidents are system failures. Every statement must be blameless +— frame findings as system improvement opportunities, never as individual fault.** + +## Step 0: Gather Context + +Before writing, confirm inputs are available: + +> **Required:** +> +> - Incident ID and title +> - Severity and duration +> - RCA document path (or summary if RCA is not yet written) +> - Linked issue file path (optional) +> +> **Output path:** Where should the post-mortem be written? +> +> Default: `docs/postmortems/{id}-{slug}-postmortem.md` +> Suggested name: `{id}-{slug}-postmortem.md` (e.g. `001-login-timeout-postmortem.md`) +> +> **Options:** A) Use default | B) Specify a different path + +## Step 1: Summary + +One paragraph for a non-technical audience — what happened, what was the +impact, and how was it resolved: + +```text +[2–3 sentences. No jargon. Written for stakeholders and future team members.] +``` + +## Step 2: Timeline + +Build a precise chronological timeline: + +```text +All times UTC: + +HH:MM — [event] — [who / what system] +HH:MM — [alert fired / page sent] +HH:MM — [response action] +HH:MM — [mitigation deployed] +HH:MM — [full resolution] + +Key markers: + Impact start: HH:MM + Detection: HH:MM (+N min) + Response start: HH:MM (+N min) + Resolution: HH:MM (+N min) + Total duration: N hours N minutes +``` + +## Step 3: Impact + +Quantify impact precisely: + +```text +Users affected: [N users | N% of traffic | all users] +Error rate: [N% of requests] +Latency: [p99 increased from Nms to Nms] +Data loss: [none | describe scope] +SLA breach: [yes — N minutes over limit | no] +Customer comms: [status page | direct notification | none] +``` + +## Step 4: Root Cause + +Restate the root cause from the RCA in one paragraph. Link to the RCA for +the full technical analysis: + +```text +Root cause: [one clear, systemic, blameless statement] + +See [RCA document]({id}-{slug}-rca.md) for the full technical analysis. +``` + +## Step 5: Action Items + +Carry over action items from the RCA, categorised and owned: + +| Item | Category | Owner | Due | Status | +| ---- | ---------- | ----- | --- | ------ | +| | Prevention | | | | +| | Detection | | | | +| | Response | | | | + +## Step 6: Write the Post-Mortem Document + +Write to the confirmed output path: + +```markdown +# {id}: {title} — Post-Mortem + +<!-- Suggested name: {id}-{slug}-postmortem.md --> + +> **date:** YYYY-MM-DD +> **severity:** P{1–4} +> **status:** draft | review | closed +> **issue:** [{id}]({id}-{slug}.md) +> **rca:** [{id}-{slug}-rca.md]({id}-{slug}-rca.md) + +## summary + +## timeline + +| Time (UTC) | Event | +| --- | --- | +| HH:MM | | + +## impact + +## root cause + +## resolution + +## action items + +| Item | Owner | Due | Status | +| --- | --- | --- | --- | + +## lessons learned +``` + +## Output + +```text +Post-Mortem Complete +════════════════════ + +Incident: [ID / title] +Severity: [P1–P4] +Duration: [N hours N minutes] +Action items: [N total] + +Written to: [path/to/{id}-{slug}-postmortem.md] +Status: Draft — ready for team review +``` diff --git a/src/vstack/_templates/skills/rca/config.yaml b/src/vstack/_templates/skills/rca/config.yaml new file mode 100644 index 0000000..0111cd4 --- /dev/null +++ b/src/vstack/_templates/skills/rca/config.yaml @@ -0,0 +1,17 @@ +name: rca +version: 20260503001 +description: | + Root cause analysis for incidents and bugs. Guides a systematic technical + investigation and produces an RCA document linked to the triggering issue. + Use when asked to "write an RCA", "root cause this incident", or "document + what went wrong technically". Called by the incident skill; also invoked + directly by the engineer role. +argument-hint: '[incident or issue to analyse]' + +license: MIT +compatibility: Requires a skills-compatible agent with repository file access and terminal command execution when needed. +metadata: + owner: vstack + maturity: stable +user-invocable: true +disable-model-invocation: false diff --git a/src/vstack/_templates/skills/rca/template.md b/src/vstack/_templates/skills/rca/template.md new file mode 100644 index 0000000..25b1c16 --- /dev/null +++ b/src/vstack/_templates/skills/rca/template.md @@ -0,0 +1,174 @@ +{{SKILL_CONTEXT}} + +# rca — Root Cause Analysis + +Guide a systematic technical investigation and produce an RCA document linked +to the triggering issue. The goal is a specific, evidence-backed root cause — +not the proximate trigger. + +## Out of scope + +- Stakeholder communication and blameless post-mortems (use `postmortem`) +- Live incident triage (use `incident`) +- General code debugging (use `debug`) +- Security vulnerability analysis (use `security`) + +**Golden rule: The root cause is the deepest systemic condition that, if fixed, +prevents this class of incident from recurring. It is never "human error".** + +## Step 0: Gather Context + +Before analysis, establish the facts: + +> **Collect:** +> +> - Issue or incident ID and title +> - Severity (P1–P4 or SEV1–SEV3) +> - When did it start and end? (UTC timestamps) +> - What was the observable symptom? +> - What was done to resolve it? +> - Is there a linked issue file? (e.g. `issues/001-login-timeout.md`) + +```bash +# Review recent changes in the window +git log --oneline --since="YYYY-MM-DD" --until="YYYY-MM-DD" 2>/dev/null | head -30 + +# Find relevant config or infra changes +git log --oneline --since="YYYY-MM-DD" -- '*.yaml' '*.yml' '*.toml' 2>/dev/null | head -20 +``` + +Confirm output location before writing: + +> **Output path:** Where should the RCA be written? +> +> Default: alongside the issue file, or `docs/` if no issue path is known. +> Suggested name: `{id}-{slug}-rca.md` (e.g. `001-login-timeout-rca.md`) +> +> **Options:** A) Use default | B) Specify a different path + +## Step 1: What Happened + +Describe the incident factually and concisely: + +```text +What happened: + [Factual description — what system, what failed, what was the user impact] + +Severity: [P1 | P2 | P3 | P4] +Start: [YYYY-MM-DD HH:MM UTC] +End: [YYYY-MM-DD HH:MM UTC] +Duration: [N hours N minutes] +``` + +## Step 2: Root Cause — 5 Whys + +Use the 5-Whys technique to reach the systemic condition: + +```text +Why did [impact] happen? + Because [immediate cause]. + +Why did [immediate cause] happen? + Because [contributing factor]. + +Why did [contributing factor] exist? + Because [deeper cause]. + +Why did [deeper cause] exist? + Because [systemic gap]. + +Root cause: + [One specific, systemic statement] + +Evidence: + [What confirms this conclusion] +``` + +## Step 3: Contributing Factors + +List all conditions that made this possible — not just the trigger: + +| Category | Factor | +| ------------- | ------ | +| Code / logic | | +| Configuration | | +| Deployment | | +| Monitoring | | +| Process | | +| External | | + +## Step 4: Detection + +How was this discovered, and could it have been caught faster? + +```text +Detected by: [alert | user report | manual check] +Time to detect: [N minutes after impact start] +Detection gap: [why not faster — threshold too high | missing alert | other] +``` + +## Step 5: Resolution + +What was done to restore service? + +```text +Resolution: [what was done] +Time to resolve: [N minutes after detection] +``` + +## Step 6: Action Items + +Action items must be specific, owned, and time-bound: + +| Item | Category | Owner | Due | +| ---- | ---------- | ----- | --- | +| | Prevention | | | +| | Detection | | | +| | Response | | | + +## Step 7: Write the RCA Document + +Write to the confirmed output path: + +```markdown +# {id}: {title} — Root Cause Analysis + +<!-- Suggested name: {id}-{slug}-rca.md --> + +> **date:** YYYY-MM-DD +> **severity:** P{1–4} +> **status:** draft | in-progress | resolved +> **issue:** [{id}]({id}-{slug}.md) + +## what happened + +## root cause + +## contributing factors + +## detection + +## resolution + +## action items + +| Item | Owner | Due | +| --- | --- | --- | + +## lessons learned +``` + +## Output + +```text +RCA Complete +════════════ + +Incident: [ID / title] +Severity: [P1–P4] +Root cause: [one-line summary] +Action items:[N total] + +Written to: [path/to/{id}-{slug}-rca.md] +Status: Draft — ready for review +``` diff --git a/src/vstack/_templates/skills/release-notes/template.md b/src/vstack/_templates/skills/release-notes/template.md index c930703..597966d 100644 --- a/src/vstack/_templates/skills/release-notes/template.md +++ b/src/vstack/_templates/skills/release-notes/template.md @@ -24,12 +24,20 @@ the release notes. This skill describes the procedure, not the file paths. Verify that the evidence the invoking agent has designated as required is present and not empty. Report any missing items and stop if blockers exist. -Typical evidence to check (agent-defined): +Check each artifact the invoking agent listed as required. For each file: +```bash +[ -f "<path>" ] && echo "✓ <path>" || echo "✗ MISSING: <path>" +``` + +Typically this includes: + +- Requirements or scope artifact +- Architecture or design overview - Test results or verification report - Security findings or sign-off - Change summary (git log, diff stat, or agent-provided summary) -- Acceptance criteria from requirements +- `CHANGELOG.md` If any required evidence is missing: **STOP and report to the invoking agent**. @@ -74,7 +82,10 @@ Use this structure: ## Evidence reviewed | evidence | status | |----------|--------| -| [evidence item] | ✓ / ✗ MISSING | +| [requirements artifact] | ✓ | +| [architecture artifact] | ✓ | +| [test report] | ✓ | +| [security report] | ✓ | ``` Rules: diff --git a/src/vstack/_templates/skills/requirements/template.md b/src/vstack/_templates/skills/requirements/template.md index fccc36c..bd8a1df 100644 --- a/src/vstack/_templates/skills/requirements/template.md +++ b/src/vstack/_templates/skills/requirements/template.md @@ -17,8 +17,7 @@ work from. - Primary deliverable: `docs/product/requirements.md` - Baseline-first default: write final requirements directly to `docs/product/requirements.md` on the feature branch. -- Optional WIP area for complex/uncertain intake: `docs/delta/{intake-id}/REQUIREMENTS_DELTA.md` -- Before merge: consolidate any required content from optional delta notes back into `docs/product/requirements.md`, then remove stale WIP notes. +- Before merge: confirm requirements are complete and consistent before merge. ## Step 0: Context diff --git a/src/vstack/_templates/skills/security/template.md b/src/vstack/_templates/skills/security/template.md index 8306e21..015bcf5 100644 --- a/src/vstack/_templates/skills/security/template.md +++ b/src/vstack/_templates/skills/security/template.md @@ -16,9 +16,8 @@ with severity, evidence, and specific remediation. ## Deliverable and artifact policy -- Primary deliverable: `docs/security-report.md` -- Baseline-first default: write final findings directly to `docs/security-report.md` on the feature branch. -- Optional WIP area for complex/uncertain efforts: `docs/delta/{id}/SECURITY_DELTA.md` +- Primary deliverable: `docs/reports/security-report.md` +- Baseline-first default: write final findings directly to `docs/reports/security-report.md` on the feature branch. - Before merge: consolidate severity-ranked findings and mitigations into baseline security reports. ## Audit Scope diff --git a/src/vstack/_templates/skills/threat-model/template.md b/src/vstack/_templates/skills/threat-model/template.md index 65e8db5..77bb6fa 100644 --- a/src/vstack/_templates/skills/threat-model/template.md +++ b/src/vstack/_templates/skills/threat-model/template.md @@ -22,10 +22,10 @@ This skill uses a practical framework selection model: ## Deliverable and artifact policy -- Primary deliverable: `docs/architecture/threat-model.md` -- Baseline-first default: write final threat model directly to `docs/architecture/threat-model.md` on the feature branch. -- Optional WIP area for complex/uncertain efforts: `docs/delta/{id}/THREAT_MODEL_DELTA.md` -- Before merge: consolidate delta notes into the baseline threat model. +- Primary deliverable: a threat model document owned by the `architect` role. +- The invoking agent determines the output path. Default (architect role): `docs/architecture/threat-model.md`. +- Baseline-first default: write the final threat model directly to the designated output path on the feature branch. +- Before merge: confirm the threat model on the feature branch is complete before merge. ## Framework selection guide @@ -57,7 +57,7 @@ Collect architecture evidence first: ```bash find docs -maxdepth 3 -type f \ - \( -name 'architecture.md' -o -name 'design.md' -o -name 'requirements.md' -o -name 'openapi*.yaml' -o -name '*.proto' \) \ + \( -name 'overview.md' -o -name 'requirements.md' -o -name 'openapi*.yaml' -o -name '*.proto' \) \ 2>/dev/null | sort ``` diff --git a/src/vstack/_templates/skills/verify/template.md b/src/vstack/_templates/skills/verify/template.md index 3c57192..18f18ed 100644 --- a/src/vstack/_templates/skills/verify/template.md +++ b/src/vstack/_templates/skills/verify/template.md @@ -23,10 +23,9 @@ Use `inspect` for read-only auditing. ## Deliverable and artifact policy -- Primary deliverable: `docs/test-report.md` -- Additional deliverables when applicable: `docs/security-report.md`, `docs/performance-baseline.md` +- Primary deliverable: `docs/reports/test-report.md` +- Additional deliverables when applicable: `docs/reports/security-report.md`, `docs/reports/performance-baseline.md` - Baseline-first default: write final verification outcomes directly to baseline reports on the feature branch. -- Optional WIP area for complex/uncertain efforts: `docs/delta/{id}/VERIFY_DELTA.md` - Before merge: consolidate final findings, severity, and ship-readiness verdict into baseline reports. ## Step 0: Route Mode diff --git a/src/vstack/_templates/skills/vision/template.md b/src/vstack/_templates/skills/vision/template.md index bbe2af8..b197c09 100644 --- a/src/vstack/_templates/skills/vision/template.md +++ b/src/vstack/_templates/skills/vision/template.md @@ -18,8 +18,7 @@ during this review — that comes after the plan is approved. - Primary deliverable: `docs/product/vision.md` - Baseline-first default: write approved vision decisions directly to `docs/product/vision.md` on the feature branch. -- Optional WIP area for complex/uncertain scope exploration: `docs/delta/{intake-id}/VISION_DELTA.md` -- Before merge: consolidate any required content from optional delta notes back into `docs/product/vision.md`, then remove stale WIP notes. +- Before merge: confirm the vision doc on the feature branch is complete and approved before merge. ## Review posture Envision the platform, not just the feature. Push scope UP. Ask "what would make this 10x better for 2x the effort?" Present each scope-expanding idea as a question. The user opts in or out. diff --git a/src/vstack/agents/generator.py b/src/vstack/agents/generator.py index dc119ac..f2b08d5 100644 --- a/src/vstack/agents/generator.py +++ b/src/vstack/agents/generator.py @@ -1,20 +1,194 @@ -"""AgentGenerator — thin wrapper around GenericArtifactGenerator for agents. +"""AgentGenerator — generator for agent artifacts with artifacts-section support. Import :class:`AgentGenerator` to get a generator pre-configured for the -``agents`` artifact type. All behaviour is inherited from -:class:`~vstack.artifacts.generator.GenericArtifactGenerator`. +``agents`` artifact type. The generator extends +:class:`~vstack.artifacts.generator.GenericArtifactGenerator` with +per-template placeholder injection that builds the ``## artifacts you use`` +subsections from each agent's ``config.yaml`` ``artifacts:`` block. + +Placeholder tokens injected per template: + +``{{AGENT_ARTIFACTS_INPUT}}`` + Markdown ``### input`` block (header + table) built from ``artifacts.input``, + or an empty string when no input entries are configured. + +``{{AGENT_ARTIFACTS_OUTPUT}}`` + Markdown ``### output`` block (header + table) built from ``artifacts.output``, + or an empty string when no output entries are configured. + +``{{AGENT_ARTIFACTS_INPUT_COMMENTS}}`` + Verbatim text from ``artifacts.input_comments``, or an empty string. + +``{{AGENT_ARTIFACTS_OUTPUT_COMMENTS}}`` + Verbatim text from ``artifacts.output_comments``, or an empty string. + +Path construction rules +----------------------- +Input items in ``artifacts.input`` are relative to :data:`~vstack.constants.ARTIFACTS_DOCS_ROOT` +(default ``"docs"``), so ``product/**/*.md`` renders as ``docs/product/**/*.md``. + +Output items in ``artifacts.output`` are interpreted as follows: + +- When ``artifacts.dir`` is set: items are relative to ``{root}/{dir}/``, e.g. + ``overview.md`` → ``docs/architecture/overview.md``. +- When an item (string or ``path`` key) starts with ``./``, the ``./`` prefix is + stripped and the remainder is used verbatim — allowing paths outside the docs + root (e.g. ``./src/**/*``, ``./tests/**/*``). +- When ``artifacts.dir`` is absent: all output items are used verbatim. """ from __future__ import annotations +from pathlib import Path + from vstack.agents.config import AGENT_TYPE from vstack.artifacts.generator import GenericArtifactGenerator -from vstack.constants import TEMPLATES_ROOT +from vstack.constants import ARTIFACTS_DOCS_ROOT, TEMPLATES_ROOT class AgentGenerator(GenericArtifactGenerator): """Generate agent artifacts using the built-in agent type configuration.""" - def __init__(self) -> None: - """Create an agent generator bound to the built-in template root.""" - super().__init__(AGENT_TYPE, TEMPLATES_ROOT) + def __init__( + self, + templates_root: Path | None = None, + *, + artifacts_root: str = ARTIFACTS_DOCS_ROOT, + ) -> None: + """Create an agent generator bound to *templates_root*. + + Args: + templates_root: Root directory containing the source templates. + When ``None``, the built-in package template root is used. + artifacts_root: Root directory for all agent artifacts. Defaults + to :data:`~vstack.constants.ARTIFACTS_DOCS_ROOT`. Override + via ``artifacts.root`` in ``.vstack/config.yaml`` to relocate + generated artifact paths (e.g. ``"documentation"`` instead of + ``"docs"``). + """ + super().__init__( + AGENT_TYPE, templates_root if templates_root is not None else TEMPLATES_ROOT + ) + self.artifacts_root = artifacts_root + + def template_partials(self, tmpl_dir: Path) -> dict[str, str]: + """Inject per-template artifact placeholder tokens. + + Returns a dict with four keys: + ``AGENT_ARTIFACTS_INPUT``, ``AGENT_ARTIFACTS_OUTPUT``, + ``AGENT_ARTIFACTS_INPUT_COMMENTS``, ``AGENT_ARTIFACTS_OUTPUT_COMMENTS``. + """ + from vstack.frontmatter import FrontmatterParser + + artifact_config = self.load_artifact_config(tmpl_dir) + artifacts = artifact_config.get("artifacts") or {} + + # The minimal YAML parser stores nested dicts as raw indented strings. + # Re-parse by stripping the 2-space indent that the raw-block mode preserves. + if isinstance(artifacts, str): + dedented = "\n".join( + line[2:] if line.startswith(" ") else line for line in artifacts.split("\n") + ) + artifacts = FrontmatterParser.parse_yaml(dedented) or {} + + if not isinstance(artifacts, dict): + artifacts = {} + + doc_root = self.artifacts_root + agent_dir: str = str(artifacts.get("dir", "")).strip() + + raw_inputs: list = artifacts.get("input", []) + if not isinstance(raw_inputs, list): + raw_inputs = [] + raw_outputs: list = artifacts.get("output", []) + if not isinstance(raw_outputs, list): + raw_outputs = [] + + input_entries = [ + {"path": f"{doc_root}/{item}", "notes": ""} + for item in raw_inputs + if isinstance(item, str) + ] + output_entries = self._resolve_output_entries(raw_outputs, doc_root, agent_dir) + + return { + "AGENT_ARTIFACTS_INPUT": self._build_section("input", input_entries), + "AGENT_ARTIFACTS_OUTPUT": self._build_section("output", output_entries), + "AGENT_ARTIFACTS_INPUT_COMMENTS": str(artifacts.get("input_comments", "") or ""), + "AGENT_ARTIFACTS_OUTPUT_COMMENTS": str(artifacts.get("output_comments", "") or ""), + } + + @staticmethod + def _resolve_output_entries( + raw_outputs: list, doc_root: str, agent_dir: str + ) -> list[dict[str, str]]: + """Resolve raw output config items to display-path dicts. + + :param raw_outputs: List of strings or dicts from ``artifacts.output``. + :param doc_root: Global artifacts root directory (e.g. ``"docs"``). + :param agent_dir: Subdirectory for this agent (e.g. ``"architecture"``). + When empty, output paths are used verbatim. + :returns: List of ``{"path": ..., "notes": ...}`` dicts. + """ + result: list[dict[str, str]] = [] + for item in raw_outputs: + if isinstance(item, str): + path, notes = item, "" + elif isinstance(item, dict): + path, notes = str(item.get("path", "")), str(item.get("notes", "")) + else: + continue + + if path.startswith("./"): + # Explicit project-root path; strip marker and use verbatim. + display = path[2:] + elif agent_dir: + display = f"{doc_root}/{agent_dir}/{path}" + else: + display = path + + result.append({"path": display, "notes": notes}) + return result + + @staticmethod + def _build_table(entries: list[dict[str, str]]) -> str: + """Build a Markdown table from normalised artifact entry dicts. + + Produces a single-column ``Artifact`` table when no entry has notes, + or a two-column ``Artifact | Notes`` table when any entry does. + All rows are padded to equal column widths. + """ + cells = [f"`{e['path']}`" for e in entries] + notes_cells = [e.get("notes", "") for e in entries] + has_notes = any(notes_cells) + + if has_notes: + art_w = max(len("Artifact"), *(len(c) for c in cells)) + notes_w = max(len("Notes"), *(len(n) for n in notes_cells)) + lines = [ + f"| {'Artifact':<{art_w}} | {'Notes':<{notes_w}} |", + f"| {'-' * art_w} | {'-' * notes_w} |", + *( + f"| {cell:<{art_w}} | {note:<{notes_w}} |" + for cell, note in zip(cells, notes_cells) + ), + ] + else: + col_w = max(len("Artifact"), *(len(c) for c in cells)) + lines = [ + f"| {'Artifact':<{col_w}} |", + f"| {'-' * col_w} |", + *(f"| {cell:<{col_w}} |" for cell in cells), + ] + + return "\n".join(lines) + + @staticmethod + def _build_section(heading: str, entries: list[dict[str, str]]) -> str: + """Build a ``### {heading}`` Markdown subsection with a table. + + Returns an empty string when *entries* is empty. + """ + if not entries: + return "" + return f"### {heading}\n\n{AgentGenerator._build_table(entries)}" diff --git a/src/vstack/artifacts/generator.py b/src/vstack/artifacts/generator.py index ce81ce0..8cbcf80 100644 --- a/src/vstack/artifacts/generator.py +++ b/src/vstack/artifacts/generator.py @@ -144,6 +144,17 @@ def load_artifact_config(self, tmpl_dir: Path) -> dict: # ── Rendering ───────────────────────────────────────────────────────────── + def template_partials(self, tmpl_dir: Path) -> dict[str, str]: + """Return per-template placeholder overrides merged with global partials. + + Subclasses may override this method to inject additional ``{{TOKEN}}`` + resolvers that depend on the specific template being rendered (for + example, resolvers built from the template's ``config.yaml``). + + Returns an empty dict by default. + """ + return {} + def render(self, tmpl_dir: Path) -> RenderedArtifact: """Render a single template directory to a :class:`~vstack.artifacts.models.RenderedArtifact`. @@ -156,8 +167,9 @@ def render(self, tmpl_dir: Path) -> RenderedArtifact: tmpl_file = tmpl_dir / self.config.template_filename content = tmpl_file.read_text(encoding="utf-8") - # Resolve {{PLACEHOLDER}} tokens via partials - resolved = self.resolve_placeholders(content, self.load_partials()) + # Resolve {{PLACEHOLDER}} tokens via partials (merged with per-template extras) + partials = {**self.load_partials(), **self.template_partials(tmpl_dir)} + resolved = self.resolve_placeholders(content, partials) # Split existing frontmatter from body parsed = FrontmatterParser.parse(resolved) diff --git a/src/vstack/cli/base.py b/src/vstack/cli/base.py index d877f4d..476d648 100644 --- a/src/vstack/cli/base.py +++ b/src/vstack/cli/base.py @@ -15,6 +15,7 @@ class CommandContext: args: Namespace install_dir: Path | None only: list[str] | None + excluded_names: dict[str, list[str]] | None = None def require_install_dir(self, command_name: str) -> Path: """Return install_dir or raise when the command requires one.""" diff --git a/src/vstack/cli/catalog.py b/src/vstack/cli/catalog.py index 65c0eec..d09cfd4 100644 --- a/src/vstack/cli/catalog.py +++ b/src/vstack/cli/catalog.py @@ -7,6 +7,7 @@ from typing import TYPE_CHECKING from vstack.cli.base import BaseCommand +from vstack.cli.init import InitCommand from vstack.cli.install import InstallCommand from vstack.cli.manifest import ManifestCommand from vstack.cli.status import StatusCommand @@ -50,6 +51,7 @@ class ManifestSubcommandConfig: "status", "manifest", "install", + "init", "uninstall", ) @@ -95,7 +97,7 @@ class ManifestSubcommandConfig: ), "install": TopLevelCommandConfig( command_factory=InstallCommand, - help_text="Generate and install artifacts (--only to filter types)", + help_text="First-run project setup: create .vstack/, seed docs/, then run init", requires_install_dir=True, resolve_only_for_scope=True, include_scope_group=True, @@ -103,6 +105,16 @@ class ManifestSubcommandConfig: scope_help="Install into <dir>/.github/", only_help="Install only these artifact types, e.g. --only skill agent", ), + "init": TopLevelCommandConfig( + command_factory=InitCommand, + help_text="Regenerate and install artifacts idempotently (CI and day-to-day use)", + requires_install_dir=True, + resolve_only_for_scope=True, + include_scope_group=True, + include_only_option=True, + scope_help="Regenerate into <dir>/.github/", + only_help="Regenerate only these artifact types, e.g. --only skill agent", + ), "uninstall": TopLevelCommandConfig( command_factory=UninstallCommand, help_text="Safely remove vstack-managed files tracked in the manifest", diff --git a/src/vstack/cli/constants.py b/src/vstack/cli/constants.py index 0d34898..c43e328 100644 --- a/src/vstack/cli/constants.py +++ b/src/vstack/cli/constants.py @@ -38,6 +38,8 @@ "onboard", "dependency", "incident", + "rca", + "postmortem", "gh-issues", "codeql", "dependabot", diff --git a/src/vstack/cli/init.py b/src/vstack/cli/init.py new file mode 100644 index 0000000..a7f2dd2 --- /dev/null +++ b/src/vstack/cli/init.py @@ -0,0 +1,537 @@ +"""Init command — idempotent artifact regeneration for CI and day-to-day use.""" + +from __future__ import annotations + +import datetime +import sys +from pathlib import Path +from typing import TYPE_CHECKING + +from vstack.cli.base import BaseCommand, CommandContext +from vstack.cli.constants import Colors +from vstack.cli.helpers import normalize_targeted_names +from vstack.constants import VERSION +from vstack.manifest import ( + CURRENT_HASH_ALGORITHM, + CURRENT_MANIFEST_VERSION, + Manifest, + content_hash, + hash_with_algorithm, + preserve_existing_entry, + preserved_manifest_entries, +) + +if TYPE_CHECKING: + from vstack.cli.service import CommandService + + +class InitCommand(BaseCommand): + """Regenerate and install artifacts idempotently into the selected install directory. + + ``vstack init`` is the day-to-day and CI regeneration command. It is safe + to re-run at any time — in CI pipelines and after ``pip install --upgrade vstack``. + Conservative install semantics apply: untracked files are never overwritten, + tracked files with local modifications are skipped and reported. + """ + + def __init__(self, service: CommandService) -> None: + self._service = service + + @staticmethod + def _version_gt(new: str, existing: str | None) -> bool: + """Return ``True`` when *new* template revision is strictly greater than *existing*. + + Supported formats are numeric dot-separated revisions (legacy) and plain + numeric revisions such as ``YYYYMMDDNNN``. + """ + + def _tuple(v: str) -> tuple[int, ...]: + """Convert a version-like string to an integer tuple for comparison.""" + try: + return tuple(int(x) for x in v.split(".")) + except (ValueError, AttributeError): + return (0,) + + return _tuple(new) > _tuple(existing or "0") + + @staticmethod + def _existing_entries_for_init(gens, existing_manifest: Manifest | None): + """Build an existing-entry lookup for selected generators.""" + if existing_manifest is None: + return {} + + existing_entries = {} + for gen in gens: + for entry in existing_manifest.entries_for(gen.config.manifest_key): + key = f"{gen.config.type_name}/{entry.name}" + existing_entries[key] = entry + return existing_entries + + @staticmethod + def _installed_content_matches(*, out_file: Path, existing_entry) -> bool | None: + """Return whether on-disk content still matches the manifest checksum.""" + if not out_file.exists() or existing_entry.checksum is None: + return None + checksum_algorithm = (existing_entry.checksum_algorithm or CURRENT_HASH_ALGORITHM).lower() + try: + return ( + hash_with_algorithm(out_file.read_text(encoding="utf-8"), checksum_algorithm) + == existing_entry.checksum + ) + except ValueError: + return None + + @staticmethod + def _install_decision( + *, + force: bool, + force_name: bool, + adopt_name: bool, + update: bool, + out_file: Path, + existing_entry, + new_version: str, + ) -> tuple[str, str | None]: + """Return install action and optional explanatory reason.""" + if force or force_name: + return "install", None + if not out_file.exists(): + return "install", None + if existing_entry is None: + if adopt_name: + return "adopt", "existing file adopted into vstack manifest" + return "preserve", "existing file is not tracked by vstack" + current_matches = InitCommand._installed_content_matches( + out_file=out_file, + existing_entry=existing_entry, + ) + if current_matches is False: + return "preserve", "local changes detected" + if current_matches is None: + return "preserve", "tracked file has no stored checksum" + + if not update: + return "install", None + + if existing_entry.version is not None: + return ( + ("update", None) + if InitCommand._version_gt(new_version, existing_entry.version) + else ("skip", None) + ) + return "preserve", "tracked file has no stored version" + + @staticmethod + def _print_install_action( + *, + colors, + prefix: str, + rel: str, + action: str, + existing_version: str | None, + new_version: str, + out_file: Path, + force: bool, + force_name: bool = False, + reason: str | None = None, + ) -> None: + """Print install/update/skip output line for one artifact.""" + if action == "adopt": + print( + f" {prefix}{colors.CYAN}≈{colors.RESET} {rel}" + f" {colors.DIM}adopted — {reason}{colors.RESET}" + ) + return + + if action == "preserve": + print( + f" {prefix}{colors.YELLOW}↷{colors.RESET} {rel}" + f" {colors.DIM}preserved — {reason}{colors.RESET}" + ) + return + + if action == "skip": + print( + f" {prefix}{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 or force_name) 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}" + ) + + @staticmethod + def _record_manifest_entry( + *, + new_entries, + gen, + artifact_name: str, + version: str | None, + checksum: str, + checksum_algorithm: str, + ) -> None: + """Append one installed artifact entry to in-memory manifest data.""" + from vstack.manifest import ArtifactEntry + + new_entries.setdefault(gen.config.manifest_key, []).append( + ArtifactEntry( + name=artifact_name, + file=gen.install_relative_path(artifact_name), + version=version, + checksum=checksum, + checksum_algorithm=checksum_algorithm, + ) + ) + + @staticmethod + def _adopted_manifest_values( + *, + out_file: Path, + ) -> tuple[str | None, str] | None: + """Return adopted version/checksum from on-disk content. + + Version is read from ``VSTACK-META.artifact_version`` when present. + """ + from vstack.artifacts.generator import GenericArtifactGenerator + + try: + content = out_file.read_text(encoding="utf-8") + except OSError: + return None + + metadata = GenericArtifactGenerator.parse_generation_metadata(content) + adopted_version = metadata.get("artifact_version") if metadata else None + return adopted_version, content_hash(content) + + @staticmethod + def _load_existing_manifest( + *, + service: CommandService, + install_dir: Path, + gens, + ): + """Read and validate current manifest state for an init run.""" + manifest_file = service.manifest_for(install_dir) + existing_manifest = manifest_file.read() + if existing_manifest is None and manifest_file.read_error: + print(f"ERROR: {manifest_file.read_error}", file=sys.stderr) + return None, None, None, None + + selected_manifest_keys = {gen.config.manifest_key for gen in gens} + existing_entries = InitCommand._existing_entries_for_init(gens, existing_manifest) + new_entries = preserved_manifest_entries( + existing_manifest, + selected_manifest_keys, + ) + return manifest_file, existing_manifest, existing_entries, new_entries + + @staticmethod + def _install_single_artifact( + *, + service: CommandService, + gen, + artifact, + out_dir: Path, + colors, + prefix: str, + force: bool, + update: bool, + dry_run: bool, + targeted_force_names: set[str], + targeted_adopt_names: set[str], + existing_entries, + new_entries, + checksum_algorithm: str, + ) -> str: + """Apply install decision flow for one rendered artifact and return the action taken.""" + out_file = out_dir / gen.output_path(artifact.name) + new_version = (artifact.frontmatter or {}).get("version") or VERSION + key = f"{gen.config.type_name}/{artifact.name}" + existing_entry = existing_entries.get(key) + existing_version = existing_entry.version if existing_entry is not None else None + rel = service.label(out_file) + force_name = ( + artifact.name in targeted_force_names + or key in targeted_force_names + or rel in targeted_force_names + ) + adopt_name = ( + artifact.name in targeted_adopt_names + or key in targeted_adopt_names + or rel in targeted_adopt_names + ) + adopted_values: tuple[str | None, str] | None = None + + if artifact.unresolved: + print( + f" {colors.YELLOW}⚠{colors.RESET} {rel} unresolved: {artifact.unresolved}", + file=sys.stderr, + ) + + action, reason = InitCommand._install_decision( + force=force, + force_name=force_name, + adopt_name=adopt_name, + update=update, + out_file=out_file, + existing_entry=existing_entry, + new_version=new_version, + ) + + if action == "adopt" and out_file.exists(): + adopted_values = InitCommand._adopted_manifest_values(out_file=out_file) + if adopted_values is None: + action = "preserve" + reason = "existing file is unreadable; could not adopt into vstack manifest" + + InitCommand._print_install_action( + colors=colors, + prefix=prefix, + rel=rel, + action=action, + existing_version=existing_version, + new_version=new_version, + out_file=out_file, + force=force, + force_name=force_name, + reason=reason, + ) + + if not dry_run and action in {"install", "update"}: + 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") + + if action in {"install", "update"}: + InitCommand._record_manifest_entry( + new_entries=new_entries, + gen=gen, + artifact_name=artifact.name, + version=new_version, + checksum=content_hash(artifact.content), + checksum_algorithm=checksum_algorithm, + ) + return action + + if action == "adopt" and adopted_values is not None: + adopted_version, adopted_checksum = adopted_values + InitCommand._record_manifest_entry( + new_entries=new_entries, + gen=gen, + artifact_name=artifact.name, + version=adopted_version, + checksum=adopted_checksum, + checksum_algorithm=checksum_algorithm, + ) + return action + + if existing_entry is not None: + preserve_existing_entry( + new_entries=new_entries, + manifest_key=gen.config.manifest_key, + existing_entry=existing_entry, + ) + return action + + @staticmethod + def _write_manifest( + *, + service: CommandService, + manifest_file, + new_entries, + checksum_algorithm: str, + colors, + ) -> None: + """Persist the manifest after a non-dry-run init.""" + manifest = Manifest( + manifest_version=CURRENT_MANIFEST_VERSION, + hash_algorithm=checksum_algorithm, + vstack_version=VERSION, + installed_at=datetime.datetime.now(datetime.UTC).isoformat(), + artifacts=new_entries, + ) + manifest_file.write(manifest) + print(f" {colors.DIM}wrote {service.label(manifest_file.path)}{colors.RESET}") + + @staticmethod + def _print_summary( + *, + colors, + action_counts: dict[str, int], + preserved_selectors: list[str], + dry_run: bool, + ) -> None: + """Print a readable summary and conflict guidance after an init run.""" + installed = action_counts.get("install", 0) + updated = action_counts.get("update", 0) + preserved = action_counts.get("preserve", 0) + skipped = action_counts.get("skip", 0) + adopted = action_counts.get("adopt", 0) + + install_label = "installed" + summary_title = "Summary (dry-run)" if dry_run else "Summary" + total = installed + updated + preserved + skipped + adopted + + print() + print(f" {colors.BOLD}{summary_title}{colors.RESET}") + print(f" total processed : {colors.BOLD}{total}{colors.RESET}") + print(f" {install_label:<15}: {colors.BOLD}{installed}{colors.RESET}") + print(f" updated : {colors.BOLD}{updated}{colors.RESET}") + print(f" preserved : {colors.BOLD}{preserved}{colors.RESET}") + print(f" skipped : {colors.BOLD}{skipped}{colors.RESET}") + print(f" adopted : {colors.BOLD}{adopted}{colors.RESET}") + + if preserved: + noun = "file" if preserved == 1 else "files" + selectors_suffix = " Preserved selectors:" if preserved_selectors else "" + print() + print( + f" {colors.YELLOW}⚠{colors.RESET} " + f"{preserved} {noun} preserved — existing files were not overwritten." + f"{selectors_suffix}" + ) + if preserved_selectors: + for selector in sorted(preserved_selectors): + print(f" - {selector}") + print(" Next steps:") + print(f" {colors.DIM}--force{colors.RESET} overwrite all") + print( + f" {colors.DIM}--force-name <name|type/name>{colors.RESET} " + "overwrite one artifact" + ) + print( + f" {colors.DIM}--adopt-name <name|type/name>{colors.RESET} " + "take ownership without overwriting" + ) + + @staticmethod + def execute( + service: CommandService, + install_dir: Path, + *, + only: list[str] | None = None, + force: bool = False, + force_names: list[str] | None = None, + adopt_names: list[str] | None = None, + update: bool = False, + dry_run: bool = False, + excluded_names: dict[str, list[str]] | None = None, + ) -> int: + """Generate and install artifacts idempotently into *install_dir*. + + :param excluded_names: Optional mapping of singular type name + (e.g. ``"skill"``) to a list of artifact names to skip for that + type. Populated from the ``exclude:`` block in + ``.vstack/config.yaml`` and applied before any install action. + Excluded artifacts are reported on screen but not written to disk + and not recorded in the manifest. + """ + colors = Colors + checksum_algorithm = CURRENT_HASH_ALGORITHM + + gens = [g for g in service.generators if only is None or g.config.type_name in only] + targeted_force_names = normalize_targeted_names(force_names) + targeted_adopt_names = normalize_targeted_names(adopt_names) + + manifest_file, _, existing_entries, new_entries = InitCommand._load_existing_manifest( + service=service, + install_dir=install_dir, + gens=gens, + ) + if manifest_file is None or existing_entries is None or new_entries is None: + return 1 + + prefix = f"{colors.DIM}[dry-run]{colors.RESET} " if dry_run else "" + all_ok = True + action_counts: dict[str, int] = {} + preserved_selectors: set[str] = set() + + for gen in gens: + out_dir = install_dir / gen.config.output_subdir + artifacts = gen.render_all() + + for artifact in artifacts: + if excluded_names and artifact.name in excluded_names.get(gen.config.type_name, []): + out_file = out_dir / gen.install_relative_path(artifact.name) + rel = service.label(out_file) + print( + f" {prefix}{colors.DIM}↷{colors.RESET} {rel}" + f" {colors.DIM}excluded by config{colors.RESET}" + ) + continue + artifact_action = InitCommand._install_single_artifact( + service=service, + gen=gen, + artifact=artifact, + out_dir=out_dir, + colors=colors, + prefix=prefix, + force=force, + update=update, + dry_run=dry_run, + targeted_force_names=targeted_force_names, + targeted_adopt_names=targeted_adopt_names, + existing_entries=existing_entries, + new_entries=new_entries, + checksum_algorithm=checksum_algorithm, + ) + action_counts[artifact_action] = action_counts.get(artifact_action, 0) + 1 + if artifact_action == "preserve": + preserved_selectors.add(f"{gen.config.type_name}/{artifact.name}") + + # Verify source for unresolvable issues. + verify_result = gen.verify_input() + for msg in verify_result.messages: + if msg.level == "fail": + print(f" ERROR [{gen.config.type_name}]: {msg.message}", file=sys.stderr) + all_ok = False + + InitCommand._print_summary( + colors=colors, + action_counts=action_counts, + preserved_selectors=sorted(preserved_selectors), + dry_run=dry_run, + ) + + if not dry_run: + InitCommand._write_manifest( + service=service, + manifest_file=manifest_file, + new_entries=new_entries, + checksum_algorithm=checksum_algorithm, + colors=colors, + ) + + return 0 if all_ok else 1 + + def run( + self, + *, + context: CommandContext, + ) -> int: + """Execute the init command and return a process-style status code.""" + install_dir = context.require_install_dir("init") + + return InitCommand.execute( + self._service, + install_dir, + only=context.only, + excluded_names=context.excluded_names, + force=getattr(context.args, "force", False), + force_names=getattr(context.args, "force_names", None), + adopt_names=getattr(context.args, "adopt_name", None), + update=getattr(context.args, "update", False), + dry_run=getattr(context.args, "dry_run", False), + ) diff --git a/src/vstack/cli/install.py b/src/vstack/cli/install.py index 1660e37..b0d8fc2 100644 --- a/src/vstack/cli/install.py +++ b/src/vstack/cli/install.py @@ -1,512 +1,151 @@ -"""Install command wrapper.""" +"""Install command — first-run project setup wizard.""" from __future__ import annotations -import datetime -import sys from pathlib import Path from typing import TYPE_CHECKING from vstack.cli.base import BaseCommand, CommandContext from vstack.cli.constants import Colors -from vstack.cli.helpers import normalize_targeted_names -from vstack.constants import VERSION -from vstack.manifest import ( - CURRENT_HASH_ALGORITHM, - CURRENT_MANIFEST_VERSION, - Manifest, - content_hash, - hash_with_algorithm, - preserve_existing_entry, - preserved_manifest_entries, -) +from vstack.cli.init import InitCommand if TYPE_CHECKING: from vstack.cli.service import CommandService class InstallCommand(BaseCommand): - """Install artifacts into the selected install directory.""" + """First-run project setup wizard. + + Seeds ``.vstack/`` scaffold and per-agent guidance templates from package + templates (additive only), then delegates artifact generation to + :class:`~vstack.cli.init.InitCommand`. + """ def __init__(self, service: CommandService) -> None: self._service = service @staticmethod - def _version_gt(new: str, existing: str | None) -> bool: - """Return ``True`` when *new* template revision is strictly greater than *existing*. - - Supported formats are numeric dot-separated revisions (legacy) and plain - numeric revisions such as ``YYYYMMDDNNN``. - """ - - def _tuple(v: str) -> tuple[int, ...]: - """Convert a version-like string to an integer tuple for comparison.""" - try: - return tuple(int(x) for x in v.split(".")) - except (ValueError, AttributeError): - return (0,) - - return _tuple(new) > _tuple(existing or "0") - - @staticmethod - def _existing_entries_for_install(gens, existing_manifest: Manifest | None): - """Build an existing-entry lookup for selected generators.""" - if existing_manifest is None: - return {} - - existing_entries = {} - for gen in gens: - for entry in existing_manifest.entries_for(gen.config.manifest_key): - key = f"{gen.config.type_name}/{entry.name}" - existing_entries[key] = entry - return existing_entries - - @staticmethod - def _installed_content_matches(*, out_file: Path, existing_entry) -> bool | None: - """Return whether on-disk content still matches the manifest checksum.""" - if not out_file.exists() or existing_entry.checksum is None: - return None - checksum_algorithm = (existing_entry.checksum_algorithm or CURRENT_HASH_ALGORITHM).lower() - try: - return ( - hash_with_algorithm(out_file.read_text(encoding="utf-8"), checksum_algorithm) - == existing_entry.checksum - ) - except ValueError: - return None - - @staticmethod - def _install_decision( - *, - force: bool, - force_name: bool, - adopt_name: bool, - update: bool, - out_file: Path, - existing_entry, - new_version: str, - ) -> tuple[str, str | None]: - """Return install action and optional explanatory reason.""" - if force or force_name: - return "install", None - if not out_file.exists(): - return "install", None - if existing_entry is None: - if adopt_name: - return "adopt", "existing file adopted into vstack manifest" - return "preserve", "existing file is not tracked by vstack" - current_matches = InstallCommand._installed_content_matches( - out_file=out_file, - existing_entry=existing_entry, - ) - if current_matches is False: - return "preserve", "local changes detected" - if current_matches is None: - return "preserve", "tracked file has no stored checksum" - - if not update: - return "install", None - - if existing_entry.version is not None: - return ( - ("update", None) - if InstallCommand._version_gt(new_version, existing_entry.version) - else ("skip", None) - ) - return "preserve", "tracked file has no stored version" - - @staticmethod - def _print_install_action( - *, - colors, - prefix: str, - rel: str, - action: str, - existing_version: str | None, - new_version: str, - out_file: Path, - force: bool, - force_name: bool = False, - reason: str | None = None, - ) -> None: - """Print install/update/skip output line for one artifact.""" - if action == "adopt": - print( - f" {prefix}{colors.CYAN}≈{colors.RESET} {rel}" - f" {colors.DIM}adopted — {reason}{colors.RESET}" - ) - return - - if action == "preserve": - print( - f" {prefix}{colors.YELLOW}↷{colors.RESET} {rel}" - f" {colors.DIM}preserved — {reason}{colors.RESET}" - ) - return - - if action == "skip": - print( - f" {prefix}{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 or force_name) 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}" - ) - - @staticmethod - def _record_manifest_entry( - *, - new_entries, - gen, - artifact_name: str, - version: str | None, - checksum: str, - checksum_algorithm: str, - ) -> None: - """Append one installed artifact entry to in-memory manifest data.""" - from vstack.manifest import ArtifactEntry - - new_entries.setdefault(gen.config.manifest_key, []).append( - ArtifactEntry( - name=artifact_name, - file=gen.install_relative_path(artifact_name), - version=version, - checksum=checksum, - checksum_algorithm=checksum_algorithm, - ) - ) - - @staticmethod - def _adopted_manifest_values( - *, - out_file: Path, - ) -> tuple[str | None, str] | None: - """Return adopted version/checksum from on-disk content. - - Version is read from ``VSTACK-META.artifact_version`` when present. - """ - from vstack.artifacts.generator import GenericArtifactGenerator - - try: - content = out_file.read_text(encoding="utf-8") - except OSError: - return None - - metadata = GenericArtifactGenerator.parse_generation_metadata(content) - adopted_version = metadata.get("artifact_version") if metadata else None - return adopted_version, content_hash(content) - - @staticmethod - def _load_existing_manifest( - *, - service: CommandService, - install_dir: Path, - gens, - ): - """Read and validate current manifest state for an install run.""" - manifest_file = service.manifest_for(install_dir) - existing_manifest = manifest_file.read() - if existing_manifest is None and manifest_file.read_error: - print(f"ERROR: {manifest_file.read_error}", file=sys.stderr) - return None, None, None, None - - selected_manifest_keys = {gen.config.manifest_key for gen in gens} - existing_entries = InstallCommand._existing_entries_for_install(gens, existing_manifest) - new_entries = preserved_manifest_entries( - existing_manifest, - selected_manifest_keys, - ) - return manifest_file, existing_manifest, existing_entries, new_entries - - @staticmethod - def _install_single_artifact( - *, - service: CommandService, - gen, - artifact, - out_dir: Path, - colors, - prefix: str, - force: bool, - update: bool, - dry_run: bool, - targeted_force_names: set[str], - targeted_adopt_names: set[str], - existing_entries, - new_entries, - checksum_algorithm: str, - ) -> str: - """Apply install decision flow for one rendered artifact and return the action taken.""" - out_file = out_dir / gen.output_path(artifact.name) - new_version = (artifact.frontmatter or {}).get("version") or VERSION - key = f"{gen.config.type_name}/{artifact.name}" - existing_entry = existing_entries.get(key) - existing_version = existing_entry.version if existing_entry is not None else None - rel = service.label(out_file) - force_name = ( - artifact.name in targeted_force_names - or key in targeted_force_names - or rel in targeted_force_names - ) - adopt_name = ( - artifact.name in targeted_adopt_names - or key in targeted_adopt_names - or rel in targeted_adopt_names - ) - adopted_values: tuple[str | None, str] | None = None - - if artifact.unresolved: - print( - f" {colors.YELLOW}⚠{colors.RESET} {rel} unresolved: {artifact.unresolved}", - file=sys.stderr, - ) - - action, reason = InstallCommand._install_decision( - force=force, - force_name=force_name, - adopt_name=adopt_name, - update=update, - out_file=out_file, - existing_entry=existing_entry, - new_version=new_version, - ) - - if action == "adopt" and out_file.exists(): - adopted_values = InstallCommand._adopted_manifest_values(out_file=out_file) - if adopted_values is None: - action = "preserve" - reason = "existing file is unreadable; could not adopt into vstack manifest" - - InstallCommand._print_install_action( - colors=colors, - prefix=prefix, - rel=rel, - action=action, - existing_version=existing_version, - new_version=new_version, - out_file=out_file, - force=force, - force_name=force_name, - reason=reason, - ) - - if not dry_run and action in {"install", "update"}: - 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") - - if action in {"install", "update"}: - InstallCommand._record_manifest_entry( - new_entries=new_entries, - gen=gen, - artifact_name=artifact.name, - version=new_version, - checksum=content_hash(artifact.content), - checksum_algorithm=checksum_algorithm, - ) - return action - - if action == "adopt" and adopted_values is not None: - adopted_version, adopted_checksum = adopted_values - InstallCommand._record_manifest_entry( - new_entries=new_entries, - gen=gen, - artifact_name=artifact.name, - version=adopted_version, - checksum=adopted_checksum, - checksum_algorithm=checksum_algorithm, - ) - return action - - if existing_entry is not None: - preserve_existing_entry( - new_entries=new_entries, - manifest_key=gen.config.manifest_key, - existing_entry=existing_entry, - ) - return action - - @staticmethod - def _write_manifest( - *, - service: CommandService, - manifest_file, - new_entries, - checksum_algorithm: str, - colors, - ) -> None: - """Persist the manifest after a non-dry-run install.""" - manifest = Manifest( - manifest_version=CURRENT_MANIFEST_VERSION, - hash_algorithm=checksum_algorithm, - vstack_version=VERSION, - installed_at=datetime.datetime.now(datetime.UTC).isoformat(), - artifacts=new_entries, - ) - manifest_file.write(manifest) - print(f" {colors.DIM}wrote {service.label(manifest_file.path)}{colors.RESET}") - - @staticmethod - def _print_summary( + def _seed_project( *, + project_root: Path, + templates_root: Path, colors, - action_counts: dict[str, int], - preserved_selectors: list[str], dry_run: bool, ) -> None: - """Print a readable summary and conflict guidance after an install run.""" - installed = action_counts.get("install", 0) - updated = action_counts.get("update", 0) - preserved = action_counts.get("preserve", 0) - skipped = action_counts.get("skip", 0) - adopted = action_counts.get("adopt", 0) - - install_label = "installed" - summary_title = "Summary (dry-run)" if dry_run else "Summary" - total = installed + updated + preserved + skipped + adopted - - print() - print(f" {colors.BOLD}{summary_title}{colors.RESET}") - print(f" total processed : {colors.BOLD}{total}{colors.RESET}") - print(f" {install_label:<15}: {colors.BOLD}{installed}{colors.RESET}") - print(f" updated : {colors.BOLD}{updated}{colors.RESET}") - print(f" preserved : {colors.BOLD}{preserved}{colors.RESET}") - print(f" skipped : {colors.BOLD}{skipped}{colors.RESET}") - print(f" adopted : {colors.BOLD}{adopted}{colors.RESET}") - - if preserved: - noun = "file" if preserved == 1 else "files" - selectors_suffix = " Preserved selectors:" if preserved_selectors else "" - print() - print( - f" {colors.YELLOW}⚠{colors.RESET} " - f"{preserved} {noun} preserved — existing files were not overwritten." - f"{selectors_suffix}" - ) - if preserved_selectors: - for selector in sorted(preserved_selectors): - print(f" - {selector}") - print(" Next steps:") - print(f" {colors.DIM}--force{colors.RESET} overwrite all") - print( - f" {colors.DIM}--force-name <name|type/name>{colors.RESET} " - "overwrite one artifact" - ) - print( - f" {colors.DIM}--adopt-name <name|type/name>{colors.RESET} " - "take ownership without overwriting" - ) - - @staticmethod - def execute( - service: CommandService, - install_dir: Path, - *, - only: list[str] | None = None, - force: bool = False, - force_names: list[str] | None = None, - adopt_names: list[str] | None = None, - update: bool = False, - dry_run: bool = False, - ) -> int: - """Generate and install artifacts into install_dir.""" - colors = Colors - checksum_algorithm = CURRENT_HASH_ALGORITHM + """Seed project-scope files from package templates (additive only). - gens = [g for g in service.generators if only is None or g.config.type_name in only] - targeted_force_names = normalize_targeted_names(force_names) - targeted_adopt_names = normalize_targeted_names(adopt_names) + Two sources are seeded into *project_root*: - manifest_file, _, existing_entries, new_entries = InstallCommand._load_existing_manifest( - service=service, - install_dir=install_dir, - gens=gens, - ) - if manifest_file is None or existing_entries is None or new_entries is None: - return 1 + 1. ``_templates/project/`` — base ``.vstack/`` scaffold (config, tmp/). + 2. ``_templates/agents/{name}/artifacts/`` — per-role guidance templates + seeded to ``.vstack/templates/{name}/artifacts/``. + Existing files are never modified. + """ prefix = f"{colors.DIM}[dry-run]{colors.RESET} " if dry_run else "" - all_ok = True - action_counts: dict[str, int] = {} - preserved_selectors: set[str] = set() - for gen in gens: - out_dir = install_dir / gen.config.output_subdir - artifacts = gen.render_all() - - for artifact in artifacts: - artifact_action = InstallCommand._install_single_artifact( - service=service, - gen=gen, - artifact=artifact, - out_dir=out_dir, - colors=colors, - prefix=prefix, - force=force, - update=update, - dry_run=dry_run, - targeted_force_names=targeted_force_names, - targeted_adopt_names=targeted_adopt_names, - existing_entries=existing_entries, - new_entries=new_entries, - checksum_algorithm=checksum_algorithm, + def _seed_file(src_file: Path, dst: Path) -> None: + if dst.exists(): + rel = dst.relative_to(project_root) + print( + f" {prefix}{colors.YELLOW}↷{colors.RESET} {rel}" + f" {colors.DIM}skipped — already exists{colors.RESET}" ) - action_counts[artifact_action] = action_counts.get(artifact_action, 0) + 1 - if artifact_action == "preserve": - preserved_selectors.add(f"{gen.config.type_name}/{artifact.name}") - - # Verify source for unresolvable issues. - verify_result = gen.verify_input() - for msg in verify_result.messages: - if msg.level == "fail": - print(f" ERROR [{gen.config.type_name}]: {msg.message}", file=sys.stderr) - all_ok = False - - InstallCommand._print_summary( - colors=colors, - action_counts=action_counts, - preserved_selectors=sorted(preserved_selectors), - dry_run=dry_run, - ) + else: + rel = dst.relative_to(project_root) + print( + f" {prefix}{colors.GREEN}✓{colors.RESET} " + f"{colors.BOLD}{rel}{colors.RESET}" + f" {colors.DIM}seeded{colors.RESET}" + ) + if not dry_run: + dst.parent.mkdir(parents=True, exist_ok=True) + dst.write_text(src_file.read_text(encoding="utf-8"), encoding="utf-8") + + # 1. Base .vstack/ scaffold + project_seed = templates_root / "project" + if project_seed.exists(): + for src_file in sorted(project_seed.rglob("*")): + if src_file.is_file(): + _seed_file(src_file, project_root / src_file.relative_to(project_seed)) + + # 2. Per-agent artifacts → .vstack/templates/{agent}/artifacts/ + agents_root = templates_root / "agents" + if agents_root.exists(): + for agent_dir in sorted(agents_root.iterdir()): + if not agent_dir.is_dir(): + continue + artifacts_dir = agent_dir / "artifacts" + if not artifacts_dir.exists(): + continue + for src_file in sorted(artifacts_dir.rglob("*")): + if src_file.is_file(): + rel = src_file.relative_to(artifacts_dir) + dst = ( + project_root + / ".vstack" + / "templates" + / agent_dir.name + / "artifacts" + / rel + ) + _seed_file(src_file, dst) + @staticmethod + def _write_vstack_gitignore(*, project_root: Path, dry_run: bool) -> None: + """Write (or overwrite) ``.vstack/.gitignore`` with generated content. + + Unlike seed files, ``.gitignore`` is always regenerated on install so it + stays current with changes to the ``.vstack/`` directory layout. The file + ignores scratch/temporary output under ``.vstack/tmp/`` while keeping all + other committed files — ``.gitignore`` itself, ``config.yaml``, + ``vstack.json``, and the seeded ``templates/`` tree — tracked by Git. + """ + content = "# Generated by vstack install — do not edit manually.\ntmp/\n" + gitignore = project_root / ".vstack" / ".gitignore" if not dry_run: - InstallCommand._write_manifest( - service=service, - manifest_file=manifest_file, - new_entries=new_entries, - checksum_algorithm=checksum_algorithm, - colors=colors, - ) - - return 0 if all_ok else 1 + gitignore.parent.mkdir(parents=True, exist_ok=True) + gitignore.write_text(content, encoding="utf-8") def run( self, *, context: CommandContext, ) -> int: + """Execute the install wizard and return a process-style status code. + + For local installs, seeds ``.vstack/`` scaffold and per-agent guidance + templates (additive only) before generating ``.github/`` artifacts. + Global installs skip seeding and generate artifacts directly. + """ install_dir = context.require_install_dir("install") + use_global = getattr(context.args, "use_global", False) + dry_run = getattr(context.args, "dry_run", False) + colors = Colors - return InstallCommand.execute( + if not use_global: + project_root = install_dir.parent + print(f"\n {colors.BOLD}Seeding project files{colors.RESET}") + InstallCommand._seed_project( + project_root=project_root, + templates_root=self._service.root, + colors=colors, + dry_run=dry_run, + ) + InstallCommand._write_vstack_gitignore( + project_root=project_root, + dry_run=dry_run, + ) + + print(f"\n {colors.BOLD}Generating .github/ artifacts{colors.RESET}") + return InitCommand.execute( self._service, install_dir, only=context.only, + excluded_names=context.excluded_names, force=getattr(context.args, "force", False), force_names=getattr(context.args, "force_names", None), adopt_names=getattr(context.args, "adopt_name", None), update=getattr(context.args, "update", False), - dry_run=getattr(context.args, "dry_run", False), + dry_run=dry_run, ) diff --git a/src/vstack/cli/interface.py b/src/vstack/cli/interface.py index 436d1b7..ddf1bc7 100644 --- a/src/vstack/cli/interface.py +++ b/src/vstack/cli/interface.py @@ -7,10 +7,20 @@ from vstack.cli.base import CommandContext from vstack.cli.catalog import COMMAND_CATALOG -from vstack.cli.constants import GLOBAL_SUPPORTED_TYPE_NAMES +from vstack.cli.constants import GLOBAL_SUPPORTED_TYPE_NAMES, KNOWN_TYPE_NAMES from vstack.cli.parser import CommandLineParser from vstack.cli.registry import build_command_registry from vstack.cli.service import CommandService +from vstack.constants import ARTIFACTS_DOCS_ROOT +from vstack.frontmatter import FrontmatterParser + +# Maps plural config.yaml keys to singular internal type names. +_CONFIG_TYPE_ALIAS: dict[str, str] = { + "agents": "agent", + "instructions": "instruction", + "prompts": "prompt", + "skills": "skill", +} class CommandLineInterface: @@ -69,13 +79,92 @@ def _resolve_only_filter( return self.resolve_only_for_scope(args) return getattr(args, "only", None) + @staticmethod + def _read_exclude( + install_dir: Path | None, + ) -> tuple[frozenset[str], dict[str, list[str]]]: + """Read ``exclude:`` from ``.vstack/config.yaml`` when available. + + Returns a pair: + + - *excluded_types*: set of singular type names to skip entirely + (from ``type: all``). + - *excluded_names*: mapping of singular type name → artifact names + to skip within that type (from ``type: [name, …]``). + + Both are empty when *install_dir* is ``None``, when the config file + does not exist, or when the ``exclude:`` key is absent. + """ + if install_dir is None: + return frozenset(), {} + config_path = install_dir.parent / ".vstack" / "config.yaml" + if not config_path.exists(): + return frozenset(), {} + parsed = FrontmatterParser.parse_yaml(config_path.read_text(encoding="utf-8")) + raw_exclude = parsed.get("exclude", "") + # The minimal YAML parser stores nested mappings as raw indented strings. + # Re-parse by stripping the 2-space indent to access sub-keys. + if isinstance(raw_exclude, str) and raw_exclude.strip(): + dedented = "\n".join( + line[2:] if line.startswith(" ") else line for line in raw_exclude.split("\n") + ) + raw_exclude = FrontmatterParser.parse_yaml(dedented) or {} + if not isinstance(raw_exclude, dict): + return frozenset(), {} + excluded_types: set[str] = set() + excluded_names: dict[str, list[str]] = {} + for config_key, value in raw_exclude.items(): + type_name = _CONFIG_TYPE_ALIAS.get(config_key) + if type_name is None: + continue # unknown key, ignore gracefully + if type_name == "agent": + raise ValueError( + "exclude: agents is not supported. " + "The six-role agent chain is an atomic unit and cannot be partially " + "excluded. See ADR-022." + ) + if isinstance(value, str) and value.strip().lower() == "all": + excluded_types.add(type_name) + elif isinstance(value, list): + names = [str(n) for n in value if isinstance(n, str)] + if names: + excluded_names[type_name] = names + return frozenset(excluded_types), excluded_names + + @staticmethod + def _read_artifacts_root(install_dir: Path | None) -> str: + """Read ``artifacts.root`` from ``.vstack/config.yaml`` when available. + + Returns :data:`~vstack.constants.ARTIFACTS_DOCS_ROOT` when *install_dir* + is ``None``, when the config file does not exist, or when the key is + absent or blank. + """ + if install_dir is None: + return ARTIFACTS_DOCS_ROOT + config_path = install_dir.parent / ".vstack" / "config.yaml" + if not config_path.exists(): + return ARTIFACTS_DOCS_ROOT + parsed = FrontmatterParser.parse_yaml(config_path.read_text(encoding="utf-8")) + artifacts = parsed.get("artifacts", "") + # The minimal YAML parser stores nested mappings as raw indented strings. + # Re-parse by stripping the 2-space indent to access sub-keys. + if isinstance(artifacts, str) and artifacts.strip(): + dedented = "\n".join( + line[2:] if line.startswith(" ") else line for line in artifacts.split("\n") + ) + artifacts = FrontmatterParser.parse_yaml(dedented) or {} + if not isinstance(artifacts, dict): + return ARTIFACTS_DOCS_ROOT + value = artifacts.get("root", "") + if isinstance(value, str) and value.strip(): + return value.strip() + return ARTIFACTS_DOCS_ROOT + def run(self) -> int: """Run one CLI invocation and return a process-style status code.""" cli_parser = self._parser_cls() parser = cli_parser.build() args = parser.parse_args() - service = self._service_cls(templates_root=self._templates_root) - commands = build_command_registry(service) command_config = COMMAND_CATALOG[args.command] resolved_install_dir = self._resolve_install_dir( @@ -83,11 +172,25 @@ def run(self) -> int: args=args, requires_install_dir=command_config.requires_install_dir, ) + artifacts_root = self._read_artifacts_root(resolved_install_dir) + service = self._service_cls( + templates_root=self._templates_root, artifacts_root=artifacts_root + ) + commands = build_command_registry(service) effective_only = self._resolve_only_filter( args=args, resolve_only_for_scope=command_config.resolve_only_for_scope, ) + excluded_types, excluded_names = self._read_exclude(resolved_install_dir) + if excluded_types: + base = effective_only if effective_only is not None else list(KNOWN_TYPE_NAMES) + effective_only = [t for t in base if t not in excluded_types] command = commands[args.command] - context = CommandContext(args=args, install_dir=resolved_install_dir, only=effective_only) + context = CommandContext( + args=args, + install_dir=resolved_install_dir, + only=effective_only, + excluded_names=excluded_names or None, + ) return command.run(context=context) diff --git a/src/vstack/cli/parser.py b/src/vstack/cli/parser.py index 3e2d411..d48eedf 100644 --- a/src/vstack/cli/parser.py +++ b/src/vstack/cli/parser.py @@ -204,6 +204,52 @@ def _add_install_command(self, sub: SubparserFactory) -> None: help="Show what would be installed without writing files", ) + def _add_init_command(self, sub: SubparserFactory) -> None: + """Register the ``init`` subcommand.""" + init_config = COMMAND_CATALOG["init"] + parser = self._add_scoped_only_command(sub, command_name="init", config=init_config) + + mode = parser.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", + ) + + parser.add_argument( + "--force-name", + dest="force_names", + action="append", + metavar="<name|type/name>", + help=( + "Force regenerate one named artifact without overwriting everything. " + "Accepts a plain name (e.g. engineer) or a type/name selector " + "(e.g. agent/engineer). Repeat to target multiple artifacts." + ), + ) + parser.add_argument( + "--adopt-name", + action="append", + default=None, + metavar="<name|type/name>", + help=( + "Adopt only the named existing unmanaged artifact into the manifest without overwriting it. " + "Accepts a plain name (e.g. engineer) or a type/name selector " + "(e.g. agent/engineer). Repeat to target multiple artifacts." + ), + ) + parser.add_argument( + "--dry-run", + dest="dry_run", + action="store_true", + help="Show what would be regenerated without writing files", + ) + def _add_uninstall_command(self, sub: SubparserFactory) -> None: """Register the ``uninstall`` subcommand.""" uninstall_config = COMMAND_CATALOG["uninstall"] @@ -291,6 +337,7 @@ def build(self) -> argparse.ArgumentParser: "status": self._add_status_command, "manifest": self._add_manifest_command, "install": self._add_install_command, + "init": self._add_init_command, "uninstall": self._add_uninstall_command, } for command_name in TOP_LEVEL_COMMAND_ORDER: diff --git a/src/vstack/cli/service.py b/src/vstack/cli/service.py index 2f8f212..42ca8d2 100644 --- a/src/vstack/cli/service.py +++ b/src/vstack/cli/service.py @@ -19,8 +19,11 @@ import sys from pathlib import Path +from vstack.agents.config import AGENT_TYPE +from vstack.agents.generator import AgentGenerator from vstack.artifacts.generator import GenericArtifactGenerator from vstack.cli.constants import KNOWN_TYPES, ArtifactState +from vstack.constants import ARTIFACTS_DOCS_ROOT, VSTACK_DIR_NAME from vstack.manifest import CURRENT_MANIFEST_VERSION, ManifestFile, hash_with_algorithm @@ -31,15 +34,27 @@ class CommandService: while keeping per-type behavior in ``ArtifactTypeConfig`` definitions. """ - def __init__(self, templates_root: Path) -> None: + def __init__(self, templates_root: Path, *, artifacts_root: str = ARTIFACTS_DOCS_ROOT) -> None: """Create generators for all known artifact families. + Per-type generator subclasses are used when available so that + type-specific placeholder injection (e.g. ``{{AGENT_ARTIFACTS_*}}``) + runs during install without requiring changes to + :class:`~vstack.artifacts.generator.GenericArtifactGenerator`. + Args: templates_root: Root directory containing the source templates. + artifacts_root: Root directory for agent artifact paths. Passed + through to :class:`~vstack.agents.generator.AgentGenerator`. + Defaults to :data:`~vstack.constants.ARTIFACTS_DOCS_ROOT`; + override via ``artifacts.root`` in ``.vstack/config.yaml``. """ self.root = templates_root self.generators: list[GenericArtifactGenerator] = [ - GenericArtifactGenerator(tc, templates_root) for tc in KNOWN_TYPES + AgentGenerator(templates_root, artifacts_root=artifacts_root) + if tc is AGENT_TYPE + else GenericArtifactGenerator(tc, templates_root) + for tc in KNOWN_TYPES ] # ── Shared helpers ─────────────────────────────────────────────────────── @@ -51,8 +66,16 @@ def label(self, path: Path) -> str: except ValueError: return str(path) - def manifest_for(self, install_dir: Path): - """Build the manifest accessor for a given install directory.""" + def manifest_for(self, install_dir: Path) -> ManifestFile: + """Build the manifest accessor for a given install directory. + + For project installs (``install_dir`` named ``.github``), the manifest + lives in the sibling ``.vstack/`` directory. For global installs and + test fixtures that pass arbitrary directories, it falls back to + ``install_dir`` directly. + """ + if install_dir.name == ".github": + return ManifestFile(parent_dir=install_dir.parent / VSTACK_DIR_NAME) return ManifestFile(parent_dir=install_dir) def gen_for(self, type_name: str) -> GenericArtifactGenerator | None: @@ -139,9 +162,9 @@ def install( update: Install only when a newer version is available. dry_run: Print what would happen without writing any files. """ - from vstack.cli.install import InstallCommand + from vstack.cli.init import InitCommand - return InstallCommand.execute( + return InitCommand.execute( self, install_dir, only=only, diff --git a/src/vstack/cli/status.py b/src/vstack/cli/status.py index cddc4a6..be49bce 100644 --- a/src/vstack/cli/status.py +++ b/src/vstack/cli/status.py @@ -54,7 +54,7 @@ def color(code: str, text: str) -> str: return StatusReport.render_missing_manifest( output_format=output_format, install_dir=install_dir, - error="vstack.json not found; run vstack install before using status", + error=".vstack/vstack.json not found; run vstack install before using status", color=color, ) diff --git a/src/vstack/constants.py b/src/vstack/constants.py index ef96d01..75da934 100644 --- a/src/vstack/constants.py +++ b/src/vstack/constants.py @@ -14,6 +14,11 @@ TEMPLATES_ROOT = Path(str(_PACKAGE_ROOT / "_templates")) +# Default root directory for all role artifacts. Individual agent configs specify +# only the subdirectory via ``artifacts.dir``; this root is applied at render time. +# Override per project via ``artifacts.root`` in ``.vstack/config.yaml``. +ARTIFACTS_DOCS_ROOT = "docs" + _SEMVER_TAG_RE = re.compile(r"^\d+\.\d+\.\d+$") @@ -82,3 +87,4 @@ def _resolve_version() -> str: VERSION = _resolve_version() MANIFEST_FILENAME = "vstack.json" +VSTACK_DIR_NAME = ".vstack" diff --git a/tests/vstack/agents/test_generation.py b/tests/vstack/agents/test_generation.py index ec98d18..0a6cc05 100644 --- a/tests/vstack/agents/test_generation.py +++ b/tests/vstack/agents/test_generation.py @@ -35,6 +35,28 @@ def test_architect_agent_includes_model_and_handoffs(self, tmp_path: Path) -> No assert isinstance(handoffs, list) assert handoffs assert handoffs[0].get("agent") == "designer" - assert "Translate docs/architecture/architecture.md" in str(handoffs[0].get("prompt")) + prompt = str(handoffs[0].get("prompt", "")) + assert len(prompt) > 20 + assert handoffs[0].get("label") assert parsed.content.lstrip().startswith("# architect") + + def test_all_agents_include_generated_artifacts_section(self, tmp_path: Path) -> None: + """Test that all generated agents include artifacts tables from config.yaml.""" + result = run_vstack(["install", "--only", "agent", "--target", str(tmp_path)], timeout=60) + assert result.returncode == 0 + + agents_dir = tmp_path / ".github" / "agents" + for agent_file in agents_dir.glob("*.agent.md"): + content = agent_file.read_text(encoding="utf-8") + assert "## artifacts you use" in content, ( + f"{agent_file.name} missing '## artifacts you use' section" + ) + assert "Agents do not write to artifacts owned by other roles." in content, ( + f"{agent_file.name} missing role-boundary notice" + ) + # Placeholders must not appear in rendered output + for token in ("AGENT_ARTIFACTS_INPUT", "AGENT_ARTIFACTS_OUTPUT"): + assert f"{{{{{token}}}}}" not in content, ( + f"{agent_file.name} has unresolved {{{{{token}}}}} placeholder" + ) diff --git a/tests/vstack/agents/test_generator.py b/tests/vstack/agents/test_generator.py index a2d0ae0..02b821c 100644 --- a/tests/vstack/agents/test_generator.py +++ b/tests/vstack/agents/test_generator.py @@ -2,7 +2,10 @@ from __future__ import annotations +from pathlib import Path + from vstack.agents.generator import AgentGenerator +from vstack.constants import ARTIFACTS_DOCS_ROOT class TestAgentGenerator: @@ -12,3 +15,274 @@ def test_generator_uses_agent_type(self) -> None: """Test that generator uses agent type.""" gen = AgentGenerator() assert gen.config.type_name == "agent" + + def test_template_partials_returns_four_keys(self, tmp_path: Path) -> None: + """Test that template_partials always returns all four placeholder keys.""" + tmpl_dir = tmp_path / "architect" + tmpl_dir.mkdir() + (tmpl_dir / "config.yaml").write_text("name: architect\n", encoding="utf-8") + + result = AgentGenerator().template_partials(tmpl_dir) + + assert set(result) == { + "AGENT_ARTIFACTS_INPUT", + "AGENT_ARTIFACTS_OUTPUT", + "AGENT_ARTIFACTS_INPUT_COMMENTS", + "AGENT_ARTIFACTS_OUTPUT_COMMENTS", + } + + def test_template_partials_empty_when_no_artifacts_block(self, tmp_path: Path) -> None: + """Test that all artifact placeholders are empty strings when config has no artifacts.""" + tmpl_dir = tmp_path / "plain" + tmpl_dir.mkdir() + (tmpl_dir / "config.yaml").write_text("name: plain\n", encoding="utf-8") + + result = AgentGenerator().template_partials(tmpl_dir) + + assert result["AGENT_ARTIFACTS_INPUT"] == "" + assert result["AGENT_ARTIFACTS_OUTPUT"] == "" + assert result["AGENT_ARTIFACTS_INPUT_COMMENTS"] == "" + assert result["AGENT_ARTIFACTS_OUTPUT_COMMENTS"] == "" + + def test_template_partials_input_prefixed_with_docs_root(self, tmp_path: Path) -> None: + """Test that input paths are prefixed with ARTIFACTS_DOCS_ROOT.""" + tmpl_dir = tmp_path / "architect" + tmpl_dir.mkdir() + (tmpl_dir / "config.yaml").write_text( + "name: architect\nartifacts:\n dir: architecture\n input:\n - product/**/*.md\n", + encoding="utf-8", + ) + + result = AgentGenerator().template_partials(tmpl_dir) + + assert f"`{ARTIFACTS_DOCS_ROOT}/product/**/*.md`" in result["AGENT_ARTIFACTS_INPUT"] + + def test_template_partials_output_prefixed_with_root_and_dir(self, tmp_path: Path) -> None: + """Test that output paths are prefixed with root/dir when dir is set.""" + tmpl_dir = tmp_path / "architect" + tmpl_dir.mkdir() + (tmpl_dir / "config.yaml").write_text( + "name: architect\nartifacts:\n dir: architecture\n output:\n - overview.md\n", + encoding="utf-8", + ) + + result = AgentGenerator().template_partials(tmpl_dir) + + assert ( + f"`{ARTIFACTS_DOCS_ROOT}/architecture/overview.md`" in result["AGENT_ARTIFACTS_OUTPUT"] + ) + + def test_template_partials_output_verbatim_when_no_dir(self, tmp_path: Path) -> None: + """Test that output paths are used verbatim when no dir is set.""" + tmpl_dir = tmp_path / "engineer" + tmpl_dir.mkdir() + (tmpl_dir / "config.yaml").write_text( + "name: engineer\nartifacts:\n output:\n - path: ./src/**/*\n", + encoding="utf-8", + ) + + result = AgentGenerator().template_partials(tmpl_dir) + + assert "`src/**/*`" in result["AGENT_ARTIFACTS_OUTPUT"] + assert "docs/" not in result["AGENT_ARTIFACTS_OUTPUT"] + + def test_template_partials_dotslash_strips_prefix_with_dir(self, tmp_path: Path) -> None: + """Test that ./path output items bypass dir prefix even when dir is set.""" + tmpl_dir = tmp_path / "tester" + tmpl_dir.mkdir() + (tmpl_dir / "config.yaml").write_text( + "name: tester\nartifacts:\n dir: reports\n output:\n - ./tests/**/*\n", + encoding="utf-8", + ) + + result = AgentGenerator().template_partials(tmpl_dir) + + assert "`tests/**/*`" in result["AGENT_ARTIFACTS_OUTPUT"] + assert "docs/reports/" not in result["AGENT_ARTIFACTS_OUTPUT"] + + def test_template_partials_input_comments_from_config(self, tmp_path: Path) -> None: + """Test that input_comments config field populates AGENT_ARTIFACTS_INPUT_COMMENTS.""" + tmpl_dir = tmp_path / "custom" + tmpl_dir.mkdir() + (tmpl_dir / "config.yaml").write_text( + "name: custom\nartifacts:\n input_comments: 'Read in order.'\n", + encoding="utf-8", + ) + + result = AgentGenerator().template_partials(tmpl_dir) + + assert result["AGENT_ARTIFACTS_INPUT_COMMENTS"] == "Read in order." + + def test_template_partials_output_comments_from_config(self, tmp_path: Path) -> None: + """Test that output_comments config field populates AGENT_ARTIFACTS_OUTPUT_COMMENTS.""" + tmpl_dir = tmp_path / "custom" + tmpl_dir.mkdir() + (tmpl_dir / "config.yaml").write_text( + "name: custom\nartifacts:\n output_comments: 'See ADR-001.'\n", + encoding="utf-8", + ) + + result = AgentGenerator().template_partials(tmpl_dir) + + assert result["AGENT_ARTIFACTS_OUTPUT_COMMENTS"] == "See ADR-001." + + def test_template_partials_handles_non_dict_artifacts_gracefully(self, tmp_path: Path) -> None: + """Test that template_partials handles malformed (non-dict) artifacts gracefully.""" + tmpl_dir = tmp_path / "broken" + tmpl_dir.mkdir() + # When artifacts is a list rather than a dict mapping, the generator must not crash. + (tmpl_dir / "config.yaml").write_text( + "name: broken\nartifacts:\n - foo\n - bar\n", encoding="utf-8" + ) + + result = AgentGenerator().template_partials(tmpl_dir) + + assert result["AGENT_ARTIFACTS_INPUT"] == "" + assert result["AGENT_ARTIFACTS_OUTPUT"] == "" + + def test_template_partials_handles_non_list_input_output_gracefully( + self, tmp_path: Path + ) -> None: + """Test that scalar input/output values are treated as empty lists.""" + tmpl_dir = tmp_path / "scalar" + tmpl_dir.mkdir() + # After re-parsing a nested raw block, input or output may be scalar strings + # if the indented content is malformed. Verify defensive guards hold. + (tmpl_dir / "config.yaml").write_text( + "name: scalar\nartifacts:\n dir: architecture\n input: not-a-list\n output: not-a-list\n", + encoding="utf-8", + ) + + result = AgentGenerator().template_partials(tmpl_dir) + + assert result["AGENT_ARTIFACTS_INPUT"] == "" + assert result["AGENT_ARTIFACTS_OUTPUT"] == "" + + def test_product_has_no_input_section(self, tmp_path: Path) -> None: + """Test that AGENT_ARTIFACTS_INPUT is empty for product (no input in config).""" + tmpl_dir = tmp_path / "product" + tmpl_dir.mkdir() + (tmpl_dir / "config.yaml").write_text( + "name: product\nartifacts:\n dir: product\n output:\n - vision.md\n", + encoding="utf-8", + ) + + result = AgentGenerator().template_partials(tmpl_dir) + + assert result["AGENT_ARTIFACTS_INPUT"] == "" + assert "### output" in result["AGENT_ARTIFACTS_OUTPUT"] + + def test_template_partials_uses_custom_artifacts_root(self, tmp_path: Path) -> None: + """Test that a custom artifacts_root replaces the default 'docs' prefix.""" + tmpl_dir = tmp_path / "architect" + tmpl_dir.mkdir() + (tmpl_dir / "config.yaml").write_text( + "name: architect\nartifacts:\n dir: architecture\n input:\n - product/**/*.md\n" + " output:\n - overview.md\n", + encoding="utf-8", + ) + + result = AgentGenerator(artifacts_root="documentation").template_partials(tmpl_dir) + + assert "`documentation/product/**/*.md`" in result["AGENT_ARTIFACTS_INPUT"] + assert "`documentation/architecture/overview.md`" in result["AGENT_ARTIFACTS_OUTPUT"] + assert "docs/" not in result["AGENT_ARTIFACTS_INPUT"] + assert "docs/" not in result["AGENT_ARTIFACTS_OUTPUT"] + + +class TestResolveOutputEntries: + """Unit tests for AgentGenerator._resolve_output_entries.""" + + def test_plain_string_with_dir_gets_root_and_dir_prefix(self) -> None: + """Test that plain string items are prefixed with root/dir when dir is set.""" + result = AgentGenerator._resolve_output_entries(["overview.md"], "docs", "architecture") + assert result == [{"path": "docs/architecture/overview.md", "notes": ""}] + + def test_plain_string_without_dir_is_verbatim(self) -> None: + """Test that plain string items are used verbatim when no dir is set.""" + result = AgentGenerator._resolve_output_entries(["src/**/*"], "docs", "") + assert result == [{"path": "src/**/*", "notes": ""}] + + def test_non_string_non_dict_item_is_skipped(self) -> None: + """Test that non-string, non-dict output items are silently skipped.""" + result = AgentGenerator._resolve_output_entries([42, None], "docs", "architecture") + assert result == [] + + def test_dotslash_prefix_strips_and_uses_verbatim(self) -> None: + """Test that ./path items strip the ./ and bypass dir prefix.""" + result = AgentGenerator._resolve_output_entries(["./tests/**/*"], "docs", "reports") + assert result == [{"path": "tests/**/*", "notes": ""}] + + def test_dict_item_with_notes_and_dir(self) -> None: + """Test that dict items with notes are resolved with dir prefix.""" + result = AgentGenerator._resolve_output_entries( + [{"path": "ux.md", "notes": "frontend only"}], "docs", "design" + ) + assert result == [{"path": "docs/design/ux.md", "notes": "frontend only"}] + + def test_dict_item_with_dotslash_path(self) -> None: + """Test that dict items with ./path bypass the dir prefix.""" + result = AgentGenerator._resolve_output_entries( + [{"path": "./issues/rca.md", "notes": "on issue"}], "docs", "reports" + ) + assert result == [{"path": "issues/rca.md", "notes": "on issue"}] + + def test_glob_with_slash_is_relative_to_dir(self) -> None: + """Test that glob paths like adr/*.md are prefixed with root/dir.""" + result = AgentGenerator._resolve_output_entries(["adr/*.md"], "docs", "architecture") + assert result == [{"path": "docs/architecture/adr/*.md", "notes": ""}] + + def test_double_glob_relative_to_dir(self) -> None: + """Test that **/*.md is prefixed with root/dir when dir is set.""" + result = AgentGenerator._resolve_output_entries(["**/*.md"], "docs", "reports") + assert result == [{"path": "docs/reports/**/*.md", "notes": ""}] + + +class TestBuildTable: + """Unit tests for AgentGenerator._build_table.""" + + def test_single_column_when_no_notes(self) -> None: + """Test that a single-column table is produced when no entry has notes.""" + table = AgentGenerator._build_table([{"path": "docs/foo.md", "notes": ""}]) + lines = table.splitlines() + assert lines[0].startswith("| Artifact") + assert lines[0].count("|") == 2 + assert "`docs/foo.md`" in lines[2] + + def test_two_column_when_any_entry_has_notes(self) -> None: + """Test that a two-column table is produced when at least one entry has notes.""" + entries = [ + {"path": "docs/foo.md", "notes": ""}, + {"path": "docs/bar.md", "notes": "important"}, + ] + table = AgentGenerator._build_table(entries) + assert "Notes" in table.splitlines()[0] + + def test_rows_have_equal_length(self) -> None: + """Test that all rows in the table have equal string length.""" + entries = [ + {"path": "docs/a.md", "notes": ""}, + {"path": "docs/b.md", "notes": "a very long note here"}, + ] + table = AgentGenerator._build_table(entries) + row_lengths = {len(line) for line in table.splitlines()} + assert len(row_lengths) == 1 + + +class TestBuildSection: + """Unit tests for AgentGenerator._build_section.""" + + def test_returns_empty_string_when_no_entries(self) -> None: + """Test that an empty string is returned when entries list is empty.""" + assert AgentGenerator._build_section("input", []) == "" + + def test_includes_heading_and_table(self) -> None: + """Test that the section includes the heading and table.""" + section = AgentGenerator._build_section("input", [{"path": "docs/a.md", "notes": ""}]) + assert section.startswith("### input") + assert "`docs/a.md`" in section + + def test_heading_matches_argument(self) -> None: + """Test that the section heading matches the heading argument.""" + section = AgentGenerator._build_section("output", [{"path": "docs/b.md", "notes": ""}]) + assert "### output" in section diff --git a/tests/vstack/agents/test_role_wiring.py b/tests/vstack/agents/test_role_wiring.py index 34a7b4c..eb74abd 100644 --- a/tests/vstack/agents/test_role_wiring.py +++ b/tests/vstack/agents/test_role_wiring.py @@ -49,15 +49,18 @@ def test_all_role_templates_reference_concise() -> None: assert "@#concise" in content -def test_all_role_configs_define_handoffs_block() -> None: - """Each role config should include at least one handoff entry.""" - roles = ["product", "architect", "designer", "engineer", "tester", "release"] - for role in roles: +def test_role_configs_follow_stage_handoff_policy() -> None: + """Non-release roles expose forward handoffs, release remains terminal.""" + roles_with_forward_handoff = ["product", "architect", "designer", "engineer", "tester"] + for role in roles_with_forward_handoff: config = _read(f"{role}/config.yaml") assert "handoffs:" in config assert "label:" in config assert "agent:" in config + release_config = _read("release/config.yaml") + assert "handoffs:" not in release_config + def test_all_role_handoff_targets_are_known_roles() -> None: """Each handoff target should reference one of the known role agents.""" diff --git a/tests/vstack/agents/test_template_structure.py b/tests/vstack/agents/test_template_structure.py index dcd9b77..d40ab49 100644 --- a/tests/vstack/agents/test_template_structure.py +++ b/tests/vstack/agents/test_template_structure.py @@ -17,7 +17,7 @@ "## workflow and handoffs", "## success criteria", "## failure and escalation rules", - "## artifacts you own", + "## artifacts you use", "## completion checklist", "## skills you use", ] diff --git a/tests/vstack/artifacts/test_generator.py b/tests/vstack/artifacts/test_generator.py index 20ea21a..ae57576 100644 --- a/tests/vstack/artifacts/test_generator.py +++ b/tests/vstack/artifacts/test_generator.py @@ -460,3 +460,28 @@ def test_verify_output_reports_unresolved_placeholders(self, tmp_path: Path) -> assert any( "unresolved placeholders" in m.message for m in result.messages if m.level == "fail" ) + + def test_template_partials_returns_empty_dict_by_default(self, tmp_path: Path) -> None: + """Test that template_partials returns an empty dict in the base class.""" + gen = self._make_skill_gen(tmp_path) + assert gen.template_partials(tmp_path / "any" / "dir") == {} + + def test_render_merges_template_partials_into_resolution(self, tmp_path: Path) -> None: + """Test that render uses template_partials to resolve per-template tokens.""" + + class _CustomGen(GenericArtifactGenerator): + def template_partials(self, tmpl_dir: Path) -> dict[str, str]: + return {"CUSTOM_TOKEN": "injected-value"} + + (tmp_path / "templates" / "skills" / "_partials").mkdir(parents=True) + tmpl_dir = tmp_path / "templates" / "skills" / "custom" + tmpl_dir.mkdir(parents=True) + (tmpl_dir / "template.md").write_text( + "---\nname: custom\nversion: 1.0.0\ndescription: x\n---\n{{CUSTOM_TOKEN}}\n", + encoding="utf-8", + ) + + gen = _CustomGen(SKILL_TYPE, tmp_path / "templates") + artifact = gen.render(tmpl_dir) + assert "injected-value" in artifact.content + assert artifact.unresolved == [] diff --git a/tests/vstack/cli/test_catalog.py b/tests/vstack/cli/test_catalog.py index 0968f7e..20b8bf7 100644 --- a/tests/vstack/cli/test_catalog.py +++ b/tests/vstack/cli/test_catalog.py @@ -26,6 +26,7 @@ def test_catalog_contains_expected_commands(self) -> None: "status", "manifest", "install", + "init", "uninstall", } diff --git a/tests/vstack/cli/test_constants.py b/tests/vstack/cli/test_constants.py index 754f1c2..a3d07f3 100644 --- a/tests/vstack/cli/test_constants.py +++ b/tests/vstack/cli/test_constants.py @@ -35,6 +35,8 @@ "onboard", "dependency", "incident", + "rca", + "postmortem", "gh-issues", "codeql", "dependabot", diff --git a/tests/vstack/cli/test_init.py b/tests/vstack/cli/test_init.py new file mode 100644 index 0000000..acf1729 --- /dev/null +++ b/tests/vstack/cli/test_init.py @@ -0,0 +1,643 @@ +"""Tests for InitCommand.""" + +from __future__ import annotations + +from argparse import Namespace +from pathlib import Path +from types import SimpleNamespace +from typing import Any, cast + +import pytest + +from vstack.cli.base import CommandContext +from vstack.cli.init import InitCommand +from vstack.cli.service import CommandService +from vstack.manifest import content_hash + + +class TestInitCommand: + """Test cases for InitCommand.""" + + # ------------------------------------------------------------------ + # _version_gt + # ------------------------------------------------------------------ + + def test_version_gt_true_for_higher(self) -> None: + """Higher dotted numeric revision is strictly greater.""" + assert InitCommand._version_gt("1.2.0", "1.1.9") + + def test_version_gt_false_for_equal(self) -> None: + """Equal versions are not greater.""" + assert not InitCommand._version_gt("1.2.0", "1.2.0") + + def test_version_gt_handles_invalid(self) -> None: + """Non-numeric strings are treated as (0,) and not greater than a real revision.""" + assert not InitCommand._version_gt("abc", "1.0.0") + + def test_version_gt_handles_none_existing(self) -> None: + """None for existing falls back to (0,) so any real version is greater.""" + assert InitCommand._version_gt("1.2.0", None) is True + + def test_version_gt_true_for_date_revision(self) -> None: + """Higher date-based revision is strictly greater.""" + assert InitCommand._version_gt("20260502012", "20260502011") + + def test_version_gt_false_for_same_date_revision(self) -> None: + """Equal date-based revisions are not greater.""" + assert not InitCommand._version_gt("20260502012", "20260502012") + + def test_version_gt_date_revision_gt_legacy_dotted(self) -> None: + """YYYYMMDDNNN token is greater than a legacy dotted version from an existing manifest. + + This is the real upgrade path: a freshly installed repo may have artifacts + versioned as e.g. 1.2.0 (legacy) and the new template uses 20260502012. + The comparison must return True so the artifact is upgraded, not skipped. + """ + assert InitCommand._version_gt("20260502012", "1.2.0") + + def test_version_gt_date_revision_gt_legacy_dotted_high_patch(self) -> None: + """YYYYMMDDNNN token is greater than a legacy high-patch dotted version.""" + assert InitCommand._version_gt("20260421001", "9.99.999") + + def test_version_gt_legacy_dotted_not_gt_date_revision(self) -> None: + """Legacy dotted version is never greater than a YYYYMMDDNNN token.""" + assert not InitCommand._version_gt("1.2.0", "20260502012") + + # ------------------------------------------------------------------ + # _installed_content_matches + # ------------------------------------------------------------------ + + def test_installed_content_matches_returns_none_for_unknown_algorithm( + self, tmp_path: Path + ) -> None: + """Unknown checksum algorithm is treated as indeterminate (None).""" + out_file = tmp_path / "artifact.txt" + out_file.write_text("content", encoding="utf-8") + entry = SimpleNamespace(checksum="x", checksum_algorithm="sha999") + assert ( + InitCommand._installed_content_matches(out_file=out_file, existing_entry=entry) is None + ) + + # ------------------------------------------------------------------ + # _install_decision + # ------------------------------------------------------------------ + + def test_decision_preserves_when_tracked_file_has_no_checksum(self, tmp_path: Path) -> None: + """Tracked file without stored checksum is always preserved.""" + out_file = tmp_path / "artifact.txt" + out_file.write_text("content", encoding="utf-8") + entry = SimpleNamespace(checksum=None, checksum_algorithm="sha256", version="1.0.0") + action, reason = InitCommand._install_decision( + force=False, + force_name=False, + adopt_name=False, + update=False, + out_file=out_file, + existing_entry=entry, + new_version="1.0.1", + ) + assert action == "preserve" + assert reason == "tracked file has no stored checksum" + + def test_decision_preserves_when_version_unknown_under_update(self, tmp_path: Path) -> None: + """Update mode preserves tracked files that have no stored version metadata.""" + out_file = tmp_path / "artifact.txt" + out_file.write_text("content", encoding="utf-8") + entry = SimpleNamespace( + checksum=content_hash("content"), + checksum_algorithm="sha256", + version=None, + ) + action, reason = InitCommand._install_decision( + force=False, + force_name=False, + adopt_name=False, + update=True, + out_file=out_file, + existing_entry=entry, + new_version="1.0.1", + ) + assert action == "preserve" + assert reason == "tracked file has no stored version" + + # ------------------------------------------------------------------ + # _load_existing_manifest + # ------------------------------------------------------------------ + + def test_load_existing_manifest_returns_none_tuple_on_read_error( + self, + capsys: pytest.CaptureFixture[str], + ) -> None: + """Returns a 4-tuple of None when the manifest file has a read error.""" + + class _ManifestFile: + read_error = "bad manifest" + + def read(self): + return None + + class _Service: + @staticmethod + def manifest_for(_install_dir: Path) -> _ManifestFile: + return _ManifestFile() + + result = InitCommand._load_existing_manifest( + service=cast(CommandService, _Service()), + install_dir=Path("/tmp/install"), + gens=[], + ) + assert result == (None, None, None, None) + assert "ERROR: bad manifest" in capsys.readouterr().err + + # ------------------------------------------------------------------ + # execute + # ------------------------------------------------------------------ + + def test_execute_returns_nonzero_when_manifest_loading_fails( + self, monkeypatch: pytest.MonkeyPatch + ) -> None: + """execute returns non-zero immediately when manifest loading returns None tuple.""" + monkeypatch.setattr( + "vstack.cli.init.InitCommand._load_existing_manifest", + staticmethod(lambda **_kwargs: (None, None, None, None)), + ) + service = cast(CommandService, SimpleNamespace(generators=[])) + assert InitCommand.execute(service, Path("/tmp/install")) == 1 + + def test_adopt_records_version_from_disk_metadata(self, tmp_path: Path) -> None: + """Adopted files should use on-disk artifact_version metadata, not new template version.""" + + class _Gen: + config = SimpleNamespace( + type_name="skill", + manifest_key="skills", + output_subdir="skills", + ) + + @staticmethod + def output_path(name: str) -> Path: + return Path(name) / "SKILL.md" + + @staticmethod + def install_relative_path(name: str) -> str: + return f"skills/{name}/SKILL.md" + + existing_content = ( + "# Skill\n" + "<!-- AUTO-GENERATED -->" + "<!-- VSTACK-META: " + '{"artifact_version":"1.2.3","artifact_name":"verify"}' + " -->\n" + ) + out_dir = tmp_path / "skills" + out_file = out_dir / "verify" / "SKILL.md" + out_file.parent.mkdir(parents=True) + out_file.write_text(existing_content, encoding="utf-8") + + new_entries: dict[str, list[Any]] = {} + InitCommand._install_single_artifact( + service=cast(CommandService, SimpleNamespace(label=lambda path: str(path))), + gen=_Gen(), + artifact=SimpleNamespace( + name="verify", + frontmatter={"version": "9.9.9"}, + unresolved=[], + content="new content", + ), + out_dir=out_dir, + colors=SimpleNamespace( + CYAN="", + RESET="", + DIM="", + YELLOW="", + GREEN="", + BOLD="", + ), + prefix="", + force=False, + update=False, + dry_run=True, + targeted_force_names=set(), + targeted_adopt_names={"verify"}, + existing_entries={}, + new_entries=new_entries, + checksum_algorithm="sha256", + ) + + adopted_entry = cast(Any, new_entries["skills"][0]) + assert adopted_entry.version == "1.2.3" + assert adopted_entry.checksum == content_hash(existing_content) + + def test_adopt_unreadable_file_preserves_without_crashing( + self, + monkeypatch: pytest.MonkeyPatch, + tmp_path: Path, + capsys: pytest.CaptureFixture[str], + ) -> None: + """Unreadable adopt targets should be preserved and not crash init.""" + + class _Gen: + config = SimpleNamespace( + type_name="skill", + manifest_key="skills", + output_subdir="skills", + ) + + @staticmethod + def output_path(name: str) -> Path: + return Path(name) / "SKILL.md" + + @staticmethod + def install_relative_path(name: str) -> str: + return f"skills/{name}/SKILL.md" + + out_dir = tmp_path / "skills" + out_file = out_dir / "verify" / "SKILL.md" + out_file.parent.mkdir(parents=True) + out_file.write_text("content", encoding="utf-8") + + def _raise_oserror(self: Path, encoding: str = "utf-8") -> str: + del self, encoding + raise OSError("permission denied") + + monkeypatch.setattr(Path, "read_text", _raise_oserror) + + new_entries: dict[str, list[Any]] = {} + InitCommand._install_single_artifact( + service=cast(CommandService, SimpleNamespace(label=lambda path: str(path))), + gen=_Gen(), + artifact=SimpleNamespace( + name="verify", + frontmatter={"version": "9.9.9"}, + unresolved=[], + content="new content", + ), + out_dir=out_dir, + colors=SimpleNamespace( + CYAN="", + RESET="", + DIM="", + YELLOW="", + GREEN="", + BOLD="", + ), + prefix="", + force=False, + update=False, + dry_run=True, + targeted_force_names=set(), + targeted_adopt_names={"verify"}, + existing_entries={}, + new_entries=new_entries, + checksum_algorithm="sha256", + ) + + out = capsys.readouterr().out + assert ( + "preserved — existing file is unreadable; could not adopt into vstack manifest" in out + ) + assert "skills" not in new_entries + + # ------------------------------------------------------------------ + # _print_summary + # ------------------------------------------------------------------ + + def test_print_summary_no_conflicts_shows_installed_count( + self, + capsys: pytest.CaptureFixture[str], + ) -> None: + """Summary without preserves shows heading, fixed counters, and no guidance.""" + colors = SimpleNamespace(YELLOW="", RESET="", BOLD="", DIM="", GREEN="", CYAN="") + InitCommand._print_summary( + colors=colors, + action_counts={"install": 7, "update": 1}, + preserved_selectors=[], + dry_run=False, + ) + out = capsys.readouterr().out + assert "Summary" in out + assert "total processed : 8" in out + assert "installed" in out and ": 7" in out + assert "updated" in out and ": 1" in out + assert "preserved" in out and ": 0" in out + assert "skipped" in out and ": 0" in out + assert "adopted" in out and ": 0" in out + assert "--force" not in out + + def test_print_summary_with_conflicts_shows_guidance( + self, + capsys: pytest.CaptureFixture[str], + ) -> None: + """Summary with preserved files shows count, warning, and flag guidance.""" + colors = SimpleNamespace(YELLOW="", RESET="", BOLD="", DIM="", GREEN="", CYAN="") + InitCommand._print_summary( + colors=colors, + action_counts={"install": 3, "preserve": 2}, + preserved_selectors=["agent/engineer", "skill/verify"], + dry_run=False, + ) + out = capsys.readouterr().out + assert "Summary" in out + assert "⚠" in out + assert "preserved" in out and ": 2" in out + assert "2 files preserved" in out + assert "Preserved selectors:" in out + assert "Next steps:" in out + assert "--force" in out + assert "--force-name <name|type/name>" in out + assert "--adopt-name <name|type/name>" in out + assert "- agent/engineer" in out + assert "- skill/verify" in out + + def test_print_summary_single_preserve_uses_singular_noun( + self, + capsys: pytest.CaptureFixture[str], + ) -> None: + """A single preserved file uses the singular 'file' noun.""" + colors = SimpleNamespace(YELLOW="", RESET="", BOLD="", DIM="", GREEN="", CYAN="") + InitCommand._print_summary( + colors=colors, + action_counts={"install": 1, "preserve": 1}, + preserved_selectors=["agent/engineer"], + dry_run=False, + ) + out = capsys.readouterr().out + assert "1 file preserved" in out + + def test_print_summary_dry_run_marks_header_and_keeps_installed_label( + self, + capsys: pytest.CaptureFixture[str], + ) -> None: + """Dry-run mode marks the summary header and keeps action labels consistent.""" + colors = SimpleNamespace(YELLOW="", RESET="", BOLD="", DIM="", GREEN="", CYAN="") + InitCommand._print_summary( + colors=colors, + action_counts={"install": 10}, + preserved_selectors=[], + dry_run=True, + ) + out = capsys.readouterr().out + assert "Summary (dry-run)" in out + assert "installed" in out and ": 10" in out + assert "would install" not in out + + def test_print_summary_shows_optional_counts_when_nonzero( + self, + capsys: pytest.CaptureFixture[str], + ) -> None: + """Skipped and adopted counters are rendered with their non-zero values.""" + colors = SimpleNamespace(YELLOW="", RESET="", BOLD="", DIM="", GREEN="", CYAN="") + InitCommand._print_summary( + colors=colors, + action_counts={"install": 2, "skip": 3, "adopt": 1}, + preserved_selectors=[], + dry_run=False, + ) + out = capsys.readouterr().out + assert "skipped" in out and ": 3" in out + assert "adopted" in out and ": 1" in out + + # ------------------------------------------------------------------ + # _install_single_artifact — return value + # ------------------------------------------------------------------ + + def test_install_single_artifact_returns_preserve_for_untracked_file( + self, + tmp_path: Path, + capsys: pytest.CaptureFixture[str], + ) -> None: + """Returns 'preserve' when an existing untracked file blocks install.""" + + class _Gen: + config = SimpleNamespace( + type_name="agent", + manifest_key="agents", + output_subdir="agents", + ) + + @staticmethod + def output_path(name: str) -> Path: + return Path(f"{name}.agent.md") + + @staticmethod + def install_relative_path(name: str) -> str: + return f"agents/{name}.agent.md" + + out_dir = tmp_path / "agents" + out_dir.mkdir() + (out_dir / "engineer.agent.md").write_text("existing", encoding="utf-8") + + colors = SimpleNamespace(CYAN="", RESET="", DIM="", YELLOW="", GREEN="", BOLD="") + result = InitCommand._install_single_artifact( + service=cast(CommandService, SimpleNamespace(label=lambda p: str(p))), + gen=_Gen(), + artifact=SimpleNamespace( + name="engineer", + frontmatter={"version": "1.0.0"}, + unresolved=[], + content="new content", + ), + out_dir=out_dir, + colors=colors, + prefix="", + force=False, + update=False, + dry_run=True, + targeted_force_names=set(), + targeted_adopt_names=set(), + existing_entries={}, + new_entries={}, + checksum_algorithm="sha256", + ) + assert result == "preserve" + capsys.readouterr() + + def test_install_single_artifact_returns_adopt_when_adopting( + self, + tmp_path: Path, + capsys: pytest.CaptureFixture[str], + ) -> None: + """Returns 'adopt' when taking ownership of an existing untracked file.""" + + class _Gen: + config = SimpleNamespace( + type_name="agent", + manifest_key="agents", + output_subdir="agents", + ) + + @staticmethod + def output_path(name: str) -> Path: + return Path(f"{name}.agent.md") + + @staticmethod + def install_relative_path(name: str) -> str: + return f"agents/{name}.agent.md" + + out_dir = tmp_path / "agents" + out_dir.mkdir() + (out_dir / "engineer.agent.md").write_text("existing", encoding="utf-8") + + new_entries: dict[str, list[Any]] = {} + colors = SimpleNamespace(CYAN="", RESET="", DIM="", YELLOW="", GREEN="", BOLD="") + result = InitCommand._install_single_artifact( + service=cast(CommandService, SimpleNamespace(label=lambda p: str(p))), + gen=_Gen(), + artifact=SimpleNamespace( + name="engineer", + frontmatter={"version": "1.0.0"}, + unresolved=[], + content="new content", + ), + out_dir=out_dir, + colors=colors, + prefix="", + force=False, + update=False, + dry_run=True, + targeted_force_names=set(), + targeted_adopt_names={"engineer"}, + existing_entries={}, + new_entries=new_entries, + checksum_algorithm="sha256", + ) + assert result == "adopt" + capsys.readouterr() + + # ------------------------------------------------------------------ + # run + # ------------------------------------------------------------------ + + def test_run_forwards_context_to_execute( + self, monkeypatch: pytest.MonkeyPatch, tmp_path: Path + ) -> None: + """run() unpacks CommandContext args and forwards them to execute().""" + captured: dict[str, Any] = {} + + def _fake_execute(*args, **kwargs): + captured["args"] = args + captured["kwargs"] = kwargs + return 0 + + monkeypatch.setattr("vstack.cli.init.InitCommand.execute", staticmethod(_fake_execute)) + + context = CommandContext( + args=Namespace( + force=True, force_names=["a"], adopt_name=["b"], update=True, dry_run=True + ), + install_dir=tmp_path, + only=["skill"], + ) + result = InitCommand(service=cast(CommandService, object())).run(context=context) + + assert result == 0 + assert captured["kwargs"]["force"] is True + assert captured["kwargs"]["force_names"] == ["a"] + assert captured["kwargs"]["adopt_names"] == ["b"] + assert captured["kwargs"]["update"] is True + assert captured["kwargs"]["dry_run"] is True + assert captured["kwargs"]["only"] == ["skill"] + + def test_run_raises_when_install_dir_missing(self) -> None: + """run() raises ValueError when install_dir is None.""" + context = CommandContext( + args=Namespace(), + install_dir=None, + only=None, + ) + with pytest.raises(ValueError, match="init requires install_dir"): + InitCommand(service=cast(CommandService, object())).run(context=context) + + def test_run_forwards_excluded_names_to_execute( + self, monkeypatch: pytest.MonkeyPatch, tmp_path: Path + ) -> None: + """run() passes context.excluded_names through to execute().""" + captured: dict[str, Any] = {} + + def _fake_execute(*args, **kwargs): + captured["kwargs"] = kwargs + return 0 + + monkeypatch.setattr("vstack.cli.init.InitCommand.execute", staticmethod(_fake_execute)) + + context = CommandContext( + args=Namespace( + force=False, force_names=None, adopt_name=None, update=False, dry_run=False + ), + install_dir=tmp_path, + only=None, + excluded_names={"skill": ["terraform"]}, + ) + InitCommand(service=cast(CommandService, object())).run(context=context) + + assert captured["kwargs"]["excluded_names"] == {"skill": ["terraform"]} + + def test_execute_skips_artifact_in_excluded_names( + self, monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str], tmp_path: Path + ) -> None: + """execute() skips artifacts whose name is in excluded_names, prints message.""" + install_single_calls: list[str] = [] + + def _fake_install_single(**kwargs): + install_single_calls.append(kwargs["artifact"].name) + return "install" + + monkeypatch.setattr( + "vstack.cli.init.InitCommand._install_single_artifact", + staticmethod(_fake_install_single), + ) + monkeypatch.setattr( + "vstack.cli.init.InitCommand._write_manifest", + staticmethod(lambda **_kwargs: None), + ) + monkeypatch.setattr( + "vstack.cli.init.InitCommand._print_summary", + staticmethod(lambda **_kwargs: None), + ) + + class _FakeGen: + config = SimpleNamespace( + type_name="skill", + manifest_key="skills", + output_subdir="skills", + ) + + @staticmethod + def render_all(): + return [ + SimpleNamespace(name="terraform", frontmatter={}, unresolved=[], content=""), + SimpleNamespace(name="k8s", frontmatter={}, unresolved=[], content=""), + ] + + @staticmethod + def install_relative_path(name: str) -> str: + return f"skills/{name}/SKILL.md" + + @staticmethod + def verify_input(): + return SimpleNamespace(messages=[]) + + service = cast( + CommandService, + SimpleNamespace( + generators=[_FakeGen()], + label=lambda path: str(path), + manifest_for=lambda _: SimpleNamespace(read=lambda: None, read_error=None), + ), + ) + monkeypatch.setattr( + "vstack.cli.init.InitCommand._load_existing_manifest", + staticmethod(lambda **_kwargs: (object(), object(), {}, {})), + ) + + result = InitCommand.execute( + service, + tmp_path, + excluded_names={"skill": ["terraform"]}, + ) + + assert result == 0 + assert "terraform" not in install_single_calls + assert "k8s" in install_single_calls + out = capsys.readouterr().out + assert "excluded by config" in out diff --git a/tests/vstack/cli/test_install.py b/tests/vstack/cli/test_install.py index 6dac9d3..d25aae2 100644 --- a/tests/vstack/cli/test_install.py +++ b/tests/vstack/cli/test_install.py @@ -1,4 +1,4 @@ -"""Tests for InstallCommand.""" +"""Tests for InstallCommand — first-run project setup wizard.""" from __future__ import annotations @@ -11,527 +11,433 @@ from vstack.cli.base import CommandContext from vstack.cli.install import InstallCommand from vstack.cli.service import CommandService -from vstack.manifest import content_hash class TestInstallCommand: - """Test cases for InstallCommand.""" + """Test cases for InstallCommand wizard behavior.""" # ------------------------------------------------------------------ - # _version_gt + # run (wizard behaviour) # ------------------------------------------------------------------ - def test_version_gt_true_for_higher(self) -> None: - """Higher dotted numeric revision is strictly greater.""" - assert InstallCommand._version_gt("1.2.0", "1.1.9") - - def test_version_gt_false_for_equal(self) -> None: - """Equal versions are not greater.""" - assert not InstallCommand._version_gt("1.2.0", "1.2.0") - - def test_version_gt_handles_invalid(self) -> None: - """Non-numeric strings are treated as (0,) and not greater than a real revision.""" - assert not InstallCommand._version_gt("abc", "1.0.0") - - def test_version_gt_handles_none_existing(self) -> None: - """None for existing falls back to (0,) so any real version is greater.""" - assert InstallCommand._version_gt("1.2.0", None) is True - - def test_version_gt_true_for_date_revision(self) -> None: - """Higher date-based revision is strictly greater.""" - assert InstallCommand._version_gt("20260502012", "20260502011") - - def test_version_gt_false_for_same_date_revision(self) -> None: - """Equal date-based revisions are not greater.""" - assert not InstallCommand._version_gt("20260502012", "20260502012") - - def test_version_gt_date_revision_gt_legacy_dotted(self) -> None: - """YYYYMMDDNNN token is greater than a legacy dotted version from an existing manifest. - - This is the real upgrade path: a freshly installed repo may have artifacts - versioned as e.g. 1.2.0 (legacy) and the new template uses 20260502012. - The comparison must return True so the artifact is upgraded, not skipped. - """ - assert InstallCommand._version_gt("20260502012", "1.2.0") - - def test_version_gt_date_revision_gt_legacy_dotted_high_patch(self) -> None: - """YYYYMMDDNNN token is greater than a legacy high-patch dotted version.""" - assert InstallCommand._version_gt("20260421001", "9.99.999") - - def test_version_gt_legacy_dotted_not_gt_date_revision(self) -> None: - """Legacy dotted version is never greater than a YYYYMMDDNNN token.""" - assert not InstallCommand._version_gt("1.2.0", "20260502012") - - # ------------------------------------------------------------------ - # _installed_content_matches - # ------------------------------------------------------------------ - - def test_installed_content_matches_returns_none_for_unknown_algorithm( - self, tmp_path: Path + def test_run_seeds_project_and_calls_init_execute_for_local( + self, monkeypatch: pytest.MonkeyPatch, tmp_path: Path ) -> None: - """Unknown checksum algorithm is treated as indeterminate (None).""" - out_file = tmp_path / "artifact.txt" - out_file.write_text("content", encoding="utf-8") - entry = SimpleNamespace(checksum="x", checksum_algorithm="sha999") - assert ( - InstallCommand._installed_content_matches(out_file=out_file, existing_entry=entry) - is None - ) + """run() seeds project files and calls InitCommand.execute() for local installs.""" + seeded: list[dict[str, Any]] = [] + executed: list[dict[str, Any]] = [] - # ------------------------------------------------------------------ - # _install_decision - # ------------------------------------------------------------------ + def _fake_seed(**kw: Any) -> None: + seeded.append(kw) - def test_decision_preserves_when_tracked_file_has_no_checksum(self, tmp_path: Path) -> None: - """Tracked file without stored checksum is always preserved.""" - out_file = tmp_path / "artifact.txt" - out_file.write_text("content", encoding="utf-8") - entry = SimpleNamespace(checksum=None, checksum_algorithm="sha256", version="1.0.0") - action, reason = InstallCommand._install_decision( - force=False, - force_name=False, - adopt_name=False, - update=False, - out_file=out_file, - existing_entry=entry, - new_version="1.0.1", - ) - assert action == "preserve" - assert reason == "tracked file has no stored checksum" - - def test_decision_preserves_when_version_unknown_under_update(self, tmp_path: Path) -> None: - """Update mode preserves tracked files that have no stored version metadata.""" - out_file = tmp_path / "artifact.txt" - out_file.write_text("content", encoding="utf-8") - entry = SimpleNamespace( - checksum=content_hash("content"), - checksum_algorithm="sha256", - version=None, + def _fake_execute(*_a: Any, **kw: Any) -> int: + executed.append(kw) + return 0 + + monkeypatch.setattr( + "vstack.cli.install.InstallCommand._seed_project", + staticmethod(_fake_seed), ) - action, reason = InstallCommand._install_decision( - force=False, - force_name=False, - adopt_name=False, - update=True, - out_file=out_file, - existing_entry=entry, - new_version="1.0.1", + monkeypatch.setattr( + "vstack.cli.init.InitCommand.execute", + staticmethod(_fake_execute), ) - assert action == "preserve" - assert reason == "tracked file has no stored version" - - # ------------------------------------------------------------------ - # _load_existing_manifest - # ------------------------------------------------------------------ - - def test_load_existing_manifest_returns_none_tuple_on_read_error( - self, - capsys: pytest.CaptureFixture[str], - ) -> None: - """Returns a 4-tuple of None when the manifest file has a read error.""" - class _ManifestFile: - read_error = "bad manifest" - - def read(self): - return None - - class _Service: - @staticmethod - def manifest_for(_install_dir: Path) -> _ManifestFile: - return _ManifestFile() + from argparse import Namespace - result = InstallCommand._load_existing_manifest( - service=cast(CommandService, _Service()), - install_dir=Path("/tmp/install"), - gens=[], + install_dir = tmp_path / ".github" + fake_service = SimpleNamespace(root=tmp_path / "templates") + context = CommandContext( + args=Namespace( + force=True, + force_names=["a"], + adopt_name=["b"], + update=True, + dry_run=False, + use_global=False, + ), + install_dir=install_dir, + only=["skill"], ) - assert result == (None, None, None, None) - assert "ERROR: bad manifest" in capsys.readouterr().err - - # ------------------------------------------------------------------ - # execute - # ------------------------------------------------------------------ - - def test_execute_returns_nonzero_when_manifest_loading_fails( - self, monkeypatch: pytest.MonkeyPatch + result = InstallCommand(service=cast(CommandService, fake_service)).run(context=context) + assert result == 0 + assert len(seeded) == 1 + assert seeded[0]["project_root"] == tmp_path + assert len(executed) == 1 + assert executed[0]["force"] is True + assert executed[0]["adopt_names"] == ["b"] + + def test_run_skips_seeding_for_global_install( + self, monkeypatch: pytest.MonkeyPatch, tmp_path: Path ) -> None: - """execute returns non-zero immediately when manifest loading returns None tuple.""" + """run() skips seeding when --global is active.""" + seeded: list[dict[str, Any]] = [] + monkeypatch.setattr( - "vstack.cli.install.InstallCommand._load_existing_manifest", - staticmethod(lambda **_kwargs: (None, None, None, None)), - ) - service = cast(CommandService, SimpleNamespace(generators=[])) - assert InstallCommand.execute(service, Path("/tmp/install")) == 1 - - def test_adopt_records_version_from_disk_metadata(self, tmp_path: Path) -> None: - """Adopted files should use on-disk artifact_version metadata, not new template version.""" - - class _Gen: - config = SimpleNamespace( - type_name="skill", - manifest_key="skills", - output_subdir="skills", - ) - - @staticmethod - def output_path(name: str) -> Path: - return Path(name) / "SKILL.md" - - @staticmethod - def install_relative_path(name: str) -> str: - return f"skills/{name}/SKILL.md" - - existing_content = ( - "# Skill\n" - "<!-- AUTO-GENERATED -->" - "<!-- VSTACK-META: " - '{"artifact_version":"1.2.3","artifact_name":"verify"}' - " -->\n" - ) - out_dir = tmp_path / "skills" - out_file = out_dir / "verify" / "SKILL.md" - out_file.parent.mkdir(parents=True) - out_file.write_text(existing_content, encoding="utf-8") - - new_entries: dict[str, list[Any]] = {} - InstallCommand._install_single_artifact( - service=cast(CommandService, SimpleNamespace(label=lambda path: str(path))), - gen=_Gen(), - artifact=SimpleNamespace( - name="verify", - frontmatter={"version": "9.9.9"}, - unresolved=[], - content="new content", - ), - out_dir=out_dir, - colors=SimpleNamespace( - CYAN="", - RESET="", - DIM="", - YELLOW="", - GREEN="", - BOLD="", - ), - prefix="", - force=False, - update=False, - dry_run=True, - targeted_force_names=set(), - targeted_adopt_names={"verify"}, - existing_entries={}, - new_entries=new_entries, - checksum_algorithm="sha256", + "vstack.cli.install.InstallCommand._seed_project", + staticmethod(lambda **kw: seeded.append(kw)), ) + monkeypatch.setattr("vstack.cli.init.InitCommand.execute", staticmethod(lambda *a, **kw: 0)) - adopted_entry = cast(Any, new_entries["skills"][0]) - assert adopted_entry.version == "1.2.3" - assert adopted_entry.checksum == content_hash(existing_content) + from argparse import Namespace - def test_adopt_unreadable_file_preserves_without_crashing( - self, - monkeypatch: pytest.MonkeyPatch, - tmp_path: Path, - capsys: pytest.CaptureFixture[str], - ) -> None: - """Unreadable adopt targets should be preserved and not crash install.""" - - class _Gen: - config = SimpleNamespace( - type_name="skill", - manifest_key="skills", - output_subdir="skills", - ) - - @staticmethod - def output_path(name: str) -> Path: - return Path(name) / "SKILL.md" - - @staticmethod - def install_relative_path(name: str) -> str: - return f"skills/{name}/SKILL.md" - - out_dir = tmp_path / "skills" - out_file = out_dir / "verify" / "SKILL.md" - out_file.parent.mkdir(parents=True) - out_file.write_text("content", encoding="utf-8") - - def _raise_oserror(self: Path, encoding: str = "utf-8") -> str: - del self, encoding - raise OSError("permission denied") - - monkeypatch.setattr(Path, "read_text", _raise_oserror) - - new_entries: dict[str, list[Any]] = {} - InstallCommand._install_single_artifact( - service=cast(CommandService, SimpleNamespace(label=lambda path: str(path))), - gen=_Gen(), - artifact=SimpleNamespace( - name="verify", - frontmatter={"version": "9.9.9"}, - unresolved=[], - content="new content", - ), - out_dir=out_dir, - colors=SimpleNamespace( - CYAN="", - RESET="", - DIM="", - YELLOW="", - GREEN="", - BOLD="", + context = CommandContext( + args=Namespace( + force=False, + force_names=None, + adopt_name=None, + update=False, + dry_run=False, + use_global=True, ), - prefix="", - force=False, - update=False, - dry_run=True, - targeted_force_names=set(), - targeted_adopt_names={"verify"}, - existing_entries={}, - new_entries=new_entries, - checksum_algorithm="sha256", + install_dir=tmp_path / ".github", + only=None, ) + InstallCommand(service=cast(CommandService, object())).run(context=context) + assert seeded == [] - out = capsys.readouterr().out - assert ( - "preserved — existing file is unreadable; could not adopt into vstack manifest" in out - ) - assert "skills" not in new_entries - - # ------------------------------------------------------------------ - # run (context forwarding) - # ------------------------------------------------------------------ - - def test_run_forwards_context_to_execute( + def test_run_forwards_execute_kwargs( self, monkeypatch: pytest.MonkeyPatch, tmp_path: Path ) -> None: - """run() unpacks CommandContext args and forwards them to execute().""" + """run() passes all context args through to InitCommand.execute().""" captured: dict[str, Any] = {} + monkeypatch.setattr( + "vstack.cli.install.InstallCommand._seed_project", + staticmethod(lambda **kw: None), + ) + def _fake_execute(*args, **kwargs): captured["args"] = args captured["kwargs"] = kwargs return 1 - monkeypatch.setattr( - "vstack.cli.install.InstallCommand.execute", staticmethod(_fake_execute) - ) + monkeypatch.setattr("vstack.cli.init.InitCommand.execute", staticmethod(_fake_execute)) from argparse import Namespace + fake_service = SimpleNamespace(root=tmp_path / "templates") context = CommandContext( args=Namespace( - force=True, force_names=["a"], adopt_name=["b"], update=True, dry_run=True + force=True, + force_names=["a"], + adopt_name=["b"], + update=True, + dry_run=True, + use_global=False, ), install_dir=tmp_path, only=["skill"], ) - assert InstallCommand(service=cast(CommandService, object())).run(context=context) == 1 + assert InstallCommand(service=cast(CommandService, fake_service)).run(context=context) == 1 assert captured["kwargs"]["adopt_names"] == ["b"] assert captured["kwargs"]["force"] is True + def test_run_forwards_excluded_names_to_execute( + self, monkeypatch: pytest.MonkeyPatch, tmp_path: Path + ) -> None: + """run() passes context.excluded_names through to InitCommand.execute().""" + captured: dict[str, Any] = {} + + monkeypatch.setattr( + "vstack.cli.install.InstallCommand._seed_project", + staticmethod(lambda **kw: None), + ) + monkeypatch.setattr( + "vstack.cli.install.InstallCommand._write_vstack_gitignore", + staticmethod(lambda **kw: None), + ) + + def _fake_execute(*args, **kwargs): + captured["kwargs"] = kwargs + return 0 + + monkeypatch.setattr("vstack.cli.init.InitCommand.execute", staticmethod(_fake_execute)) + + from argparse import Namespace + + fake_service = SimpleNamespace(root=tmp_path / "templates") + context = CommandContext( + args=Namespace( + force=False, + force_names=None, + adopt_name=None, + update=False, + dry_run=False, + use_global=False, + ), + install_dir=tmp_path / ".github", + only=None, + excluded_names={"skill": ["helm", "terraform"]}, + ) + result = InstallCommand(service=cast(CommandService, fake_service)).run(context=context) + + assert result == 0 + assert captured["kwargs"]["excluded_names"] == {"skill": ["helm", "terraform"]} + # ------------------------------------------------------------------ - # _print_summary + # _seed_project # ------------------------------------------------------------------ - def test_print_summary_no_conflicts_shows_installed_count( - self, - capsys: pytest.CaptureFixture[str], + def test_seed_project_copies_new_files( + self, tmp_path: Path, capsys: pytest.CaptureFixture[str] ) -> None: - """Summary without preserves shows heading, fixed counters, and no guidance.""" - colors = SimpleNamespace(YELLOW="", RESET="", BOLD="", DIM="", GREEN="", CYAN="") - InstallCommand._print_summary( + """_seed_project writes files that do not yet exist in project_root.""" + templates_root = tmp_path / "templates" + seed_dir = templates_root / "project" / ".vstack" + seed_dir.mkdir(parents=True) + (seed_dir / "config.yaml").write_text("project:\n name: ''\n", encoding="utf-8") + + project_root = tmp_path / "project" + colors = SimpleNamespace(YELLOW="", RESET="", BOLD="", DIM="", GREEN="") + InstallCommand._seed_project( + project_root=project_root, + templates_root=templates_root, colors=colors, - action_counts={"install": 7, "update": 1}, - preserved_selectors=[], dry_run=False, ) - out = capsys.readouterr().out - assert "Summary" in out - assert "total processed : 8" in out - assert "installed" in out and ": 7" in out - assert "updated" in out and ": 1" in out - assert "preserved" in out and ": 0" in out - assert "skipped" in out and ": 0" in out - assert "adopted" in out and ": 0" in out - assert "--force" not in out - - def test_print_summary_with_conflicts_shows_guidance( - self, - capsys: pytest.CaptureFixture[str], + + dst = project_root / ".vstack" / "config.yaml" + assert dst.exists() + assert dst.read_text(encoding="utf-8") == "project:\n name: ''\n" + assert "seeded" in capsys.readouterr().out + + def test_seed_project_skips_existing_files( + self, tmp_path: Path, capsys: pytest.CaptureFixture[str] ) -> None: - """Summary with preserved files shows count, warning, and flag guidance.""" - colors = SimpleNamespace(YELLOW="", RESET="", BOLD="", DIM="", GREEN="", CYAN="") - InstallCommand._print_summary( + """_seed_project does not overwrite files that already exist.""" + templates_root = tmp_path / "templates" + seed_dir = templates_root / "project" / ".vstack" + seed_dir.mkdir(parents=True) + (seed_dir / "config.yaml").write_text("new content", encoding="utf-8") + + project_root = tmp_path / "project" + (project_root / ".vstack").mkdir(parents=True) + (project_root / ".vstack" / "config.yaml").write_text("existing", encoding="utf-8") + + colors = SimpleNamespace(YELLOW="", RESET="", BOLD="", DIM="", GREEN="") + InstallCommand._seed_project( + project_root=project_root, + templates_root=templates_root, colors=colors, - action_counts={"install": 3, "preserve": 2}, - preserved_selectors=["agent/engineer", "skill/verify"], dry_run=False, ) - out = capsys.readouterr().out - assert "Summary" in out - assert "⚠" in out - assert "preserved" in out and ": 2" in out - assert "2 files preserved" in out - assert "Preserved selectors:" in out - assert "Next steps:" in out - assert "--force" in out - assert "--force-name <name|type/name>" in out - assert "--adopt-name <name|type/name>" in out - assert "- agent/engineer" in out - assert "- skill/verify" in out - - def test_print_summary_single_preserve_uses_singular_noun( - self, - capsys: pytest.CaptureFixture[str], - ) -> None: - """A single preserved file uses the singular 'file' noun.""" - colors = SimpleNamespace(YELLOW="", RESET="", BOLD="", DIM="", GREEN="", CYAN="") - InstallCommand._print_summary( + + assert (project_root / ".vstack" / "config.yaml").read_text(encoding="utf-8") == "existing" + assert "skipped" in capsys.readouterr().out + + def test_seed_project_dry_run_does_not_write(self, tmp_path: Path) -> None: + """_seed_project in dry_run mode prints but does not write files.""" + templates_root = tmp_path / "templates" + seed_dir = templates_root / "project" / ".vstack" + seed_dir.mkdir(parents=True) + (seed_dir / "config.yaml").write_text("content", encoding="utf-8") + + project_root = tmp_path / "project" + colors = SimpleNamespace(YELLOW="", RESET="", BOLD="", DIM="", GREEN="") + InstallCommand._seed_project( + project_root=project_root, + templates_root=templates_root, + colors=colors, + dry_run=True, + ) + + assert not (project_root / ".vstack" / "config.yaml").exists() + + def test_seed_project_noop_when_no_project_templates(self, tmp_path: Path) -> None: + """_seed_project does nothing when neither project/ nor agents/ dirs exist.""" + colors = SimpleNamespace(YELLOW="", RESET="", BOLD="", DIM="", GREEN="") + project_root = tmp_path / "project" + InstallCommand._seed_project( + project_root=project_root, + templates_root=tmp_path / "nonexistent", colors=colors, - action_counts={"install": 1, "preserve": 1}, - preserved_selectors=["agent/engineer"], dry_run=False, ) - out = capsys.readouterr().out - assert "1 file preserved" in out + assert not project_root.exists() - def test_print_summary_dry_run_marks_header_and_keeps_installed_label( - self, - capsys: pytest.CaptureFixture[str], + def test_seed_project_seeds_agent_artifacts_to_vstack_templates( + self, tmp_path: Path, capsys: pytest.CaptureFixture[str] ) -> None: - """Dry-run mode marks the summary header and keeps action labels consistent.""" - colors = SimpleNamespace(YELLOW="", RESET="", BOLD="", DIM="", GREEN="", CYAN="") - InstallCommand._print_summary( + """_seed_project copies agent artifacts/ to .vstack/templates/{agent}/artifacts/.""" + templates_root = tmp_path / "templates" + artifact = templates_root / "agents" / "tester" / "artifacts" / "test-report.md" + artifact.parent.mkdir(parents=True) + artifact.write_text("# Test Report\n", encoding="utf-8") + + project_root = tmp_path / "project" + colors = SimpleNamespace(YELLOW="", RESET="", BOLD="", DIM="", GREEN="") + InstallCommand._seed_project( + project_root=project_root, + templates_root=templates_root, colors=colors, - action_counts={"install": 10}, - preserved_selectors=[], - dry_run=True, + dry_run=False, ) - out = capsys.readouterr().out - assert "Summary (dry-run)" in out - assert "installed" in out and ": 10" in out - assert "would install" not in out - - def test_print_summary_shows_optional_counts_when_nonzero( - self, - capsys: pytest.CaptureFixture[str], + + dst = project_root / ".vstack" / "templates" / "tester" / "artifacts" / "test-report.md" + assert dst.exists() + assert dst.read_text(encoding="utf-8") == "# Test Report\n" + assert "seeded" in capsys.readouterr().out + + def test_seed_project_skips_existing_agent_artifact( + self, tmp_path: Path, capsys: pytest.CaptureFixture[str] ) -> None: - """Skipped and adopted counters are rendered with their non-zero values.""" - colors = SimpleNamespace(YELLOW="", RESET="", BOLD="", DIM="", GREEN="", CYAN="") - InstallCommand._print_summary( + """_seed_project does not overwrite existing agent artifact files.""" + templates_root = tmp_path / "templates" + artifact = templates_root / "agents" / "tester" / "artifacts" / "test-report.md" + artifact.parent.mkdir(parents=True) + artifact.write_text("new content", encoding="utf-8") + + project_root = tmp_path / "project" + dst = project_root / ".vstack" / "templates" / "tester" / "artifacts" / "test-report.md" + dst.parent.mkdir(parents=True) + dst.write_text("existing", encoding="utf-8") + + colors = SimpleNamespace(YELLOW="", RESET="", BOLD="", DIM="", GREEN="") + InstallCommand._seed_project( + project_root=project_root, + templates_root=templates_root, + colors=colors, + dry_run=False, + ) + + assert dst.read_text(encoding="utf-8") == "existing" + assert "skipped" in capsys.readouterr().out + + def test_seed_project_skips_non_directory_entries_in_agents_root(self, tmp_path: Path) -> None: + """_seed_project ignores non-directory entries directly under agents/.""" + templates_root = tmp_path / "templates" + agents_root = templates_root / "agents" + agents_root.mkdir(parents=True) + # A file directly in agents/ — not a directory, must be skipped + (agents_root / "README.md").write_text("ignore me", encoding="utf-8") + + project_root = tmp_path / "project" + colors = SimpleNamespace(YELLOW="", RESET="", BOLD="", DIM="", GREEN="") + InstallCommand._seed_project( + project_root=project_root, + templates_root=templates_root, + colors=colors, + dry_run=False, + ) + assert not project_root.exists() + + def test_seed_project_skips_agent_dir_without_artifacts_subdir(self, tmp_path: Path) -> None: + """_seed_project skips agent directories that have no artifacts/ subdir.""" + templates_root = tmp_path / "templates" + agent_dir = templates_root / "agents" / "engineer" + agent_dir.mkdir(parents=True) + # No artifacts/ subdir — nothing should be seeded + (agent_dir / "config.yaml").write_text("name: engineer\n", encoding="utf-8") + + project_root = tmp_path / "project" + colors = SimpleNamespace(YELLOW="", RESET="", BOLD="", DIM="", GREEN="") + InstallCommand._seed_project( + project_root=project_root, + templates_root=templates_root, colors=colors, - action_counts={"install": 2, "skip": 3, "adopt": 1}, - preserved_selectors=[], dry_run=False, ) - out = capsys.readouterr().out - assert "skipped" in out and ": 3" in out - assert "adopted" in out and ": 1" in out + assert not project_root.exists() # ------------------------------------------------------------------ - # _install_single_artifact — return value + # _write_vstack_gitignore # ------------------------------------------------------------------ - def test_install_single_artifact_returns_preserve_for_untracked_file( - self, - tmp_path: Path, - capsys: pytest.CaptureFixture[str], + def test_write_vstack_gitignore_creates_file(self, tmp_path: Path) -> None: + """_write_vstack_gitignore writes .vstack/.gitignore with correct content.""" + InstallCommand._write_vstack_gitignore(project_root=tmp_path, dry_run=False) + + gitignore = tmp_path / ".vstack" / ".gitignore" + assert gitignore.exists() + content = gitignore.read_text(encoding="utf-8") + assert "tmp/\n" in content + assert "*\n" not in content + + def test_write_vstack_gitignore_overwrites_existing(self, tmp_path: Path) -> None: + """_write_vstack_gitignore replaces any existing .vstack/.gitignore.""" + vstack_dir = tmp_path / ".vstack" + vstack_dir.mkdir() + (vstack_dir / ".gitignore").write_text("old content", encoding="utf-8") + + InstallCommand._write_vstack_gitignore(project_root=tmp_path, dry_run=False) + + assert (vstack_dir / ".gitignore").read_text(encoding="utf-8") != "old content" + + def test_write_vstack_gitignore_dry_run_does_not_write(self, tmp_path: Path) -> None: + """_write_vstack_gitignore in dry_run mode does not create the file.""" + InstallCommand._write_vstack_gitignore(project_root=tmp_path, dry_run=True) + + assert not (tmp_path / ".vstack" / ".gitignore").exists() + + def test_write_vstack_gitignore_creates_parent_dirs(self, tmp_path: Path) -> None: + """_write_vstack_gitignore creates .vstack/ if it does not exist.""" + project_root = tmp_path / "new_project" + InstallCommand._write_vstack_gitignore(project_root=project_root, dry_run=False) + + assert (project_root / ".vstack" / ".gitignore").exists() + + def test_run_calls_write_vstack_gitignore_for_local( + self, monkeypatch: pytest.MonkeyPatch, tmp_path: Path ) -> None: - """Returns 'preserve' when an existing untracked file blocks install.""" - - class _Gen: - config = SimpleNamespace( - type_name="agent", - manifest_key="agents", - output_subdir="agents", - ) - - @staticmethod - def output_path(name: str) -> Path: - return Path(f"{name}.agent.md") - - @staticmethod - def install_relative_path(name: str) -> str: - return f"agents/{name}.agent.md" - - out_dir = tmp_path / "agents" - out_dir.mkdir() - (out_dir / "engineer.agent.md").write_text("existing", encoding="utf-8") - - colors = SimpleNamespace(CYAN="", RESET="", DIM="", YELLOW="", GREEN="", BOLD="") - result = InstallCommand._install_single_artifact( - service=cast(CommandService, SimpleNamespace(label=lambda p: str(p))), - gen=_Gen(), - artifact=SimpleNamespace( - name="engineer", - frontmatter={"version": "1.0.0"}, - unresolved=[], - content="new content", + """run() calls _write_vstack_gitignore for local (non-global) installs.""" + written: list[dict] = [] + + monkeypatch.setattr( + "vstack.cli.install.InstallCommand._seed_project", + staticmethod(lambda **kw: None), + ) + monkeypatch.setattr( + "vstack.cli.install.InstallCommand._write_vstack_gitignore", + staticmethod(lambda **kw: written.append(kw)), + ) + monkeypatch.setattr("vstack.cli.init.InitCommand.execute", staticmethod(lambda *a, **kw: 0)) + + from argparse import Namespace + + fake_service = SimpleNamespace(root=tmp_path / "templates") + context = CommandContext( + args=Namespace( + force=False, + force_names=None, + adopt_name=None, + update=False, + dry_run=False, + use_global=False, ), - out_dir=out_dir, - colors=colors, - prefix="", - force=False, - update=False, - dry_run=True, - targeted_force_names=set(), - targeted_adopt_names=set(), - existing_entries={}, - new_entries={}, - checksum_algorithm="sha256", + install_dir=tmp_path / ".github", + only=None, ) - assert result == "preserve" - capsys.readouterr() + InstallCommand(service=cast(CommandService, fake_service)).run(context=context) + assert len(written) == 1 + assert written[0]["project_root"] == tmp_path + assert written[0]["dry_run"] is False - def test_install_single_artifact_returns_adopt_when_adopting( - self, - tmp_path: Path, - capsys: pytest.CaptureFixture[str], + def test_run_skips_write_vstack_gitignore_for_global( + self, monkeypatch: pytest.MonkeyPatch, tmp_path: Path ) -> None: - """Returns 'adopt' when taking ownership of an existing untracked file.""" - - class _Gen: - config = SimpleNamespace( - type_name="agent", - manifest_key="agents", - output_subdir="agents", - ) - - @staticmethod - def output_path(name: str) -> Path: - return Path(f"{name}.agent.md") - - @staticmethod - def install_relative_path(name: str) -> str: - return f"agents/{name}.agent.md" - - out_dir = tmp_path / "agents" - out_dir.mkdir() - (out_dir / "engineer.agent.md").write_text("existing", encoding="utf-8") - - new_entries: dict[str, list[Any]] = {} - colors = SimpleNamespace(CYAN="", RESET="", DIM="", YELLOW="", GREEN="", BOLD="") - result = InstallCommand._install_single_artifact( - service=cast(CommandService, SimpleNamespace(label=lambda p: str(p))), - gen=_Gen(), - artifact=SimpleNamespace( - name="engineer", - frontmatter={"version": "1.0.0"}, - unresolved=[], - content="new content", + """run() does not call _write_vstack_gitignore for global installs.""" + written: list[dict] = [] + + monkeypatch.setattr( + "vstack.cli.install.InstallCommand._write_vstack_gitignore", + staticmethod(lambda **kw: written.append(kw)), + ) + monkeypatch.setattr("vstack.cli.init.InitCommand.execute", staticmethod(lambda *a, **kw: 0)) + + from argparse import Namespace + + context = CommandContext( + args=Namespace( + force=False, + force_names=None, + adopt_name=None, + update=False, + dry_run=False, + use_global=True, ), - out_dir=out_dir, - colors=colors, - prefix="", - force=False, - update=False, - dry_run=True, - targeted_force_names=set(), - targeted_adopt_names={"engineer"}, - existing_entries={}, - new_entries=new_entries, - checksum_algorithm="sha256", + install_dir=tmp_path / ".github", + only=None, ) - assert result == "adopt" - capsys.readouterr() + InstallCommand(service=cast(CommandService, object())).run(context=context) + assert written == [] diff --git a/tests/vstack/cli/test_interface.py b/tests/vstack/cli/test_interface.py index 411fae1..2e3c2f2 100644 --- a/tests/vstack/cli/test_interface.py +++ b/tests/vstack/cli/test_interface.py @@ -56,8 +56,9 @@ def run(self, *, context: CommandContext): class _Service: """Service construction test double.""" - def __init__(self, *, templates_root) -> None: + def __init__(self, *, templates_root, artifacts_root: str = "docs") -> None: self.templates_root = templates_root + self.artifacts_root = artifacts_root class TestCommandLineInterface: @@ -150,3 +151,213 @@ def test_resolve_only_for_scope_accepts_allowed_global_types(self) -> None: """Global mode accepts allowed types unchanged.""" args = argparse.Namespace(use_global=True, only=["skill", "agent"]) assert CommandLineInterface.resolve_only_for_scope(args) == ["skill", "agent"] + + +class TestReadArtifactsRoot: + """Tests for CommandLineInterface._read_artifacts_root.""" + + def test_returns_default_when_install_dir_is_none(self) -> None: + """Returns ARTIFACTS_DOCS_ROOT when no install dir is provided.""" + from vstack.constants import ARTIFACTS_DOCS_ROOT + + assert CommandLineInterface._read_artifacts_root(None) == ARTIFACTS_DOCS_ROOT + + def test_returns_default_when_config_file_absent(self, tmp_path: Path) -> None: + """Returns ARTIFACTS_DOCS_ROOT when .vstack/config.yaml does not exist.""" + from vstack.constants import ARTIFACTS_DOCS_ROOT + + install_dir = tmp_path / ".github" + assert CommandLineInterface._read_artifacts_root(install_dir) == ARTIFACTS_DOCS_ROOT + + def test_returns_value_from_config(self, tmp_path: Path) -> None: + """Returns the artifacts.root value from .vstack/config.yaml.""" + vstack_dir = tmp_path / ".vstack" + vstack_dir.mkdir() + (vstack_dir / "config.yaml").write_text( + "artifacts:\n root: documentation\n", encoding="utf-8" + ) + + install_dir = tmp_path / ".github" + assert CommandLineInterface._read_artifacts_root(install_dir) == "documentation" + + def test_returns_default_when_value_is_blank(self, tmp_path: Path) -> None: + """Returns ARTIFACTS_DOCS_ROOT when artifacts.root is present but blank.""" + from vstack.constants import ARTIFACTS_DOCS_ROOT + + vstack_dir = tmp_path / ".vstack" + vstack_dir.mkdir() + (vstack_dir / "config.yaml").write_text("artifacts:\n root:\n", encoding="utf-8") + + install_dir = tmp_path / ".github" + assert CommandLineInterface._read_artifacts_root(install_dir) == ARTIFACTS_DOCS_ROOT + + def test_returns_default_when_key_absent_in_config(self, tmp_path: Path) -> None: + """Returns ARTIFACTS_DOCS_ROOT when config.yaml exists but has no artifacts.root key.""" + from vstack.constants import ARTIFACTS_DOCS_ROOT + + vstack_dir = tmp_path / ".vstack" + vstack_dir.mkdir() + (vstack_dir / "config.yaml").write_text("exclude:\n prompts: all\n", encoding="utf-8") + + install_dir = tmp_path / ".github" + assert CommandLineInterface._read_artifacts_root(install_dir) == ARTIFACTS_DOCS_ROOT + + def test_run_passes_artifacts_root_from_config_to_service( + self, monkeypatch, tmp_path: Path + ) -> None: + """run() reads artifacts.root from .vstack/config.yaml and passes it to the service.""" + vstack_dir = tmp_path / ".vstack" + vstack_dir.mkdir() + (vstack_dir / "config.yaml").write_text("artifacts:\n root: custom\n", encoding="utf-8") + + install_dir = tmp_path / ".github" + args = argparse.Namespace(command="install", only=None, use_global=False) + parser = _Parser(args=args, resolved_target=install_dir) + command = _Command(exit_code=0) + captured: list[str] = [] + + class _CapturingService: + def __init__(self, *, templates_root, artifacts_root: str = "docs") -> None: + captured.append(artifacts_root) + + monkeypatch.setattr( + "vstack.cli.interface.build_command_registry", + lambda service: {"install": command}, + ) + + interface = CommandLineInterface( + parser_cls=cast(Any, lambda: parser), + service_cls=cast(Any, _CapturingService), + templates_root=tmp_path, + ) + interface.run() + + assert captured == ["custom"] + + +class TestReadExclude: + """Tests for CommandLineInterface._read_exclude.""" + + def test_returns_empty_when_install_dir_is_none(self) -> None: + """Returns empty sets when no install dir is provided.""" + excluded_types, excluded_names = CommandLineInterface._read_exclude(None) + assert excluded_types == frozenset() + assert excluded_names == {} + + def test_returns_empty_when_config_absent(self, tmp_path: Path) -> None: + """Returns empty sets when .vstack/config.yaml does not exist.""" + install_dir = tmp_path / ".github" + excluded_types, excluded_names = CommandLineInterface._read_exclude(install_dir) + assert excluded_types == frozenset() + assert excluded_names == {} + + def test_returns_excluded_type_for_all(self, tmp_path: Path) -> None: + """Returns excluded type when a type entry is 'all'.""" + vstack_dir = tmp_path / ".vstack" + vstack_dir.mkdir() + (vstack_dir / "config.yaml").write_text("exclude:\n instructions: all\n", encoding="utf-8") + excluded_types, excluded_names = CommandLineInterface._read_exclude(tmp_path / ".github") + assert "instruction" in excluded_types + assert excluded_names == {} + + def test_returns_excluded_names_for_list(self, tmp_path: Path) -> None: + """Returns name list when a type entry is a list of artifact names.""" + vstack_dir = tmp_path / ".vstack" + vstack_dir.mkdir() + (vstack_dir / "config.yaml").write_text( + "exclude:\n skills:\n - terraform\n - helm\n", encoding="utf-8" + ) + excluded_types, excluded_names = CommandLineInterface._read_exclude(tmp_path / ".github") + assert excluded_types == frozenset() + assert excluded_names == {"skill": ["terraform", "helm"]} + + def test_handles_mixed_all_and_list(self, tmp_path: Path) -> None: + """Handles both 'all' and name-list entries in the same exclude block.""" + vstack_dir = tmp_path / ".vstack" + vstack_dir.mkdir() + (vstack_dir / "config.yaml").write_text( + "exclude:\n prompts: all\n skills:\n - helm\n", encoding="utf-8" + ) + excluded_types, excluded_names = CommandLineInterface._read_exclude(tmp_path / ".github") + assert "prompt" in excluded_types + assert excluded_names == {"skill": ["helm"]} + + def test_ignores_unknown_keys(self, tmp_path: Path) -> None: + """Unknown type keys in exclude block are silently ignored.""" + vstack_dir = tmp_path / ".vstack" + vstack_dir.mkdir() + (vstack_dir / "config.yaml").write_text("exclude:\n workflows: all\n", encoding="utf-8") + excluded_types, excluded_names = CommandLineInterface._read_exclude(tmp_path / ".github") + assert excluded_types == frozenset() + assert excluded_names == {} + + def test_raises_when_agents_excluded(self, tmp_path: Path) -> None: + """Configuring exclude: agents raises ValueError referencing ADR-022.""" + vstack_dir = tmp_path / ".vstack" + vstack_dir.mkdir() + (vstack_dir / "config.yaml").write_text("exclude:\n agents: all\n", encoding="utf-8") + with pytest.raises(ValueError, match="ADR-022"): + CommandLineInterface._read_exclude(tmp_path / ".github") + + def test_returns_empty_when_exclude_key_absent(self, tmp_path: Path) -> None: + """Returns empty sets when config.yaml has no exclude key.""" + vstack_dir = tmp_path / ".vstack" + vstack_dir.mkdir() + (vstack_dir / "config.yaml").write_text("artifacts:\n root: docs\n", encoding="utf-8") + excluded_types, excluded_names = CommandLineInterface._read_exclude(tmp_path / ".github") + assert excluded_types == frozenset() + assert excluded_names == {} + + def test_run_removes_excluded_type_from_only(self, monkeypatch, tmp_path: Path) -> None: + """run() removes excluded types from effective_only when exclude: type: all.""" + vstack_dir = tmp_path / ".vstack" + vstack_dir.mkdir() + (vstack_dir / "config.yaml").write_text( + "exclude:\n prompts: all\n instructions: all\n", encoding="utf-8" + ) + install_dir = tmp_path / ".github" + args = argparse.Namespace(command="install", only=None, use_global=False) + parser = _Parser(args=args, resolved_target=install_dir) + command = _Command(exit_code=0) + + monkeypatch.setattr( + "vstack.cli.interface.build_command_registry", + lambda service: {"install": command}, + ) + interface = CommandLineInterface( + parser_cls=cast(Any, lambda: parser), + service_cls=cast(Any, _Service), + templates_root=tmp_path, + ) + interface.run() + + assert command.calls[0].only is not None + assert "prompt" not in command.calls[0].only + assert "instruction" not in command.calls[0].only + assert "skill" in command.calls[0].only + assert "agent" in command.calls[0].only + + def test_run_passes_excluded_names_to_context(self, monkeypatch, tmp_path: Path) -> None: + """run() passes name-level exclusions as context.excluded_names.""" + vstack_dir = tmp_path / ".vstack" + vstack_dir.mkdir() + (vstack_dir / "config.yaml").write_text( + "exclude:\n skills:\n - terraform\n - helm\n", encoding="utf-8" + ) + install_dir = tmp_path / ".github" + args = argparse.Namespace(command="install", only=None, use_global=False) + parser = _Parser(args=args, resolved_target=install_dir) + command = _Command(exit_code=0) + + monkeypatch.setattr( + "vstack.cli.interface.build_command_registry", + lambda service: {"install": command}, + ) + interface = CommandLineInterface( + parser_cls=cast(Any, lambda: parser), + service_cls=cast(Any, _Service), + templates_root=tmp_path, + ) + interface.run() + + assert command.calls[0].excluded_names == {"skill": ["terraform", "helm"]} diff --git a/tests/vstack/cli/test_parser.py b/tests/vstack/cli/test_parser.py index 8e30bc3..c24123b 100644 --- a/tests/vstack/cli/test_parser.py +++ b/tests/vstack/cli/test_parser.py @@ -82,7 +82,15 @@ def test_build_parser_has_expected_commands(self) -> None: ) assert subcommands_action is not None subcommands = set(cast(Any, subcommands_action).choices.keys()) - assert subcommands == {"validate", "verify", "status", "manifest", "install", "uninstall"} + assert subcommands == { + "validate", + "verify", + "status", + "manifest", + "install", + "init", + "uninstall", + } def test_verify_accepts_only_filter(self) -> None: """Test that verify command supports --only type filters.""" diff --git a/tests/vstack/cli/test_service.py b/tests/vstack/cli/test_service.py index 3b1841f..79e1a93 100644 --- a/tests/vstack/cli/test_service.py +++ b/tests/vstack/cli/test_service.py @@ -35,6 +35,19 @@ def test_validate_exits_nonzero_for_empty_templates(self, tmp_path: Path) -> Non svc = CommandService(templates_root=tmp_path) assert svc.validate() == 1 + def test_manifest_for_dot_github_redirects_to_dot_vstack(self, tmp_path: Path) -> None: + """manifest_for() returns a ManifestFile in .vstack/ for .github install dirs.""" + svc = CommandService(templates_root=tmp_path) + install_dir = tmp_path / ".github" + mf = svc.manifest_for(install_dir) + assert mf.path == tmp_path / ".vstack" / "vstack.json" + + def test_manifest_for_non_github_dir_uses_install_dir(self, tmp_path: Path) -> None: + """manifest_for() places the manifest directly in install_dir for non-.github dirs.""" + svc = CommandService(templates_root=tmp_path) + mf = svc.manifest_for(tmp_path) + assert mf.path == tmp_path / "vstack.json" + def test_label_prefers_relative(self, tmp_path: Path) -> None: """label() returns a path relative to templates_root when possible.""" svc = CommandService(templates_root=tmp_path) @@ -53,6 +66,14 @@ def test_cli_class_uses_known_types(self) -> None: names = {g.config.type_name for g in svc.generators} assert names == {"skill", "agent", "instruction", "prompt"} + def test_custom_artifacts_root_is_passed_to_agent_generator(self, tmp_path: Path) -> None: + """artifacts_root kwarg is forwarded to the AgentGenerator instance.""" + from vstack.agents.generator import AgentGenerator + + svc = CommandService(templates_root=tmp_path, artifacts_root="documentation") + agent_gen = next(g for g in svc.generators if isinstance(g, AgentGenerator)) + assert agent_gen.artifacts_root == "documentation" + def test_gen_for_returns_none_for_unknown_type(self) -> None: """gen_for() returns None for an unrecognized type name.""" svc = CommandService(templates_root=TEMPLATES_ROOT) @@ -64,8 +85,8 @@ def test_gen_for_returns_none_for_unknown_type(self) -> None: ("validate", "vstack.cli.validate", "ValidateCommand", {"only": ["skill"]}, 10), ( "install", - "vstack.cli.install", - "InstallCommand", + "vstack.cli.init", + "InitCommand", { "install_dir": Path("/tmp/install"), "only": ["skill"], @@ -187,14 +208,15 @@ def test_install_only_preserves_manifest_entries_for_other_types(self, tmp_path: ], }, } - (install_dir / "vstack.json").write_text(json.dumps(manifest), encoding="utf-8") + (tmp_path / ".vstack").mkdir(parents=True, exist_ok=True) + (tmp_path / ".vstack" / "vstack.json").write_text(json.dumps(manifest), encoding="utf-8") svc = CommandService(templates_root=TEMPLATES_ROOT) rc = svc.install(install_dir, only=["instruction"]) assert rc == 0 updated: dict[str, Any] = json.loads( - (install_dir / "vstack.json").read_text(encoding="utf-8") + (tmp_path / ".vstack" / "vstack.json").read_text(encoding="utf-8") ) assert "instructions" in updated["artifacts"] assert updated["artifacts"]["agents"] == manifest["artifacts"]["agents"] @@ -220,7 +242,8 @@ def test_install_update_skips_when_version_not_newer(self, tmp_path: Path) -> No }, } install_dir.mkdir(parents=True) - (install_dir / "vstack.json").write_text(json.dumps(manifest), encoding="utf-8") + (tmp_path / ".vstack").mkdir(parents=True, exist_ok=True) + (tmp_path / ".vstack" / "vstack.json").write_text(json.dumps(manifest), encoding="utf-8") (install_dir / "skills" / "vision").mkdir(parents=True) (install_dir / "skills" / "vision" / "SKILL.md").write_text("old", encoding="utf-8") @@ -241,7 +264,7 @@ def test_install_preserves_existing_unmanaged_file(self, tmp_path: Path) -> None assert rc == 0 assert artifact_path.read_text(encoding="utf-8") == "user content" - manifest = json.loads((install_dir / "vstack.json").read_text(encoding="utf-8")) + manifest = json.loads((tmp_path / ".vstack" / "vstack.json").read_text(encoding="utf-8")) skill_entries = manifest["artifacts"]["skills"] assert all(entry["name"] != "vision" for entry in skill_entries) @@ -257,7 +280,7 @@ def test_install_adopt_name_tracks_existing_unmanaged_file(self, tmp_path: Path) assert rc == 0 assert artifact_path.read_text(encoding="utf-8") == "user content" - manifest = json.loads((install_dir / "vstack.json").read_text(encoding="utf-8")) + manifest = json.loads((tmp_path / ".vstack" / "vstack.json").read_text(encoding="utf-8")) vision = next( entry for entry in manifest["artifacts"]["skills"] if entry["name"] == "vision" ) @@ -276,7 +299,7 @@ def test_install_force_name_overwrites_existing_unmanaged_file(self, tmp_path: P assert rc == 0 assert artifact_path.read_text(encoding="utf-8") != "user content" - manifest = json.loads((install_dir / "vstack.json").read_text(encoding="utf-8")) + manifest = json.loads((tmp_path / ".vstack" / "vstack.json").read_text(encoding="utf-8")) vision = next( entry for entry in manifest["artifacts"]["skills"] if entry["name"] == "vision" ) @@ -293,7 +316,9 @@ def test_install_update_preserves_locally_modified_tracked_file(self, tmp_path: original = artifact_path.read_text(encoding="utf-8") artifact_path.write_text(original + "\nlocal edit\n", encoding="utf-8") - manifest_before = json.loads((install_dir / "vstack.json").read_text(encoding="utf-8")) + manifest_before = json.loads( + (tmp_path / ".vstack" / "vstack.json").read_text(encoding="utf-8") + ) vision_before = next( entry for entry in manifest_before["artifacts"]["skills"] if entry["name"] == "vision" ) @@ -301,7 +326,9 @@ def test_install_update_preserves_locally_modified_tracked_file(self, tmp_path: assert svc.install(install_dir, only=["skill"], update=True) == 0 assert artifact_path.read_text(encoding="utf-8").endswith("local edit\n") - manifest_after = json.loads((install_dir / "vstack.json").read_text(encoding="utf-8")) + manifest_after = json.loads( + (tmp_path / ".vstack" / "vstack.json").read_text(encoding="utf-8") + ) vision_after = next( entry for entry in manifest_after["artifacts"]["skills"] if entry["name"] == "vision" ) @@ -400,7 +427,8 @@ def test_install_existing_without_update_rewrites_clean_tracked_file( }, } install_dir.mkdir(parents=True) - (install_dir / "vstack.json").write_text(json.dumps(manifest), encoding="utf-8") + (tmp_path / ".vstack").mkdir(parents=True, exist_ok=True) + (tmp_path / ".vstack" / "vstack.json").write_text(json.dumps(manifest), encoding="utf-8") (install_dir / "skills" / "vision").mkdir(parents=True) artifact_path = install_dir / "skills" / "vision" / "SKILL.md" artifact_path.write_text(old_content, encoding="utf-8") @@ -434,7 +462,8 @@ def test_install_existing_without_update_preserves_modified_tracked_file( }, } install_dir.mkdir(parents=True) - (install_dir / "vstack.json").write_text(json.dumps(manifest), encoding="utf-8") + (tmp_path / ".vstack").mkdir(parents=True, exist_ok=True) + (tmp_path / ".vstack" / "vstack.json").write_text(json.dumps(manifest), encoding="utf-8") (install_dir / "skills" / "vision").mkdir(parents=True) artifact_path = install_dir / "skills" / "vision" / "SKILL.md" artifact_path.write_text(old_content + "-local-edit", encoding="utf-8") @@ -465,7 +494,8 @@ def test_install_update_newer_version_writes_file(self, tmp_path: Path) -> None: }, } install_dir.mkdir(parents=True) - (install_dir / "vstack.json").write_text(json.dumps(manifest), encoding="utf-8") + (tmp_path / ".vstack").mkdir(parents=True, exist_ok=True) + (tmp_path / ".vstack" / "vstack.json").write_text(json.dumps(manifest), encoding="utf-8") (install_dir / "skills" / "vision").mkdir(parents=True) (install_dir / "skills" / "vision" / "SKILL.md").write_text(old_content, encoding="utf-8") @@ -574,7 +604,8 @@ def test_verify_output_uses_manifest_names(self, tmp_path: Path) -> None: "artifacts": {"skill": [{"name": "custom", "file": "skills/custom/SKILL.md"}]}, } install_dir.mkdir(parents=True, exist_ok=True) - (install_dir / "vstack.json").write_text(json.dumps(manifest), encoding="utf-8") + (tmp_path / ".vstack").mkdir(parents=True, exist_ok=True) + (tmp_path / ".vstack" / "vstack.json").write_text(json.dumps(manifest), encoding="utf-8") svc = CommandService(templates_root=TEMPLATES_ROOT) rc = svc.verify(install_dir=install_dir, source=False, output=True) @@ -673,7 +704,8 @@ def test_status_fails_when_manifest_schema_is_legacy(self, tmp_path: Path) -> No """status() returns 1 and prints guidance for a schema-v1 manifest.""" install_dir = tmp_path / ".github" install_dir.mkdir(parents=True) - (install_dir / "vstack.json").write_text( + (tmp_path / ".vstack").mkdir(parents=True, exist_ok=True) + (tmp_path / ".vstack" / "vstack.json").write_text( json.dumps( { "vstack_version": "1.3.6", @@ -693,7 +725,8 @@ def test_status_fails_when_untracked_collision_exists(self, tmp_path: Path) -> N artifact_path = install_dir / "skills" / "vision" / "SKILL.md" artifact_path.parent.mkdir(parents=True) artifact_path.write_text("user content", encoding="utf-8") - (install_dir / "vstack.json").write_text( + (tmp_path / ".vstack").mkdir(parents=True, exist_ok=True) + (tmp_path / ".vstack" / "vstack.json").write_text( json.dumps( { "manifest_version": 2, @@ -732,7 +765,8 @@ def test_status_legacy_entry_without_checksum_is_warning_not_failure( instruction_file = install_dir / "instructions" / "python.instructions.md" instruction_file.parent.mkdir(parents=True) instruction_file.write_text("legacy content\n", encoding="utf-8") - (install_dir / "vstack.json").write_text( + (tmp_path / ".vstack").mkdir(parents=True, exist_ok=True) + (tmp_path / ".vstack" / "vstack.json").write_text( json.dumps( { "manifest_version": 2, @@ -786,9 +820,9 @@ def test_uninstall_removes_manifest_and_outputs(self, tmp_path: Path) -> None: install_dir = tmp_path / ".github" svc = CommandService(templates_root=TEMPLATES_ROOT) assert svc.install(install_dir, only=["skill", "agent"]) == 0 - assert (install_dir / "vstack.json").exists() + assert (tmp_path / ".vstack" / "vstack.json").exists() assert svc.uninstall(install_dir) == 0 - assert not (install_dir / "vstack.json").exists() + assert not (tmp_path / ".vstack" / "vstack.json").exists() def test_uninstall_without_manifest_and_without_files(self, tmp_path: Path) -> None: """uninstall() returns 0 when there is no manifest and no tracked files.""" @@ -816,7 +850,8 @@ def test_uninstall_non_directory_artifact_removes_file(self, tmp_path: Path) -> ] }, } - (install_dir / "vstack.json").write_text(json.dumps(manifest), encoding="utf-8") + (tmp_path / ".vstack").mkdir(parents=True, exist_ok=True) + (tmp_path / ".vstack" / "vstack.json").write_text(json.dumps(manifest), encoding="utf-8") svc = CommandService(templates_root=TEMPLATES_ROOT) assert svc.uninstall(install_dir) == 0 @@ -834,7 +869,9 @@ def test_uninstall_preserves_locally_modified_tracked_file(self, tmp_path: Path) assert svc.uninstall(install_dir, only=["instruction"]) == 0 assert instruction_file.exists() - manifest_after = json.loads((install_dir / "vstack.json").read_text(encoding="utf-8")) + manifest_after = json.loads( + (tmp_path / ".vstack" / "vstack.json").read_text(encoding="utf-8") + ) assert manifest_after["artifacts"].get("instructions") def test_uninstall_force_name_removes_locally_modified_tracked_file( @@ -1100,7 +1137,8 @@ def test_manifest_upgrade_migrates_legacy_schema(self, tmp_path: Path) -> None: """manifest_upgrade() migrates schema-v1 manifest to current schema.""" install_dir = tmp_path / ".github" install_dir.mkdir(parents=True) - (install_dir / "vstack.json").write_text( + (tmp_path / ".vstack").mkdir(parents=True, exist_ok=True) + (tmp_path / ".vstack" / "vstack.json").write_text( json.dumps( { "vstack_version": "1.3.6", @@ -1114,7 +1152,7 @@ def test_manifest_upgrade_migrates_legacy_schema(self, tmp_path: Path) -> None: svc = CommandService(templates_root=TEMPLATES_ROOT) assert svc.manifest_upgrade(install_dir) == 0 - upgraded = json.loads((install_dir / "vstack.json").read_text(encoding="utf-8")) + upgraded = json.loads((tmp_path / ".vstack" / "vstack.json").read_text(encoding="utf-8")) assert upgraded["manifest_version"] == 2 assert upgraded["hash_algorithm"] == "sha256" @@ -1134,7 +1172,8 @@ def test_manifest_upgrade_backfill_adds_checksum_for_footer_tagged_legacy_entry( encoding="utf-8", ) - (install_dir / "vstack.json").write_text( + (tmp_path / ".vstack").mkdir(parents=True, exist_ok=True) + (tmp_path / ".vstack" / "vstack.json").write_text( json.dumps( { "manifest_version": 2, @@ -1158,7 +1197,7 @@ def test_manifest_upgrade_backfill_adds_checksum_for_footer_tagged_legacy_entry( svc = CommandService(templates_root=TEMPLATES_ROOT) assert svc.manifest_upgrade(install_dir, backfill=True) == 0 - upgraded = json.loads((install_dir / "vstack.json").read_text(encoding="utf-8")) + upgraded = json.loads((tmp_path / ".vstack" / "vstack.json").read_text(encoding="utf-8")) entry = upgraded["artifacts"]["skills"][0] assert entry["checksum_algorithm"] == "sha256" assert isinstance(entry["checksum"], str) diff --git a/tests/vstack/manifest/test_store.py b/tests/vstack/manifest/test_store.py index 8c520b6..10ecb2c 100644 --- a/tests/vstack/manifest/test_store.py +++ b/tests/vstack/manifest/test_store.py @@ -487,7 +487,7 @@ def test_write_and_read_manifest(self, tmp_path) -> None: def test_manifest_contains_all_expected_canonical_names(self, installed_target) -> None: """Test that manifest contains all expected canonical names.""" data = json.loads( - (installed_target / ".github" / "vstack.json").read_text(encoding="utf-8") + (installed_target / ".vstack" / "vstack.json").read_text(encoding="utf-8") ) skill_names = [s["name"] for s in data["artifacts"]["skills"]] for name in EXPECTED_CANONICAL_NAMES: diff --git a/tests/vstack/test_integration.py b/tests/vstack/test_integration.py index 2e557bc..e87d054 100644 --- a/tests/vstack/test_integration.py +++ b/tests/vstack/test_integration.py @@ -69,9 +69,9 @@ def test_install_and_verify_exits_zero(self, tmp_path: Path) -> None: def test_manifest_upgrade_migrates_legacy_schema(self, tmp_path: Path) -> None: """vstack manifest upgrade migrates schema-v1 manifest to current schema.""" - install_dir = tmp_path / ".github" - install_dir.mkdir(parents=True) - (install_dir / "vstack.json").write_text( + vstack_dir = tmp_path / ".vstack" + vstack_dir.mkdir(parents=True) + (vstack_dir / "vstack.json").write_text( json.dumps( { "vstack_version": "1.3.6", @@ -87,7 +87,7 @@ def test_manifest_upgrade_migrates_legacy_schema(self, tmp_path: Path) -> None: f"vstack manifest upgrade failed:\n{upgrade.stdout}\n{upgrade.stderr}" ) - upgraded = json.loads((install_dir / "vstack.json").read_text(encoding="utf-8")) + upgraded = json.loads((vstack_dir / "vstack.json").read_text(encoding="utf-8")) assert upgraded["manifest_version"] == 2 assert upgraded["hash_algorithm"] == "sha256" @@ -112,18 +112,18 @@ def test_manifest_upgrade_backfill_adds_checksum_for_footer_tagged_legacy_entry( tmp_path: Path, ) -> None: """vstack manifest upgrade --backfill stores checksum for VSTACK-META-tagged entries.""" - install_dir = tmp_path / ".github" - install_dir.mkdir(parents=True) + vstack_dir = tmp_path / ".vstack" + vstack_dir.mkdir(parents=True) artifact_rel = "skills/vision/SKILL.md" - artifact_path = install_dir / artifact_rel + artifact_path = (tmp_path / ".github") / artifact_rel artifact_path.parent.mkdir(parents=True, exist_ok=True) artifact_path.write_text( '# Vision\n\n<!-- VSTACK-META: {"artifact_name":"vision"} -->\n', encoding="utf-8", ) - (install_dir / "vstack.json").write_text( + (vstack_dir / "vstack.json").write_text( json.dumps( { "manifest_version": 2, @@ -149,7 +149,7 @@ def test_manifest_upgrade_backfill_adds_checksum_for_footer_tagged_legacy_entry( f"vstack manifest upgrade --backfill failed:\n{upgrade.stdout}\n{upgrade.stderr}" ) - upgraded = json.loads((install_dir / "vstack.json").read_text(encoding="utf-8")) + upgraded = json.loads((vstack_dir / "vstack.json").read_text(encoding="utf-8")) entry = upgraded["artifacts"]["skills"][0] assert entry["checksum_algorithm"] == "sha256" assert isinstance(entry["checksum"], str)