From fac856553cf0491edf145fd9ba2ae1daf8bfd325 Mon Sep 17 00:00:00 2001 From: Erik Schaareman Date: Sat, 9 May 2026 01:26:39 +0200 Subject: [PATCH 01/25] feat(parser): support nested block YAML in object-list items Extend FrontmatterParser.parse_yaml to accumulate nested key/value blocks inside object-list items (e.g. handoffs: under stages[]). Empty-value 4-space keys trigger accumulation of subsequent 6-space- indented lines, stored as a stripped raw string for callers to re-parse. A trailing nested block left open at EOF is correctly flushed. This is the prerequisite for reading workflow.stages[].handoffs from .vstack/config.yaml without a full YAML library. --- src/vstack/frontmatter/parser.py | 38 ++++++++++++++++-- tests/vstack/frontmatter/test_parser.py | 51 +++++++++++++++++++++++++ 2 files changed, 86 insertions(+), 3 deletions(-) diff --git a/src/vstack/frontmatter/parser.py b/src/vstack/frontmatter/parser.py index 2223518..cc04c01 100644 --- a/src/vstack/frontmatter/parser.py +++ b/src/vstack/frontmatter/parser.py @@ -125,9 +125,12 @@ def _parse_yaml_block(raw: str) -> dict: Supports: string values, inline lists ``[a, b]``, block lists ``\n - item``, block scalars ``|``, block sequences of mappings (object-lists): - ``\n - key: val\n key2: val2``, and + ``\n - key: val\n key2: val2``, raw mapping blocks where the value is indented non-list YAML content: - ``\n server:\n type: local`` (used for ``mcp-servers``, ``hooks``, etc.). + ``\n server:\n type: local`` (used for ``mcp-servers``, ``hooks``, etc.), and + nested blocks inside object-list items: an empty-value 4-space key + (`` key:``) accumulates subsequent 6-space lines as a stripped raw + string that callers can re-parse (e.g. ``workflow.stages[].handoffs``). """ meta: dict = {} current_key = "" @@ -138,11 +141,31 @@ def _parse_yaml_block(raw: str) -> dict: in_object_block_scalar = False object_scalar_field = "" object_block_lines: list[str] = [] + in_object_nested_block = False + object_nested_key = "" + object_nested_lines: list[str] = [] for line in raw.split("\n"): if line.strip().startswith("#"): continue + # ── Nested block inside object-list item ───────────────────────── + if in_object_nested_block: + if line.startswith(" ") or line == "": + # Strip 6 leading spaces so the stored content is parseable + # as top-level YAML (for dict) or as a 0-indent list. + object_nested_lines.append(line[6:] if len(line) > 6 else "") + continue + else: + # Non-6-space line closes the nested block. + meta[current_key][-1][object_nested_key] = "\n".join( + object_nested_lines + ).rstrip() + in_object_nested_block = False + object_nested_key = "" + object_nested_lines = [] + # Fall through to process current line. + if in_object_block_scalar: if line.startswith(" ") or line == "": object_block_lines.append(line.strip()) @@ -196,8 +219,15 @@ def _parse_yaml_block(raw: str) -> dict: object_scalar_field = obj_key object_block_lines = [] meta[current_key][-1][obj_key] = "" - else: + elif obj_val: meta[current_key][-1][obj_key] = FrontmatterParser._parse_scalar(obj_val) + else: + # Empty value in an object-list item: start nested block + # accumulation for any following 6-space-indented content. + in_object_nested_block = True + object_nested_key = obj_key + object_nested_lines = [] + meta[current_key][-1][obj_key] = "" continue # Raw block trigger: 2-space non-list indented line when the current key @@ -256,6 +286,8 @@ def _parse_yaml_block(raw: str) -> dict: object_scalar_field=object_scalar_field, object_block_lines=object_block_lines, ) + if in_object_nested_block: + meta[current_key][-1][object_nested_key] = "\n".join(object_nested_lines).rstrip() if in_raw_block and raw_lines: FrontmatterParser._flush_raw_block( meta=meta, diff --git a/tests/vstack/frontmatter/test_parser.py b/tests/vstack/frontmatter/test_parser.py index 634a20e..1db77ba 100644 --- a/tests/vstack/frontmatter/test_parser.py +++ b/tests/vstack/frontmatter/test_parser.py @@ -125,3 +125,54 @@ def test_parse_yaml_string_list_coerces_from_scalar(self) -> None: meta = FrontmatterParser.parse_yaml(raw) assert isinstance(meta["tools"], list) assert meta["tools"] == ["read"] + + def test_parse_yaml_object_list_nested_block_dict(self) -> None: + """Nested dict block inside an object-list item is accumulated and stored as a string.""" + raw = ( + "stages:\n" + " - role: architect\n" + " gate: required\n" + " handoffs:\n" + " prompt: Architecture done.\n" + " agent: designer\n" + ) + meta = FrontmatterParser.parse_yaml(raw) + assert isinstance(meta["stages"], list) + stage = meta["stages"][0] + assert stage["role"] == "architect" + assert stage["gate"] == "required" + # handoffs is stored as a raw string stripped of 6-space indent + assert isinstance(stage["handoffs"], str) + assert "prompt: Architecture done." in stage["handoffs"] + assert "agent: designer" in stage["handoffs"] + + def test_parse_yaml_object_list_nested_block_with_block_scalar(self) -> None: + """Nested block inside an object-list item handles block scalar prompts.""" + raw = ( + "stages:\n" + " - role: architect\n" + " gate: required\n" + " handoffs:\n" + " prompt: >\n" + " Line one\n" + " Line two\n" + " other: value\n" + ) + meta = FrontmatterParser.parse_yaml(raw) + stage = meta["stages"][0] + assert isinstance(stage["handoffs"], str) + # Re-parsing the stored raw string should yield the folded prompt + from vstack.frontmatter import FrontmatterParser as FP + + reparsed = FP.parse_yaml(stage["handoffs"]) + assert "Line one" in reparsed["prompt"] + assert "Line two" in reparsed["prompt"] + # The key after handoffs block is parsed correctly + assert stage["other"] == "value" + + def test_parse_yaml_object_list_empty_nested_block_is_empty_string(self) -> None: + """An empty-value nested key in an object-list item stores an empty string.""" + raw = "stages:\n - role: release\n gate: required\n handoffs:\n" + meta = FrontmatterParser.parse_yaml(raw) + stage = meta["stages"][0] + assert stage["handoffs"] == "" From 9268c523252aadc13bef1ae654701ec9d36f056d Mon Sep 17 00:00:00 2001 From: Erik Schaareman Date: Sat, 9 May 2026 01:27:15 +0200 Subject: [PATCH 02/25] feat(agents): restructure config to defaults block; add baseline artifact flag MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Move handoffs and artifacts from top-level agent config.yaml into a defaults: block. The generator extracts this block and re-exposes it at the expected paths, keeping generated agent.md output identical. Add baseline: true flag on output artifact entries. Flagged items are rendered into a dedicated '### baseline docs you maintain' table with an explicit instruction to keep them current — separating living docs from per-session deliverables. Add AGENT_ARTIFACTS_BASELINE template token and inject it into all six role agent templates. Refactor AgentGenerator: - _extract_defaults(): re-parses raw indented string from minimal parser - load_artifact_config(): merges defaults into config, resolves handoffs - _resolve_handoffs(): derives next agent from workflow stage order - _resolve_output_entries(): now an instance method; adds baseline field - _build_baseline_section(): renders the new baseline table - _build_handoffs(): compatibility shim kept for test call sites HANDOFF_ITEM_SCHEMA: agent field changed to required=False (target is now derived from workflow stage order, not hardcoded in config). --- .../_templates/agents/architect/config.yaml | 23 +- .../_templates/agents/architect/template.md | 2 + .../_templates/agents/designer/config.yaml | 29 +- .../_templates/agents/designer/template.md | 2 + .../_templates/agents/engineer/config.yaml | 31 +- .../_templates/agents/engineer/template.md | 2 + .../_templates/agents/product/config.yaml | 26 +- .../_templates/agents/product/template.md | 2 + .../_templates/agents/release/config.yaml | 16 +- .../_templates/agents/release/template.md | 2 + .../_templates/agents/tester/config.yaml | 23 +- .../_templates/agents/tester/template.md | 2 + src/vstack/agents/config.py | 2 +- src/vstack/agents/generator.py | 212 ++++- tests/vstack/agents/test_generator.py | 812 ++++++++++++------ tests/vstack/agents/test_role_wiring.py | 24 +- 16 files changed, 837 insertions(+), 373 deletions(-) diff --git a/src/vstack/_templates/agents/architect/config.yaml b/src/vstack/_templates/agents/architect/config.yaml index 2f67021..b8129c2 100644 --- a/src/vstack/_templates/agents/architect/config.yaml +++ b/src/vstack/_templates/agents/architect/config.yaml @@ -20,22 +20,23 @@ model: - GPT-5.3-Codex (copilot) - Claude Opus 4.7 (copilot) agents: ["*"] -handoffs: - - label: "Go to next stage: Design" - agent: designer +target: vscode +defaults: + handoffs: prompt: > 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 + artifacts: + dir: architecture + input: + - product/**/*.md + output: + - path: overview.md + baseline: true + - path: adr/*.md + baseline: true user-invocable: true # Optional when needed: diff --git a/src/vstack/_templates/agents/architect/template.md b/src/vstack/_templates/agents/architect/template.md index 141a1e9..de2e43b 100644 --- a/src/vstack/_templates/agents/architect/template.md +++ b/src/vstack/_templates/agents/architect/template.md @@ -105,6 +105,8 @@ what work is needed: {{AGENT_ARTIFACTS_OUTPUT}} +{{AGENT_ARTIFACTS_BASELINE}} + 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. diff --git a/src/vstack/_templates/agents/designer/config.yaml b/src/vstack/_templates/agents/designer/config.yaml index bdd0bf9..cf1e64e 100644 --- a/src/vstack/_templates/agents/designer/config.yaml +++ b/src/vstack/_templates/agents/designer/config.yaml @@ -18,26 +18,27 @@ model: - Claude Sonnet 4.6 (copilot) - GPT-5.3-Codex (copilot) agents: ["*"] -handoffs: - - label: "Go to next stage: Engineering" - agent: engineer +target: vscode +defaults: + handoffs: 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. -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 + artifacts: + dir: design + input: + - architecture/**/*.md + output: + - path: overview.md + baseline: true + - path: ux.md + baseline: true + notes: frontend/fullstack scope only + - path: "**/*.md" + notes: additional detail docs per component, model, system, or domain (when scope warrants it) user-invocable: true # Optional when needed: diff --git a/src/vstack/_templates/agents/designer/template.md b/src/vstack/_templates/agents/designer/template.md index f264149..fb79d5f 100644 --- a/src/vstack/_templates/agents/designer/template.md +++ b/src/vstack/_templates/agents/designer/template.md @@ -121,6 +121,8 @@ what work is needed: {{AGENT_ARTIFACTS_OUTPUT}} +{{AGENT_ARTIFACTS_BASELINE}} + 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. diff --git a/src/vstack/_templates/agents/engineer/config.yaml b/src/vstack/_templates/agents/engineer/config.yaml index ef5d39f..676e789 100644 --- a/src/vstack/_templates/agents/engineer/config.yaml +++ b/src/vstack/_templates/agents/engineer/config.yaml @@ -19,28 +19,27 @@ model: - GPT-5.3-Codex (copilot) - Claude Sonnet 4.6 (copilot) agents: ["*"] -handoffs: - - label: "Go to next stage: Verification" - agent: tester +target: vscode +defaults: + handoffs: 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. -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 + 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 user-invocable: true # Optional when needed: diff --git a/src/vstack/_templates/agents/engineer/template.md b/src/vstack/_templates/agents/engineer/template.md index babef0b..44ae66f 100644 --- a/src/vstack/_templates/agents/engineer/template.md +++ b/src/vstack/_templates/agents/engineer/template.md @@ -112,6 +112,8 @@ what work is needed: {{AGENT_ARTIFACTS_OUTPUT}} +{{AGENT_ARTIFACTS_BASELINE}} + 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. diff --git a/src/vstack/_templates/agents/product/config.yaml b/src/vstack/_templates/agents/product/config.yaml index ecf5688..625a7c5 100644 --- a/src/vstack/_templates/agents/product/config.yaml +++ b/src/vstack/_templates/agents/product/config.yaml @@ -19,23 +19,25 @@ model: - GPT-5.3-Codex (copilot) - Claude Opus 4.7 (copilot) agents: ["*"] -handoffs: - - label: "Go to next stage: Architecture" - agent: architect +target: vscode +defaults: + handoffs: prompt: > 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 + artifacts: + dir: product + output: + - path: vision.md + baseline: true + - path: requirements.md + baseline: true + - path: roadmap.md + baseline: true + - path: changes/*.md + - path: issues/*.md user-invocable: true # Optional when needed: diff --git a/src/vstack/_templates/agents/product/template.md b/src/vstack/_templates/agents/product/template.md index 4008026..d0944a2 100644 --- a/src/vstack/_templates/agents/product/template.md +++ b/src/vstack/_templates/agents/product/template.md @@ -90,6 +90,8 @@ Handoffs you own: {{AGENT_ARTIFACTS_OUTPUT}} +{{AGENT_ARTIFACTS_BASELINE}} + 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. diff --git a/src/vstack/_templates/agents/release/config.yaml b/src/vstack/_templates/agents/release/config.yaml index 84b9420..b3c71b0 100644 --- a/src/vstack/_templates/agents/release/config.yaml +++ b/src/vstack/_templates/agents/release/config.yaml @@ -18,14 +18,16 @@ model: - Claude Sonnet 4.6 (copilot) - GPT-5.3-Codex (copilot) agents: ["*"] -artifacts: - dir: releases - input: - - "**/*.md" - output: - - path: "*.md" - notes: includes release notes and sign-off record target: vscode +defaults: + artifacts: + dir: releases + input: + - "**/*.md" + output: + - path: "*.md" + notes: includes release notes and sign-off record + baseline: false user-invocable: true # Optional when needed: diff --git a/src/vstack/_templates/agents/release/template.md b/src/vstack/_templates/agents/release/template.md index d644644..ef6679b 100644 --- a/src/vstack/_templates/agents/release/template.md +++ b/src/vstack/_templates/agents/release/template.md @@ -87,6 +87,8 @@ and wait for explicit user routing decisions. {{AGENT_ARTIFACTS_OUTPUT}} +{{AGENT_ARTIFACTS_BASELINE}} + 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. diff --git a/src/vstack/_templates/agents/tester/config.yaml b/src/vstack/_templates/agents/tester/config.yaml index 433853a..7cfe6aa 100644 --- a/src/vstack/_templates/agents/tester/config.yaml +++ b/src/vstack/_templates/agents/tester/config.yaml @@ -18,22 +18,21 @@ model: - Claude Sonnet 4.6 (copilot) - GPT-5.3-Codex (copilot) agents: ["*"] -handoffs: - - label: "Go to next stage: Release readiness" - agent: release +target: vscode +defaults: + handoffs: 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. -artifacts: - dir: reports - input: - - architecture/**/*.md - - design/**/*.md - output: - - "**/*.md" - - ./tests/**/* -target: vscode + artifacts: + dir: reports + input: + - architecture/**/*.md + - design/**/*.md + output: + - path: "**/*.md" + - path: ./tests/**/* user-invocable: true # Optional when needed: diff --git a/src/vstack/_templates/agents/tester/template.md b/src/vstack/_templates/agents/tester/template.md index 6be8546..4b3005e 100644 --- a/src/vstack/_templates/agents/tester/template.md +++ b/src/vstack/_templates/agents/tester/template.md @@ -101,6 +101,8 @@ what work is needed: {{AGENT_ARTIFACTS_OUTPUT}} +{{AGENT_ARTIFACTS_BASELINE}} + 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. diff --git a/src/vstack/agents/config.py b/src/vstack/agents/config.py index a4b832b..984d215 100644 --- a/src/vstack/agents/config.py +++ b/src/vstack/agents/config.py @@ -17,7 +17,7 @@ HANDOFF_ITEM_SCHEMA = FrontmatterSchema( [ FieldSpec("label", required=True), - FieldSpec("agent", required=True, quoted=False), + FieldSpec("agent", required=False, quoted=False), FieldSpec("prompt", required=True), FieldSpec("send", type="bool"), FieldSpec("model"), diff --git a/src/vstack/agents/generator.py b/src/vstack/agents/generator.py index f2b08d5..4887964 100644 --- a/src/vstack/agents/generator.py +++ b/src/vstack/agents/generator.py @@ -54,6 +54,7 @@ def __init__( templates_root: Path | None = None, *, artifacts_root: str = ARTIFACTS_DOCS_ROOT, + workflow_stages: list[dict] | None = None, ) -> None: """Create an agent generator bound to *templates_root*. @@ -65,18 +66,25 @@ def __init__( via ``artifacts.root`` in ``.vstack/config.yaml`` to relocate generated artifact paths (e.g. ``"documentation"`` instead of ``"docs"``). + workflow_stages: Ordered list of pipeline stage dicts read from + ``workflow.stages`` in ``.vstack/config.yaml``. Each dict has + ``role``, ``gate``, and ``handoff_prompt`` keys. When ``None`` + or empty the generator falls back to v3 behaviour: a generic + handoff label without an explicit ``agent:`` target. """ super().__init__( AGENT_TYPE, templates_root if templates_root is not None else TEMPLATES_ROOT ) self.artifacts_root = artifacts_root + self.workflow_stages: list[dict] = workflow_stages or [] def template_partials(self, tmpl_dir: Path) -> dict[str, str]: """Inject per-template artifact placeholder tokens. - Returns a dict with four keys: + Returns a dict with five keys: ``AGENT_ARTIFACTS_INPUT``, ``AGENT_ARTIFACTS_OUTPUT``, - ``AGENT_ARTIFACTS_INPUT_COMMENTS``, ``AGENT_ARTIFACTS_OUTPUT_COMMENTS``. + ``AGENT_ARTIFACTS_INPUT_COMMENTS``, ``AGENT_ARTIFACTS_OUTPUT_COMMENTS``, + and ``AGENT_ARTIFACTS_BASELINE``. """ from vstack.frontmatter import FrontmatterParser @@ -104,38 +112,168 @@ def template_partials(self, tmpl_dir: Path) -> dict[str, str]: if not isinstance(raw_outputs, list): raw_outputs = [] - input_entries = [ - {"path": f"{doc_root}/{item}", "notes": ""} + input_entries: list[dict[str, str | bool]] = [ + {"path": f"{doc_root}/{item}", "notes": "", "baseline": False} for item in raw_inputs if isinstance(item, str) ] - output_entries = self._resolve_output_entries(raw_outputs, doc_root, agent_dir) + output_entries = self._resolve_output_entries(raw_outputs, agent_dir) + baseline_entries = [e for e in output_entries if e.get("baseline")] 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 ""), + "AGENT_ARTIFACTS_BASELINE": self._build_baseline_section(baseline_entries), } - @staticmethod + def _extract_defaults(self, config: dict) -> dict: + """Return the parsed ``defaults:`` block from *config*, or an empty dict. + + The ``defaults:`` value may be stored as a raw indented YAML string by + the minimal frontmatter parser. This helper re-parses it when needed + and always returns a plain dict. + + :param config: Raw config dict as returned by ``load_artifact_config``. + :returns: Parsed ``defaults`` dict, or ``{}`` when absent or unparseable. + """ + from vstack.frontmatter import FrontmatterParser + + defaults = config.get("defaults") or {} + if isinstance(defaults, str) and defaults.strip(): + dedented = "\n".join( + line[2:] if line.startswith(" ") else line for line in defaults.split("\n") + ) + defaults = FrontmatterParser.parse_yaml(dedented) or {} + return defaults if isinstance(defaults, dict) else {} + + def load_artifact_config(self, tmpl_dir: Path) -> dict: + """Load agent config and inject workflow-derived ``handoffs`` into the result. + + Extends :meth:`~vstack.artifacts.generator.GenericArtifactGenerator.load_artifact_config` + by extracting the ``defaults:`` block from the agent's ``config.yaml``, + resolving the handoff prompt from ``defaults.handoffs.prompt``, and + injecting a fully resolved ``handoffs`` list that the frontmatter + serializer can emit directly. The ``defaults:`` key is removed from the + config so it never appears in the generated ``agent.md`` frontmatter. + + When no workflow stages are configured the fallback ``handoffs`` list + uses the agent's own prompt without an explicit ``agent:`` target, + preserving v3 behaviour. + """ + config = super().load_artifact_config(tmpl_dir) + agent_role = tmpl_dir.name + defaults = self._extract_defaults(config) + config.pop("defaults", None) + # Re-expose artifacts at top level so template_partials can read them + # via the standard artifact_config.get("artifacts") path. + if "artifacts" not in config: + artifacts_from_defaults = defaults.get("artifacts") + if artifacts_from_defaults: + config["artifacts"] = artifacts_from_defaults + handoffs_block = defaults.get("handoffs") or {} + if isinstance(handoffs_block, str) and handoffs_block.strip(): + from vstack.frontmatter import FrontmatterParser + + dedented = "\n".join( + line[2:] if line.startswith(" ") else line for line in handoffs_block.split("\n") + ) + handoffs_block = FrontmatterParser.parse_yaml(dedented) or {} + if isinstance(handoffs_block, dict): + handoff_prompt: str = str(handoffs_block.get("prompt", "") or "") + else: + handoff_prompt = "" + handoffs = self._resolve_handoffs(agent_role, handoff_prompt) + if handoffs: + config["handoffs"] = handoffs + return config + + def _resolve_handoffs(self, agent_role: str, handoff_prompt: str) -> list[dict[str, str]]: + """Resolve the handoffs list for *agent_role* from workflow stages. + + Each workflow stage may define one or more handoffs under + ``workflow.stages[].handoffs``. Each handoff dict has ``prompt`` + (required), and optional ``agent`` and ``label`` overrides. + + When no explicit ``agent`` is set on a handoff, it defaults to the + next stage in the workflow sequence. When no explicit ``label`` is + set, it defaults to ``"Go to next stage: {Agent}"``. + + The *handoff_prompt* argument (from the agent template's own + ``defaults.handoffs.prompt``) overrides the ``prompt`` of the first + handoff that targets the natural next stage (no ``agent`` override), + allowing per-template prompt customisation without editing the central + config. + + :param agent_role: Role name (template directory name). + :param handoff_prompt: Raw prompt text from the agent's ``config.yaml``. + :returns: List of handoff dicts suitable for frontmatter serialization, + or an empty list when this is the last stage or no prompts exist. + """ + if not self.workflow_stages: + # No workflow configured — cannot emit valid ``agent:`` targets. + return [] + + roles = [s["role"] for s in self.workflow_stages] + try: + idx = roles.index(agent_role) + except ValueError: + return [] + if idx >= len(roles) - 1: + return [] + next_role = roles[idx + 1] + + stage_handoffs: list[dict[str, str]] = self.workflow_stages[idx].get("handoffs", []) + if not isinstance(stage_handoffs, list): + stage_handoffs = [] + + result: list[dict[str, str]] = [] + agent_override_applied = False + + for h in stage_handoffs: + if not isinstance(h, dict): + continue + h_agent = str(h.get("agent", "") or "").strip() + target_agent = h_agent or next_role + + # Apply per-agent handoff_prompt override to the first handoff that + # targets the natural next stage (no explicit agent override). + if handoff_prompt.strip() and not h_agent and not agent_override_applied: + prompt = handoff_prompt.strip() + agent_override_applied = True + else: + prompt = str(h.get("prompt", "") or "").strip() + + if not prompt: + continue + + label = str(h.get("label", "") or "").strip() or ( + f"Go to next stage: {target_agent.capitalize()}" + ) + result.append({"label": label, "agent": target_agent, "prompt": prompt}) + + return result + def _resolve_output_entries( - raw_outputs: list, doc_root: str, agent_dir: str - ) -> list[dict[str, str]]: + self, raw_outputs: list, agent_dir: str + ) -> list[dict[str, str | bool]]: """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. + :returns: List of ``{"path": ..., "notes": ..., "baseline": ...}`` dicts. """ - result: list[dict[str, str]] = [] + doc_root = self.artifacts_root + result: list[dict[str, str | bool]] = [] for item in raw_outputs: if isinstance(item, str): - path, notes = item, "" + path, notes, baseline = item, "", False elif isinstance(item, dict): - path, notes = str(item.get("path", "")), str(item.get("notes", "")) + path = str(item.get("path", "")) + notes = str(item.get("notes", "")) + baseline = bool(item.get("baseline", False)) else: continue @@ -147,11 +285,10 @@ def _resolve_output_entries( else: display = path - result.append({"path": display, "notes": notes}) + result.append({"path": display, "notes": notes, "baseline": baseline}) return result - @staticmethod - def _build_table(entries: list[dict[str, str]]) -> str: + def _build_table(self, entries: list[dict[str, str | bool]]) -> str: """Build a Markdown table from normalised artifact entry dicts. Produces a single-column ``Artifact`` table when no entry has notes, @@ -159,7 +296,7 @@ def _build_table(entries: list[dict[str, str]]) -> str: 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] + notes_cells = [str(e.get("notes", "")) for e in entries] has_notes = any(notes_cells) if has_notes: @@ -183,12 +320,47 @@ def _build_table(entries: list[dict[str, str]]) -> str: return "\n".join(lines) - @staticmethod - def _build_section(heading: str, entries: list[dict[str, str]]) -> str: + def _build_section(self, heading: str, entries: list[dict[str, str | bool]]) -> 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)}" + return f"### {heading}\n\n{self._build_table(entries)}" + + def _build_baseline_section(self, entries: list[dict[str, str | bool]]) -> str: + """Build the ``### baseline docs you maintain`` subsection. + + Renders a table of output artifacts flagged with ``baseline: true``. + Returns an empty string when no baseline entries are present. + + :param entries: Resolved output entries where ``baseline`` is ``True``. + :returns: Markdown subsection string, or empty string. + """ + if not entries: + return "" + return ( + "### baseline docs you maintain\n\n" + "Keep these files current. Update them whenever the relevant scope, " + "design, or implementation changes — do not let them go stale.\n\n" + f"{self._build_table(entries)}" + ) + + def _build_handoffs(self, agent_role: str, handoff_prompt: str) -> str: + """Build the ``handoffs:`` frontmatter block for this agent. + + .. deprecated:: + Use :meth:`_resolve_handoffs` instead. This method is retained + only for backward compatibility with any direct call sites in tests. + """ + entries = self._resolve_handoffs(agent_role, handoff_prompt) + if not entries: + return "" + entry = entries[0] + lines = [f'handoffs:\n - label: "{entry["label"]}"'] + if "agent" in entry: + lines.append(f" agent: {entry['agent']}") + lines.append(" prompt: >") + lines.extend(f" {line}" for line in entry["prompt"].splitlines()) + return "\n".join(lines) diff --git a/tests/vstack/agents/test_generator.py b/tests/vstack/agents/test_generator.py index 02b821c..b65154b 100644 --- a/tests/vstack/agents/test_generator.py +++ b/tests/vstack/agents/test_generator.py @@ -3,286 +3,556 @@ from __future__ import annotations from pathlib import Path +from typing import Any from vstack.agents.generator import AgentGenerator from vstack.constants import ARTIFACTS_DOCS_ROOT class TestAgentGenerator: - """Test cases for AgentGenerator.""" + """Tests for AgentGenerator and all its methods.""" 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 + class TestTemplatePartials: + """Tests for AgentGenerator.template_partials.""" + + def test_returns_five_keys(self, tmp_path: Path) -> None: + """template_partials always returns all five 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", + "AGENT_ARTIFACTS_BASELINE", + } + + def test_empty_when_no_artifacts_block(self, tmp_path: Path) -> None: + """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_input_prefixed_with_docs_root(self, tmp_path: Path) -> None: + """Input paths are prefixed with ARTIFACTS_DOCS_ROOT.""" + tmpl_dir = tmp_path / "architect" + tmpl_dir.mkdir() + (tmpl_dir / "config.yaml").write_text( + "name: architect\ndefaults:\n artifacts:\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_output_prefixed_with_root_and_dir(self, tmp_path: Path) -> None: + """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\ndefaults:\n artifacts:\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_output_verbatim_when_no_dir(self, tmp_path: Path) -> None: + """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\ndefaults:\n artifacts:\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_dotslash_strips_prefix_with_dir(self, tmp_path: Path) -> None: + """./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\ndefaults:\n artifacts:\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_input_comments_from_config(self, tmp_path: Path) -> None: + """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\ndefaults:\n artifacts:\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_output_comments_from_config(self, tmp_path: Path) -> None: + """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\ndefaults:\n artifacts:\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_handles_non_dict_artifacts_gracefully(self, tmp_path: Path) -> None: + """template_partials handles malformed (non-dict) artifacts without crashing.""" + tmpl_dir = tmp_path / "broken" + tmpl_dir.mkdir() + (tmpl_dir / "config.yaml").write_text( + "name: broken\ndefaults:\n artifacts:\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_handles_non_list_input_output_gracefully(self, tmp_path: Path) -> None: + """Scalar input/output values are treated as empty lists.""" + tmpl_dir = tmp_path / "scalar" + tmpl_dir.mkdir() + (tmpl_dir / "config.yaml").write_text( + "name: scalar\ndefaults:\n artifacts:\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: + """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\ndefaults:\n artifacts:\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_uses_custom_artifacts_root(self, tmp_path: Path) -> None: + """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\ndefaults:\n artifacts:\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 TestExtractDefaults: + """Tests for AgentGenerator._extract_defaults.""" + + def test_returns_empty_dict_when_no_defaults_key(self) -> None: + """Returns an empty dict when the config has no defaults key.""" + assert AgentGenerator()._extract_defaults({}) == {} + + def test_returns_empty_dict_when_defaults_is_none(self) -> None: + """Returns an empty dict when defaults is None.""" + assert AgentGenerator()._extract_defaults({"defaults": None}) == {} + + def test_returns_dict_as_is(self) -> None: + """Returns the dict unchanged when defaults is already a plain dict.""" + assert AgentGenerator()._extract_defaults({"defaults": {"artifacts": {}}}) == { + "artifacts": {} + } + + def test_returns_empty_dict_when_defaults_is_non_dict_non_string(self) -> None: + """Returns empty dict when defaults is neither a string nor a dict.""" + assert AgentGenerator()._extract_defaults({"defaults": 42}) == {} + + def test_reparses_raw_indented_string(self) -> None: + """Re-parses a raw indented YAML string produced by the minimal parser.""" + raw_defaults = " artifacts:\n dir: design\n" + result = AgentGenerator()._extract_defaults({"defaults": raw_defaults}) + assert isinstance(result, dict) + assert "artifacts" in result + + class TestLoadArtifactConfig: + """Tests for AgentGenerator.load_artifact_config.""" + + def test_handoffs_block_as_list_yields_empty_prompt(self, tmp_path: Path) -> None: + """When handoffs parses as a list (not a dict), handoff_prompt falls back to empty.""" + tmpl_dir = tmp_path / "product" + tmpl_dir.mkdir() + (tmpl_dir / "config.yaml").write_text( + "defaults:\n handoffs:\n - prompt: foo\n", + encoding="utf-8", + ) + config = AgentGenerator().load_artifact_config(tmpl_dir) + assert "handoffs" not in config + + def test_handoffs_block_not_a_dict_or_string_yields_empty_prompt( + self, tmp_path: Path + ) -> None: + """When handoffs_block is neither a dict nor a str, handoff_prompt falls back to empty.""" + tmpl_dir = tmp_path / "product" + tmpl_dir.mkdir() + # Inject a stage that sets handoffs to a plain list value so the block-scalar + # path is triggered but is not a dict — a bare list under defaults.handoffs. + # Write config with an inline list under handoffs to hit the else branch. + (tmpl_dir / "config.yaml").write_text( + "name: product\ndefaults:\n handoffs: []\n", + encoding="utf-8", + ) + config = AgentGenerator().load_artifact_config(tmpl_dir) + assert "handoffs" not in config + + def test_handoffs_injected_when_workflow_resolves(self, tmp_path: Path) -> None: + """Resolved handoffs are injected into config when workflow stages are present.""" + tmpl_dir = tmp_path / "architect" + tmpl_dir.mkdir() + (tmpl_dir / "config.yaml").write_text( + "name: architect\n", + encoding="utf-8", + ) + stages: list[dict[str, Any]] = [ + { + "role": "architect", + "gate": "required", + "handoffs": [{"prompt": "Arch done.", "agent": "", "label": ""}], + }, + {"role": "designer", "gate": "optional", "handoffs": []}, + ] + gen = AgentGenerator(workflow_stages=stages) + config = gen.load_artifact_config(tmpl_dir) + assert "handoffs" in config + assert config["handoffs"][0]["agent"] == "designer" + + class TestResolveOutputEntries: + """Tests for AgentGenerator._resolve_output_entries.""" + + def test_plain_string_with_dir_gets_root_and_dir_prefix(self) -> None: + """Plain string items are prefixed with root/dir when dir is set.""" + result = AgentGenerator()._resolve_output_entries(["overview.md"], "architecture") + assert result == [ + {"path": "docs/architecture/overview.md", "notes": "", "baseline": False} + ] + + def test_plain_string_without_dir_is_verbatim(self) -> None: + """Plain string items are used verbatim when no dir is set.""" + result = AgentGenerator()._resolve_output_entries(["src/**/*"], "") + assert result == [{"path": "src/**/*", "notes": "", "baseline": False}] + + def test_non_string_non_dict_item_is_skipped(self) -> None: + """Non-string, non-dict output items are silently skipped.""" + result = AgentGenerator()._resolve_output_entries([42, None], "architecture") + assert result == [] + + def test_dotslash_prefix_strips_and_uses_verbatim(self) -> None: + """./path items strip the ./ and bypass dir prefix.""" + result = AgentGenerator()._resolve_output_entries(["./tests/**/*"], "reports") + assert result == [{"path": "tests/**/*", "notes": "", "baseline": False}] + + def test_dict_item_with_notes_and_dir(self) -> None: + """Dict items with notes are resolved with dir prefix.""" + result = AgentGenerator()._resolve_output_entries( + [{"path": "ux.md", "notes": "frontend only"}], "design" + ) + assert result == [ + {"path": "docs/design/ux.md", "notes": "frontend only", "baseline": False} + ] + + def test_dict_item_with_dotslash_path(self) -> None: + """Dict items with ./path bypass the dir prefix.""" + result = AgentGenerator()._resolve_output_entries( + [{"path": "./issues/rca.md", "notes": "on issue"}], "reports" + ) + assert result == [{"path": "issues/rca.md", "notes": "on issue", "baseline": False}] + + def test_glob_with_slash_is_relative_to_dir(self) -> None: + """Glob paths like adr/*.md are prefixed with root/dir.""" + result = AgentGenerator()._resolve_output_entries(["adr/*.md"], "architecture") + assert result == [ + {"path": "docs/architecture/adr/*.md", "notes": "", "baseline": False} + ] + + def test_double_glob_relative_to_dir(self) -> None: + """**/*.md is prefixed with root/dir when dir is set.""" + result = AgentGenerator()._resolve_output_entries(["**/*.md"], "reports") + assert result == [{"path": "docs/reports/**/*.md", "notes": "", "baseline": False}] + + class TestResolveHandoffs: + """Tests for AgentGenerator._resolve_handoffs.""" + + def test_returns_empty_when_no_workflow_and_no_prompt(self) -> None: + """Returns empty list when no workflow is configured and no prompt given.""" + assert AgentGenerator()._resolve_handoffs("architect", "") == [] + + def test_no_handoff_without_workflow(self) -> None: + """Returns empty list when no workflow is configured, even with a prompt. + + A handoff without an explicit ``agent:`` target is invalid per the + VS Code agent schema, so none is emitted when no workflow is configured. + """ + assert AgentGenerator()._resolve_handoffs("architect", "Do some work.") == [] + + def test_with_workflow_finds_next_role(self) -> None: + """Returns handoff with correct next agent when workflow is configured.""" + stages: list[dict[str, Any]] = [ + { + "role": "product", + "gate": "required", + "handoffs": [{"prompt": "Product done.", "agent": "", "label": ""}], + }, + { + "role": "architect", + "gate": "required", + "handoffs": [{"prompt": "Arch done.", "agent": "", "label": ""}], + }, + { + "role": "designer", + "gate": "optional", + "handoffs": [{"prompt": "Design done.", "agent": "", "label": ""}], + }, + ] + gen = AgentGenerator(workflow_stages=stages) + result = gen._resolve_handoffs("architect", "Arch done.") + assert len(result) == 1 + assert result[0]["agent"] == "designer" + assert result[0]["label"] == "Go to next stage: Designer" + + def test_last_stage_returns_empty(self) -> None: + """Returns empty list when the agent is the last stage in the workflow.""" + stages: list[dict[str, Any]] = [ + { + "role": "architect", + "gate": "required", + "handoffs": [{"prompt": "Arch done.", "agent": "", "label": ""}], + }, + {"role": "release", "gate": "required", "handoffs": []}, + ] + gen = AgentGenerator(workflow_stages=stages) + assert gen._resolve_handoffs("release", "") == [] + + def test_unknown_role_returns_empty(self) -> None: + """Returns empty list when the agent role is not found in workflow stages.""" + stages: list[dict[str, Any]] = [ + { + "role": "product", + "gate": "required", + "handoffs": [{"prompt": "Done.", "agent": "", "label": ""}], + } + ] + gen = AgentGenerator(workflow_stages=stages) + assert gen._resolve_handoffs("unknown", "Some prompt.") == [] + + def test_workflow_prompt_used_as_fallback(self) -> None: + """Uses the workflow handoffs[0].prompt when the agent's own prompt is empty.""" + stages: list[dict[str, Any]] = [ + { + "role": "engineer", + "gate": "required", + "handoffs": [{"prompt": "From workflow.", "agent": "", "label": ""}], + }, + {"role": "tester", "gate": "required", "handoffs": []}, + ] + gen = AgentGenerator(workflow_stages=stages) + result = gen._resolve_handoffs("engineer", "") + assert len(result) == 1 + assert "From workflow." in result[0]["prompt"] + + def test_multiple_handoffs_per_stage(self) -> None: + """All handoff entries are returned when a stage defines multiple.""" + stages: list[dict[str, Any]] = [ + { + "role": "architect", + "gate": "required", + "handoffs": [ + {"prompt": "Go to designer.", "agent": "", "label": ""}, + { + "prompt": "Skip to engineer.", + "agent": "engineer", + "label": "Skip design", + }, + ], + }, + {"role": "designer", "gate": "optional", "handoffs": []}, + {"role": "engineer", "gate": "required", "handoffs": []}, + ] + gen = AgentGenerator(workflow_stages=stages) + result = gen._resolve_handoffs("architect", "") + assert len(result) == 2 + assert result[0]["agent"] == "designer" + assert result[1]["agent"] == "engineer" + assert result[1]["label"] == "Skip design" + + def test_empty_workflow_prompt_returns_empty(self) -> None: + """Returns empty when both agent prompt and workflow handoffs prompts are empty.""" + stages: list[dict[str, Any]] = [ + { + "role": "engineer", + "gate": "required", + "handoffs": [{"prompt": "", "agent": "", "label": ""}], + }, + {"role": "tester", "gate": "required", "handoffs": []}, + ] + gen = AgentGenerator(workflow_stages=stages) + assert gen._resolve_handoffs("engineer", "") == [] + + def test_non_list_stage_handoffs_returns_empty(self) -> None: + """Returns empty list when stage handoffs value is not a list.""" + stages: list[dict[str, Any]] = [ + {"role": "engineer", "gate": "required", "handoffs": "bad-value"}, + {"role": "tester", "gate": "required", "handoffs": []}, + ] + gen = AgentGenerator(workflow_stages=stages) + assert gen._resolve_handoffs("engineer", "") == [] + + def test_non_dict_handoff_entry_is_skipped(self) -> None: + """Non-dict handoff entries are skipped.""" + stages: list[dict[str, Any]] = [ + { + "role": "engineer", + "gate": "required", + "handoffs": ["not-a-dict", {"prompt": "Go.", "agent": "", "label": ""}], + }, + {"role": "tester", "gate": "required", "handoffs": []}, + ] + gen = AgentGenerator(workflow_stages=stages) + result = gen._resolve_handoffs("engineer", "") + assert len(result) == 1 + assert result[0]["agent"] == "tester" + + class TestBuildHandoffs: + """Tests for AgentGenerator._build_handoffs (compatibility shim).""" + + def test_returns_empty_string_when_no_prompt(self) -> None: + """Returns empty string when no workflow and no prompt.""" + assert AgentGenerator()._build_handoffs("architect", "") == "" + + def test_returns_empty_string_without_workflow(self) -> None: + """Returns empty string when no workflow is configured, even with a prompt.""" + assert AgentGenerator()._build_handoffs("architect", "Work done.") == "" + + def test_returns_handoff_string_with_agent(self) -> None: + """Returns handoffs block with agent key when workflow is configured.""" + stages: list[dict[str, Any]] = [ + { + "role": "architect", + "gate": "required", + "handoffs": [{"prompt": "", "agent": "", "label": ""}], + }, + {"role": "designer", "gate": "optional", "handoffs": []}, + ] + gen = AgentGenerator(workflow_stages=stages) + result = gen._build_handoffs("architect", "Work done.") + assert "handoffs:" in result + assert "agent: designer" in result + + class TestBuildTable: + """Tests for AgentGenerator._build_table.""" + + def test_single_column_when_no_notes(self) -> None: + """A single-column Artifact table is produced when no entry has notes.""" + table = AgentGenerator()._build_table( + [{"path": "docs/foo.md", "notes": "", "baseline": False}] + ) + 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: + """A two-column Artifact | Notes table is produced when any entry has notes.""" + entries: list[dict[str, str | bool]] = [ + {"path": "docs/foo.md", "notes": "", "baseline": False}, + {"path": "docs/bar.md", "notes": "important", "baseline": False}, + ] + table = AgentGenerator()._build_table(entries) + assert "Notes" in table.splitlines()[0] + + def test_rows_have_equal_length(self) -> None: + """All rows in the table have equal string length.""" + entries: list[dict[str, str | bool]] = [ + {"path": "docs/a.md", "notes": "", "baseline": False}, + {"path": "docs/b.md", "notes": "a very long note here", "baseline": False}, + ] + table = AgentGenerator()._build_table(entries) + row_lengths = {len(line) for line in table.splitlines()} + assert len(row_lengths) == 1 + + class TestBuildSection: + """Tests for AgentGenerator._build_section and _build_baseline_section.""" + + def test_returns_empty_string_when_no_entries(self) -> None: + """An empty string is returned when entries list is empty.""" + assert AgentGenerator()._build_section("input", []) == "" + + def test_includes_heading_and_table(self) -> None: + """The section includes the heading and table.""" + section = AgentGenerator()._build_section( + "input", [{"path": "docs/a.md", "notes": "", "baseline": False}] + ) + assert section.startswith("### input") + assert "`docs/a.md`" in section + + def test_heading_matches_argument(self) -> None: + """The section heading matches the heading argument.""" + section = AgentGenerator()._build_section( + "output", [{"path": "docs/b.md", "notes": ""}] + ) + assert "### output" in section + + def test_baseline_section_with_entries_contains_heading(self) -> None: + """_build_baseline_section returns a section with the fixed heading when entries given.""" + entries: list[dict[str, str | bool]] = [ + {"path": "docs/architecture/overview.md", "notes": "", "baseline": True} + ] + section = AgentGenerator()._build_baseline_section(entries) + assert "### baseline docs you maintain" in section + assert "`docs/architecture/overview.md`" in section + + def test_baseline_section_empty_when_no_entries(self) -> None: + """_build_baseline_section returns an empty string when entries list is empty.""" + assert AgentGenerator()._build_baseline_section([]) == "" diff --git a/tests/vstack/agents/test_role_wiring.py b/tests/vstack/agents/test_role_wiring.py index eb74abd..eae808e 100644 --- a/tests/vstack/agents/test_role_wiring.py +++ b/tests/vstack/agents/test_role_wiring.py @@ -4,8 +4,6 @@ from pathlib import Path -from vstack.frontmatter import FrontmatterParser - TEMPLATES_ROOT = Path(__file__).resolve().parents[3] / "src" / "vstack" / "_templates" / "agents" @@ -50,26 +48,34 @@ def test_all_role_templates_reference_concise() -> None: def test_role_configs_follow_stage_handoff_policy() -> None: - """Non-release roles expose forward handoffs, release remains terminal.""" + """Non-release roles expose a handoff prompt under defaults.handoffs, release is terminal.""" roles_with_forward_handoff = ["product", "architect", "designer", "engineer", "tester"] for role in roles_with_forward_handoff: config = _read(f"{role}/config.yaml") + assert "defaults:" in config assert "handoffs:" in config - assert "label:" in config - assert "agent:" in config + assert "prompt:" 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.""" + """Handoff targets in generated agent files should reference known roles.""" + from vstack.frontmatter import FrontmatterParser + + github_dir = Path(__file__).parents[3] / ".github" / "agents" roles = ["product", "architect", "designer", "engineer", "tester", "release"] valid_targets = set(roles) for role in roles: - config = FrontmatterParser.parse_yaml(_read(f"{role}/config.yaml")) - handoffs = config.get("handoffs") or [] + agent_file = github_dir / f"{role}.agent.md" + if not agent_file.exists(): + continue + text = agent_file.read_text(encoding="utf-8") + parsed = FrontmatterParser.parse(text) + handoffs = parsed.metadata.get("handoffs") or [] for handoff in handoffs: target = handoff.get("agent") - assert target in valid_targets, f"{role} has unknown handoff target: {target!r}" + if target is not None: + assert target in valid_targets, f"{role} has unknown handoff target: {target!r}" From 2886ee19851e4f3bb792e54dbc896a823988a3dd Mon Sep 17 00:00:00 2001 From: Erik Schaareman Date: Sat, 9 May 2026 01:27:54 +0200 Subject: [PATCH 03/25] feat(workflow): add workflow contract schema to .vstack/config.yaml MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add a workflow: block to the project config schema. It defines the full pipeline as an ordered list of stages with gate, hitl, and handoffs fields. Stage schema: role: agent name (required) gate: required | optional | skip hitl: always | on-change | never handoffs: single dict or list of dicts with prompt, agent?, label? hitl semantics: always — pipeline pauses for human approval before handoff on-change — pause only when the stage made changes (default for optional) never — pipeline continues without human approval (explicit opt-out) CLI changes: CommandLineInterface._read_workflow_stages(): reads and parses workflow block from .vstack/config.yaml; preserves hitl field when present CommandLineInterface._parse_stage_handoffs(): normalises handoffs to [{prompt, agent, label}] list; supports dict, list, and legacy handoff_prompt flat-key forms CommandService: accepts workflow_stages kwarg, passes to AgentGenerator InitCommand._warn_unknown_workflow_roles(): emits stderr warning for stage roles with no matching agent template (non-fatal) Seed .vstack/config.yaml and the project template with the default six-stage pipeline (commented-out in the template; active in this repo). --- .vstack/config.yaml | 62 +++++++ .../_templates/project/.vstack/config.yaml | 68 +++++++ src/vstack/cli/init.py | 38 ++++ src/vstack/cli/interface.py | 98 +++++++++- src/vstack/cli/service.py | 18 +- tests/vstack/agents/test_generation.py | 22 +++ tests/vstack/cli/test_init.py | 72 ++++++++ tests/vstack/cli/test_interface.py | 167 +++++++++++++++++- 8 files changed, 540 insertions(+), 5 deletions(-) diff --git a/.vstack/config.yaml b/.vstack/config.yaml index 840037a..f6a2fb5 100644 --- a/.vstack/config.yaml +++ b/.vstack/config.yaml @@ -26,3 +26,65 @@ # # artifacts: # root: docs + +# Pipeline workflow — seeded by vstack install, owned by this project. +# +# gate: required — stage always runs +# gate: optional — stage may be skipped when its domain is unaffected +# gate: skip — stage is never executed (explicit opt-out) +# +# hitl: always — pipeline pauses for human approval before handoff (default for required) +# hitl: on-change — pipeline pauses only when the stage made changes (default for optional) +# hitl: never — pipeline continues without human approval (explicit opt-out) +# +workflow: + version: 1 + stages: + - role: product + gate: required + hitl: always + handoffs: + prompt: > + 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. + - role: architect + gate: required + hitl: always + handoffs: + prompt: > + 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. + - role: designer + gate: optional + hitl: on-change + handoffs: + 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. + - role: engineer + gate: required + hitl: always + handoffs: + prompt: > + Implementation is approved. Assess the current state and verify the + implementation as needed — run tests, security checks, and performance + analysis. + - role: tester + gate: required + hitl: always + handoffs: + 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. + - role: release + gate: required + hitl: always + handoffs: + prompt: "" diff --git a/src/vstack/_templates/project/.vstack/config.yaml b/src/vstack/_templates/project/.vstack/config.yaml index 840037a..e02a856 100644 --- a/src/vstack/_templates/project/.vstack/config.yaml +++ b/src/vstack/_templates/project/.vstack/config.yaml @@ -26,3 +26,71 @@ # # artifacts: # root: docs + +# Pipeline workflow — seeded by vstack install, owned by this project. +# +# Edit stages, gates, hitl policy, and handoff prompts to match your team's process. +# version allows vstack to warn on schema changes after upgrades. +# +# gate: required — stage always runs +# gate: optional — stage may be skipped when its domain is unaffected +# gate: skip — stage is never executed (explicit opt-out) +# +# hitl: always — pipeline pauses for human approval before handoff (default for required) +# hitl: on-change — pipeline pauses only when the stage made changes (default for optional) +# hitl: never — pipeline continues without human approval (explicit opt-out) +# +# workflow: +# version: 1 +# stages: +# - role: product +# gate: required +# hitl: always +# handoffs: +# prompt: > +# 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. +# - role: architect +# gate: required +# hitl: always +# handoffs: +# prompt: > +# 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. +# - role: designer +# gate: optional +# hitl: on-change +# handoffs: +# 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. +# - role: engineer +# gate: required +# hitl: always +# handoffs: +# 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. +# - role: tester +# gate: required +# hitl: always +# handoffs: +# 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. +# - role: release +# gate: required +# hitl: always +# handoffs: +# prompt: "" diff --git a/src/vstack/cli/init.py b/src/vstack/cli/init.py index a7f2dd2..5550fa8 100644 --- a/src/vstack/cli/init.py +++ b/src/vstack/cli/init.py @@ -416,6 +416,31 @@ def _print_summary( "take ownership without overwriting" ) + @staticmethod + def _warn_unknown_workflow_roles( + *, + workflow_stages: list[dict[str, str]], + known_agent_names: set[str], + colors, + ) -> None: + """Emit a warning for any workflow stage that references an unknown agent. + + Unknown roles are not fatal — a project may define custom agents. + The warning is informational only and does not affect the exit code. + + :param workflow_stages: Parsed stage list from ``workflow.stages``. + :param known_agent_names: Agent names available in the current template root. + :param colors: CLI colours helper. + """ + for stage in workflow_stages: + role = stage.get("role", "") + if role and role not in known_agent_names: + print( + f" {colors.YELLOW}⚠{colors.RESET} workflow: unknown role " + f"{colors.BOLD}{role!r}{colors.RESET} — no matching agent template found", + file=sys.stderr, + ) + @staticmethod def execute( service: CommandService, @@ -445,6 +470,19 @@ def execute( targeted_force_names = normalize_targeted_names(force_names) targeted_adopt_names = normalize_targeted_names(adopt_names) + # Validate workflow stages against known agent names (warning only). + from vstack.agents.generator import AgentGenerator + + for gen in service.generators: + if isinstance(gen, AgentGenerator) and gen.workflow_stages: + known_names = {p.name for p in gen.find_templates()} + InitCommand._warn_unknown_workflow_roles( + workflow_stages=gen.workflow_stages, + known_agent_names=known_names, + colors=colors, + ) + break + manifest_file, _, existing_entries, new_entries = InitCommand._load_existing_manifest( service=service, install_dir=install_dir, diff --git a/src/vstack/cli/interface.py b/src/vstack/cli/interface.py index ddf1bc7..e181f2d 100644 --- a/src/vstack/cli/interface.py +++ b/src/vstack/cli/interface.py @@ -160,6 +160,99 @@ def _read_artifacts_root(install_dir: Path | None) -> str: return value.strip() return ARTIFACTS_DOCS_ROOT + @staticmethod + def _read_workflow_stages(install_dir: Path | None) -> list[dict]: + """Read ``workflow.stages`` from ``.vstack/config.yaml`` when available. + + Returns an empty list when *install_dir* is ``None``, when the config + file does not exist, or when the ``workflow:`` block is absent or + contains no valid stages. + + Each returned dict has at minimum a ``role`` key. Optional keys are + ``gate``, ``hitl``, and ``handoffs`` (a list of handoff dicts, each with + at minimum a ``prompt`` key and optionally ``agent`` and ``label`` + overrides). Unknown extra keys in the stage dict are silently ignored. + """ + if install_dir is None: + return [] + config_path = install_dir.parent / ".vstack" / "config.yaml" + if not config_path.exists(): + return [] + parsed = FrontmatterParser.parse_yaml(config_path.read_text(encoding="utf-8")) + workflow = parsed.get("workflow", "") + if isinstance(workflow, str) and workflow.strip(): + dedented = "\n".join( + line[2:] if line.startswith(" ") else line for line in workflow.split("\n") + ) + workflow = FrontmatterParser.parse_yaml(dedented) or {} + if not isinstance(workflow, dict): + return [] + stages_raw = workflow.get("stages", []) + if not isinstance(stages_raw, list): + return [] + result: list[dict] = [] + for item in stages_raw: + if isinstance(item, dict) and isinstance(item.get("role"), str): + handoffs = CommandLineInterface._parse_stage_handoffs(item) + stage: dict = { + "role": item["role"], + "gate": str(item.get("gate", "required")), + "handoffs": handoffs, + } + if "hitl" in item: + stage["hitl"] = str(item["hitl"]) + result.append(stage) + return result + + @staticmethod + def _parse_stage_handoffs(item: dict) -> list[dict[str, str]]: + """Parse the ``handoffs`` entry from a workflow stage dict. + + Supports the nested block form (``handoffs: {prompt: ...}``) and + the nested list form (``handoffs: [{prompt: ...}, ...]``). Falls + back to the legacy ``handoff_prompt`` flat key for backward + compatibility with pre-3.x configs. + + Each returned dict has ``prompt``, ``agent``, and ``label`` keys + (empty string when absent). + """ + raw = item.get("handoffs", "") + parsed_block: dict | list | None = None + + if isinstance(raw, str) and raw.strip(): + parsed_block = FrontmatterParser.parse_yaml(raw.strip()) + elif isinstance(raw, (dict, list)): + parsed_block = raw + + if isinstance(parsed_block, dict): + # Single-handoff dict form: {prompt: ..., agent?: ..., label?: ...} + return [ + { + "prompt": str(parsed_block.get("prompt", "") or ""), + "agent": str(parsed_block.get("agent", "") or ""), + "label": str(parsed_block.get("label", "") or ""), + } + ] + if isinstance(parsed_block, list): + # Multi-handoff list form: [{prompt: ..., agent?: ..., label?: ...}, ...] + result = [] + for h in parsed_block: + if isinstance(h, dict): + result.append( + { + "prompt": str(h.get("prompt", "") or ""), + "agent": str(h.get("agent", "") or ""), + "label": str(h.get("label", "") or ""), + } + ) + return result + + # Backward compat: legacy flat ``handoff_prompt`` key. + legacy = str(item.get("handoff_prompt", "") or "") + if legacy: + return [{"prompt": legacy, "agent": "", "label": ""}] + return [] + def run(self) -> int: """Run one CLI invocation and return a process-style status code.""" cli_parser = self._parser_cls() @@ -173,8 +266,11 @@ def run(self) -> int: requires_install_dir=command_config.requires_install_dir, ) artifacts_root = self._read_artifacts_root(resolved_install_dir) + workflow_stages = self._read_workflow_stages(resolved_install_dir) service = self._service_cls( - templates_root=self._templates_root, artifacts_root=artifacts_root + templates_root=self._templates_root, + artifacts_root=artifacts_root, + workflow_stages=workflow_stages, ) commands = build_command_registry(service) effective_only = self._resolve_only_filter( diff --git a/src/vstack/cli/service.py b/src/vstack/cli/service.py index 42ca8d2..8ba7218 100644 --- a/src/vstack/cli/service.py +++ b/src/vstack/cli/service.py @@ -34,7 +34,13 @@ class CommandService: while keeping per-type behavior in ``ArtifactTypeConfig`` definitions. """ - def __init__(self, templates_root: Path, *, artifacts_root: str = ARTIFACTS_DOCS_ROOT) -> None: + def __init__( + self, + templates_root: Path, + *, + artifacts_root: str = ARTIFACTS_DOCS_ROOT, + workflow_stages: list[dict[str, str]] | None = None, + ) -> None: """Create generators for all known artifact families. Per-type generator subclasses are used when available so that @@ -48,10 +54,18 @@ def __init__(self, templates_root: Path, *, artifacts_root: str = ARTIFACTS_DOCS through to :class:`~vstack.agents.generator.AgentGenerator`. Defaults to :data:`~vstack.constants.ARTIFACTS_DOCS_ROOT`; override via ``artifacts.root`` in ``.vstack/config.yaml``. + workflow_stages: Ordered list of pipeline stage dicts read from + the ``workflow.stages`` block in ``.vstack/config.yaml``. + Each dict has ``role``, ``gate``, and ``handoff_prompt`` keys. + When ``None`` or empty the generator falls back to v3 behaviour. """ self.root = templates_root self.generators: list[GenericArtifactGenerator] = [ - AgentGenerator(templates_root, artifacts_root=artifacts_root) + AgentGenerator( + templates_root, + artifacts_root=artifacts_root, + workflow_stages=workflow_stages or [], + ) if tc is AGENT_TYPE else GenericArtifactGenerator(tc, templates_root) for tc in KNOWN_TYPES diff --git a/tests/vstack/agents/test_generation.py b/tests/vstack/agents/test_generation.py index 0a6cc05..cb93d29 100644 --- a/tests/vstack/agents/test_generation.py +++ b/tests/vstack/agents/test_generation.py @@ -13,6 +13,28 @@ class TestAgentGeneration: def test_architect_agent_includes_model_and_handoffs(self, tmp_path: Path) -> None: """Test that architect agent includes model and handoffs.""" + # Seed a minimal workflow config so handoffs include agent targets. + vstack_dir = tmp_path / ".vstack" + vstack_dir.mkdir(parents=True, exist_ok=True) + (vstack_dir / "config.yaml").write_text( + "workflow:\n" + " version: 1\n" + " stages:\n" + " - role: product\n" + " gate: required\n" + " handoffs:\n" + " prompt: Product done.\n" + " - role: architect\n" + " gate: required\n" + " handoffs:\n" + " prompt: Architecture done.\n" + " - role: designer\n" + " gate: optional\n" + " handoffs:\n" + " prompt: Design done.\n", + encoding="utf-8", + ) + result = run_vstack(["install", "--only", "agent", "--target", str(tmp_path)], timeout=60) assert result.returncode == 0, ( f"vstack install --only agent failed:\n{result.stdout}\n{result.stderr}" diff --git a/tests/vstack/cli/test_init.py b/tests/vstack/cli/test_init.py index acf1729..705ba48 100644 --- a/tests/vstack/cli/test_init.py +++ b/tests/vstack/cli/test_init.py @@ -641,3 +641,75 @@ def verify_input(): assert "k8s" in install_single_calls out = capsys.readouterr().out assert "excluded by config" in out + + +class TestWarnUnknownWorkflowRoles: + """Tests for InitCommand._warn_unknown_workflow_roles.""" + + def test_no_output_when_all_roles_known(self, capsys) -> None: + """No warning is emitted when all roles match known agent names.""" + from vstack.cli.constants import Colors + + stages = [{"role": "product"}, {"role": "architect"}] + known = {"product", "architect", "designer"} + InitCommand._warn_unknown_workflow_roles( + workflow_stages=stages, + known_agent_names=known, + colors=Colors, + ) + assert capsys.readouterr().err == "" + + def test_warning_emitted_for_unknown_role(self, capsys) -> None: + """A warning is printed to stderr for each unknown role in workflow stages.""" + from vstack.cli.constants import Colors + + stages = [{"role": "product"}, {"role": "custom-role"}] + known = {"product", "architect"} + InitCommand._warn_unknown_workflow_roles( + workflow_stages=stages, + known_agent_names=known, + colors=Colors, + ) + err = capsys.readouterr().err + assert "custom-role" in err + + def test_empty_role_is_skipped(self, capsys) -> None: + """Stages with empty or missing role key are silently skipped.""" + from vstack.cli.constants import Colors + + stages = [{"role": ""}, {"gate": "required"}] + known: set[str] = set() + InitCommand._warn_unknown_workflow_roles( + workflow_stages=stages, + known_agent_names=known, + colors=Colors, + ) + assert capsys.readouterr().err == "" + + def test_execute_calls_warn_for_unknown_workflow_role( + self, monkeypatch: pytest.MonkeyPatch, tmp_path: Path, capsys + ) -> None: + """execute() emits a warning when a workflow stage references an unknown agent.""" + from vstack.agents.generator import AgentGenerator + + # Patch manifest loading so execute() terminates quickly. + monkeypatch.setattr( + "vstack.cli.init.InitCommand._load_existing_manifest", + staticmethod(lambda **_kwargs: (None, None, None, None)), + ) + + stages = [ + { + "role": "unknown-role", + "gate": "required", + "handoffs": [{"prompt": "x", "agent": "", "label": ""}], + } + ] + gen = AgentGenerator(workflow_stages=stages) + monkeypatch.setattr(AgentGenerator, "find_templates", lambda self: []) + + service = cast(Any, SimpleNamespace(generators=[gen])) + InitCommand.execute(service, tmp_path) + + err = capsys.readouterr().err + assert "unknown-role" in err diff --git a/tests/vstack/cli/test_interface.py b/tests/vstack/cli/test_interface.py index 2e3c2f2..34d14cd 100644 --- a/tests/vstack/cli/test_interface.py +++ b/tests/vstack/cli/test_interface.py @@ -56,9 +56,18 @@ def run(self, *, context: CommandContext): class _Service: """Service construction test double.""" - def __init__(self, *, templates_root, artifacts_root: str = "docs") -> None: + def __init__( + self, + *, + templates_root, + artifacts_root: str = "docs", + workflow_stages=None, + excluded_names=None, + ) -> None: self.templates_root = templates_root self.artifacts_root = artifacts_root + self.workflow_stages = workflow_stages + self.excluded_names = excluded_names class TestCommandLineInterface: @@ -217,7 +226,14 @@ def test_run_passes_artifacts_root_from_config_to_service( captured: list[str] = [] class _CapturingService: - def __init__(self, *, templates_root, artifacts_root: str = "docs") -> None: + def __init__( + self, + *, + templates_root, + artifacts_root: str = "docs", + workflow_stages=None, + excluded_names=None, + ) -> None: captured.append(artifacts_root) monkeypatch.setattr( @@ -235,6 +251,153 @@ def __init__(self, *, templates_root, artifacts_root: str = "docs") -> None: assert captured == ["custom"] +class TestReadWorkflowStages: + """Tests for CommandLineInterface._read_workflow_stages.""" + + def test_returns_empty_when_install_dir_is_none(self) -> None: + """Returns empty list when install_dir is None.""" + assert CommandLineInterface._read_workflow_stages(None) == [] + + def test_returns_empty_when_config_absent(self, tmp_path: Path) -> None: + """Returns empty list when .vstack/config.yaml does not exist.""" + assert CommandLineInterface._read_workflow_stages(tmp_path / ".github") == [] + + def test_returns_empty_when_workflow_block_absent(self, tmp_path: Path) -> None: + """Returns empty list when config.yaml has no workflow block.""" + vstack_dir = tmp_path / ".vstack" + vstack_dir.mkdir() + (vstack_dir / "config.yaml").write_text("exclude:\n prompts: all\n", encoding="utf-8") + assert CommandLineInterface._read_workflow_stages(tmp_path / ".github") == [] + + def test_returns_stages_from_config(self, tmp_path: Path) -> None: + """Returns parsed stage list from workflow.stages block.""" + vstack_dir = tmp_path / ".vstack" + vstack_dir.mkdir() + (vstack_dir / "config.yaml").write_text( + "workflow:\n" + " version: 1\n" + " stages:\n" + " - role: product\n" + " gate: required\n" + " handoffs:\n" + " prompt: Product done.\n" + " - role: architect\n" + " gate: optional\n" + " handoffs:\n" + " prompt: Arch done.\n", + encoding="utf-8", + ) + result = CommandLineInterface._read_workflow_stages(tmp_path / ".github") + assert result == [ + { + "role": "product", + "gate": "required", + "handoffs": [{"prompt": "Product done.", "agent": "", "label": ""}], + }, + { + "role": "architect", + "gate": "optional", + "handoffs": [{"prompt": "Arch done.", "agent": "", "label": ""}], + }, + ] + + def test_preserves_hitl_field(self, tmp_path: Path) -> None: + """hitl field is preserved in the parsed stage dict.""" + vstack_dir = tmp_path / ".vstack" + vstack_dir.mkdir() + (vstack_dir / "config.yaml").write_text( + "workflow:\n" + " version: 1\n" + " stages:\n" + " - role: engineer\n" + " gate: required\n" + " hitl: always\n" + " handoffs:\n" + " prompt: Impl done.\n" + " - role: designer\n" + " gate: optional\n" + " hitl: on-change\n" + " handoffs:\n" + " prompt: Design done.\n", + encoding="utf-8", + ) + result = CommandLineInterface._read_workflow_stages(tmp_path / ".github") + assert result == [ + { + "role": "engineer", + "gate": "required", + "hitl": "always", + "handoffs": [{"prompt": "Impl done.", "agent": "", "label": ""}], + }, + { + "role": "designer", + "gate": "optional", + "hitl": "on-change", + "handoffs": [{"prompt": "Design done.", "agent": "", "label": ""}], + }, + ] + + def test_skips_items_without_role(self, tmp_path: Path) -> None: + """Items lacking a string 'role' key are silently skipped.""" + vstack_dir = tmp_path / ".vstack" + vstack_dir.mkdir() + (vstack_dir / "config.yaml").write_text( + "workflow:\n stages:\n - gate: required\n - role: engineer\n", + encoding="utf-8", + ) + result = CommandLineInterface._read_workflow_stages(tmp_path / ".github") + assert result == [{"role": "engineer", "gate": "required", "handoffs": []}] + + def test_returns_empty_when_stages_not_a_list(self, tmp_path: Path) -> None: + """Returns empty list when workflow.stages is not a list.""" + vstack_dir = tmp_path / ".vstack" + vstack_dir.mkdir() + (vstack_dir / "config.yaml").write_text( + "workflow:\n stages: not-a-list\n", + encoding="utf-8", + ) + result = CommandLineInterface._read_workflow_stages(tmp_path / ".github") + assert result == [] + + +class TestParseStageHandoffs: + """Tests for CommandLineInterface._parse_stage_handoffs.""" + + def test_dict_handoffs_returns_single_entry(self) -> None: + """A dict handoffs value is returned as a single-entry list.""" + item = { + "role": "architect", + "handoffs": {"prompt": "Arch done.", "agent": "designer", "label": "Go"}, + } + result = CommandLineInterface._parse_stage_handoffs(item) + assert result == [{"prompt": "Arch done.", "agent": "designer", "label": "Go"}] + + def test_list_handoffs_returns_multiple_entries(self) -> None: + """A list handoffs value returns multiple normalized entries.""" + item = { + "role": "architect", + "handoffs": [ + {"prompt": "Go to designer.", "agent": "", "label": ""}, + {"prompt": "Skip to engineer.", "agent": "engineer", "label": "Skip design"}, + ], + } + result = CommandLineInterface._parse_stage_handoffs(item) + assert len(result) == 2 + assert result[0]["prompt"] == "Go to designer." + assert result[1]["agent"] == "engineer" + + def test_backward_compat_handoff_prompt_key(self) -> None: + """Legacy flat handoff_prompt key is wrapped as single-entry handoffs.""" + item = {"role": "architect", "handoff_prompt": "Legacy prompt."} + result = CommandLineInterface._parse_stage_handoffs(item) + assert result == [{"prompt": "Legacy prompt.", "agent": "", "label": ""}] + + def test_no_handoffs_returns_empty(self) -> None: + """Returns empty list when no handoffs or handoff_prompt key is present.""" + result = CommandLineInterface._parse_stage_handoffs({"role": "architect"}) + assert result == [] + + class TestReadExclude: """Tests for CommandLineInterface._read_exclude.""" From 519b379bb1c9d441d95c114ffdd3824f6923f165 Mon Sep 17 00:00:00 2001 From: Erik Schaareman Date: Sat, 9 May 2026 01:28:42 +0200 Subject: [PATCH 04/25] docs(adr): add ADR-023 and ADR-024; supersede ADR-004; update overview ADR-023 (workflow contract): documents the workflow: block added to .vstack/config.yaml as the source of truth for pipeline stage order, gate policy, hitl policy, and handoff prompts. Covers the baseline: flag on output artifacts and the defaults: restructuring in agent config.yaml. Supersedes the handoff-in-agent-config approach. ADR-024 (subagent orchestration): documents the decision to use VS Code native subagents (runSubagent tool, agents: frontmatter) for the orchestrated pipeline. Defines the planner agent as coordinator. Rejects scripts/runner.py, MCP orchestrator, and planner-only alternatives. Supersedes ADR-004. ADR-004: status changed to 'superseded by ADR-024'; amendment added explaining that the original blocker (no platform support for subagents) was resolved when Microsoft shipped the runSubagent tool in May 2026. overview.md: ADR-004 row marked superseded; ADR-023 and ADR-024 rows added to the decision table; pipeline reference links updated. roadmap.md: workflow contract and orchestrated pipeline rows moved from candidate to in progress with ADR references. --- .../adr/004-option-a-to-b-pipeline.md | 10 +- .../architecture/adr/023-workflow-contract.md | 198 ++++++++++++++++++ .../adr/024-subagent-orchestration.md | 119 +++++++++++ docs/architecture/overview.md | 8 +- docs/product/roadmap.md | 4 +- 5 files changed, 333 insertions(+), 6 deletions(-) create mode 100644 docs/architecture/adr/023-workflow-contract.md create mode 100644 docs/architecture/adr/024-subagent-orchestration.md diff --git a/docs/architecture/adr/004-option-a-to-b-pipeline.md b/docs/architecture/adr/004-option-a-to-b-pipeline.md index aa78cb3..7cc1be7 100644 --- a/docs/architecture/adr/004-option-a-to-b-pipeline.md +++ b/docs/architecture/adr/004-option-a-to-b-pipeline.md @@ -3,7 +3,7 @@ > Maintained by: **architect** role **date:** 2026-03-27\ -**status:** accepted +**status:** superseded by [ADR-024](024-subagent-orchestration.md) ## context @@ -51,3 +51,11 @@ is a runner addition, not a skill rewrite. ## impact on future orchestrated pipeline This is the foundational decision. See also `docs/design/workflow.md`. + +## amendment — 2026-05-09 + +The assumption that VS Code lacked native subagent support was invalidated when +Microsoft shipped the `runSubagent` tool and `agents:` frontmatter in VS Code +Copilot (documented 2026-05-06). The `scripts/runner.py` approach in the +"Refactoring required" section above is therefore not needed. See +[ADR-024](024-subagent-orchestration.md) for the updated decision. diff --git a/docs/architecture/adr/023-workflow-contract.md b/docs/architecture/adr/023-workflow-contract.md new file mode 100644 index 0000000..af06974 --- /dev/null +++ b/docs/architecture/adr/023-workflow-contract.md @@ -0,0 +1,198 @@ +# ADR-023: Workflow contract as source of truth for pipeline configuration + +> Maintained by: **architect** role + +**date:** 2026-05-08\ +**status:** accepted + +## context + +vstack's six-role agent model (ADR-009) defines a sequential pipeline: +`product → architect → designer → engineer → tester → release`. Each role produces +artifacts that become the input of the next role. The pipeline can be executed manually +(a user clicks a handoff button in VS Code) or, in future, automatically by an orchestrator +agent. + +Before this change, pipeline configuration was scattered and partially hardcoded: + +- **Stage order** was implicit in the order that `handoffs:` entries were written in each agent's + `config.yaml`. There was no single file that described the full pipeline. +- **Handoff targets** were encoded inside each agent's own config as `agent: `. If a team + wanted to reorder stages or insert a custom role, they had to edit multiple agent configs. +- **Handoff prompts** were tightly coupled to the next stage's name: the product agent's prompt + said "produce or update the architecture", making it incorrect if architect was moved later in + the pipeline. +- **Gate policy** (required vs. optional stages) did not exist as a concept; every stage was + implicitly required. + +This created a structural problem for the planned `planner` orchestrator role (ADR roadmap): +an orchestrator needs a complete, deterministic, machine-readable description of the pipeline +to dispatch subagents in order. It cannot reconstruct the pipeline by reading handoff entries +spread across six separate agent configs. + +A secondary problem was the absence of any distinction between *baseline* artifacts (living docs +that must be kept current throughout a project's lifetime) and *deliverable* artifacts (output +produced per session or release, such as reports and changelogs). Agents had no explicit +instruction to maintain their baseline docs — the responsibility was implied at best. + +## decision + +### 1. Workflow config block in `.vstack/config.yaml` + +A `workflow:` block is added to the project config schema. It is seeded once by `vstack install` +with the default six-stage pipeline and is thereafter owned by the project. `vstack init` reads +this block on every run. + +```yaml +workflow: + version: 1 + stages: + - role: product + gate: required + hitl: always + handoffs: + prompt: > + 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 explicitly, then pass to the next stage. + - role: architect + gate: required + hitl: always + handoffs: + prompt: > + Architecture outputs are approved. Produce design specifications as + needed. If your domain is not affected, assess and confirm explicitly, + then pass to the next stage. + - role: designer + gate: optional + hitl: on-change + handoffs: + prompt: > + Design outputs are approved. Implement code and tests as needed. If + your domain is not affected, assess and confirm explicitly, then pass + to the next stage. + - role: engineer + gate: required + hitl: always + handoffs: + prompt: > + Implementation is approved. Verify the implementation — run tests, + security checks, and performance analysis as needed. + - role: tester + gate: required + hitl: always + handoffs: + prompt: > + Verification outputs are approved. Prepare the release — produce or + update release artifacts and sign-offs. + - role: release + gate: required + hitl: always + handoffs: + prompt: "" +``` + +The `version` key allows `vstack init` to detect schema drift after a vstack upgrade and +emit a warning, without blocking the run. + +`gate` values: `required` (stage always runs), `optional` (stage may be skipped when its domain +is unaffected by the current change), and `skip` (stage is never executed — explicit opt-out). + +`hitl` (human-in-the-loop) values control when the pipeline pauses for human approval before the +handoff is activated: + +- `always` — pipeline always pauses; the human must explicitly approve before progression + (default for `gate: required`) +- `on-change` — pipeline pauses only when the stage made changes; if the stage reports no + changes the pipeline may continue automatically (default for `gate: optional`) +- `never` — pipeline continues without human approval; must be set explicitly as a conscious + opt-out + +Both fields are informational in the current release (Option A) and are reserved for enforcement +by the future orchestrator (Option B). The decision of whether changes occurred is always made +by the agent and confirmed by the human — never inferred automatically. + +### 2. Handoffs generated from workflow config + +Agent `config.yaml` files drop the `handoffs:` block. In its place, each agent carries a single +`handoff_prompt:` string — the text to send to the next stage. The generator reads the workflow +config to determine the next stage's role name and combines it with the agent's `handoff_prompt` +to produce the full handoff block in the generated `.agent.md`. + +When no workflow config is present (absent or empty `workflow:` block), the generator falls back +to a generic label ("Continue to next stage") and omits the `agent:` field, preserving v3 behavior +with no breaking change. + +### 3. `baseline:` flag on output artifacts + +Each agent `config.yaml` output entry gains an optional `baseline: true` flag. The generator +renders flagged items into a dedicated `### baseline docs you maintain` table inside the agent's +artifacts section. Agents are explicitly instructed to keep these files current. + +Artifacts without the flag are treated as deliverables — produced per session, not maintained +indefinitely. + +### 4. Project-level artifact overrides (overlay model) + +An `agents:` block may be added to `.vstack/config.yaml` to override per-agent artifact +configuration. Only delta entries need to be specified; omitting an agent means the template +default applies. The generator merges project overrides on top of template defaults at `init` +time. + +```yaml +agents: + product: + artifacts: + output: + - path: vision.md + baseline: true +``` + +This allows project teams to promote deliverables to baseline status without forking agent +templates. + +## alternatives considered + +**Keep handoffs fully in agent config.yaml.** The existing approach keeps each agent +self-contained, but makes the pipeline order implicit and couples prompt text to stage names. +An orchestrator would have to reconstruct the pipeline by following handoff chains — fragile and +order-dependent. Rejected because it does not support a configurable or orchestrated pipeline. + +**Separate `workflow.yaml` file in `.vstack/`.** A dedicated file would have cleaner separation +of concerns. Rejected in favor of adding a `workflow:` block to the existing `config.yaml` to +keep project configuration in one place and reduce the number of files a team must manage. + +**Pipeline order as a CLI argument.** Allows ad-hoc reordering without config changes. Rejected +because pipeline preferences are stable project decisions that should survive across all +invocations (CI, local, upgrade flows) without repeating flags. + +**`depends_on` for parallel stages.** Would allow independent stages (e.g., security audit and +performance test) to run in parallel. Rejected for this release: sequential execution is correct +for the current input/output dependency model. Parallel stages require a `depends_on` mechanism +and dynamic prompt composition when multiple upstream stages complete simultaneously. Reserved +for a future ADR. + +## rationale + +The workflow config block is the minimum necessary change to make the pipeline machine-readable +without breaking existing consumers. Teams that do not configure a `workflow:` block get identical +behavior to v3. Teams that do configure it gain a single, explicit source of truth for stage +order, gate policy, and handoff text. + +Separating `handoff_prompt` (agent-owned, role-specific knowledge) from handoff target and label +(pipeline-owned, derived from workflow order) is the correct ownership boundary. The agent knows +what it produced and what context the next stage needs; it does not know which role comes next. +The workflow knows the sequence. + +The `baseline:` flag addresses a recurring problem in practice: agents produced docs but had no +explicit instruction to maintain them. Making baseline status an explicit, generated instruction +inside the agent reduces the chance of stale docs accumulating unnoticed. + +## impact on orchestrated pipeline + +This ADR is the direct prerequisite for the `planner` orchestrator role. The planner reads +`workflow.stages` to determine dispatch order, uses `gate` to decide whether to run or skip a +stage, uses `hitl` to decide whether to pause for human approval after each stage completes, and +uses `handoffs[].prompt` as the context passed to the next subagent. No further changes to the +workflow schema are required to implement sequential orchestrated dispatch. Parallel dispatch and +`depends_on` semantics are deferred to a follow-on ADR. diff --git a/docs/architecture/adr/024-subagent-orchestration.md b/docs/architecture/adr/024-subagent-orchestration.md new file mode 100644 index 0000000..955e244 --- /dev/null +++ b/docs/architecture/adr/024-subagent-orchestration.md @@ -0,0 +1,119 @@ +# ADR-024: Subagent Orchestration via VS Code Native Subagents + +> Maintained by: **architect** role + +**date:** 2026-05-09\ +**status:** accepted\ +**supersedes:** [ADR-004](004-option-a-to-b-pipeline.md) + +## context + +[ADR-004](004-option-a-to-b-pipeline.md) deferred pipeline orchestration to a future +"Option B" model, citing the absence of platform support for an agent invoking another +agent inside VS Code Copilot. The planned fallback was a `scripts/runner.py` process runner +outside the editor. + +As of May 2026, VS Code Copilot ships native subagent support: + +- The `runSubagent` tool lets a coordinator agent invoke a named custom agent as a + subagent, pass it a prompt, and receive the result back in the same session context. +- The `agents:` frontmatter property in `.agent.md` restricts which custom agents a + coordinator is allowed to invoke. +- The `user-invocable: false` property hides worker agents from the dropdown while + keeping them available as subagents. +- Nested subagents are supported up to depth 5 (opt-in via + `chat.subagents.allowInvocationsFromSubagents`). + +The workflow contract established in [ADR-023](023-workflow-contract.md) already captures +the pipeline order (`workflow.stages`), gate policy (`gate`), human-in-the-loop policy +(`hitl`), and handoff prompts (`handoffs[].prompt`) in `.vstack/config.yaml`. All inputs +the orchestrator needs are already present. + +## decision + +Implement the orchestrated pipeline using the VS Code coordinator/worker subagent pattern. +Add a `planner` agent as the coordinator. The six role agents (`product`, `architect`, +`designer`, `engineer`, `tester`, `release`) continue to exist as both user-invocable +standalone agents and as subagent workers callable by the planner. + +### planner agent responsibilities + +1. Read the workflow contract from `.vstack/config.yaml` (`workflow.stages`). +1. Dispatch each stage in sequence by invoking the role agent as a subagent with the + stage's `handoffs[].prompt` as the prompt. +1. After each stage completes, apply `hitl` policy: + - `always` — pause and present the result to the user for explicit approval before + continuing. + - `on-change` — pause only if the subagent reports making changes; continue + automatically if it reports no changes. + - `never` — continue automatically without user confirmation. +1. Apply `gate` policy before dispatching: + - `required` — always dispatch the stage. + - `optional` — skip the stage if the subagent's domain is not affected by the current + change; the planner assesses this from context. + - `skip` — never dispatch the stage. + +### agent frontmatter + +The planner agent file includes: + +```yaml +tools: ['agent', 'read_file', 'semantic_search', 'file_search'] +agents: ['product', 'architect', 'designer', 'engineer', 'tester', 'release'] +user-invocable: true +``` + +The role agents remain `user-invocable: true` so teams can still invoke them directly +without the planner. The planner is additive — it does not replace direct invocation. + +### handoff prompt composition + +The planner passes the stage's `handoffs[].prompt` text from `config.yaml` as the +subagent prompt. This is the same text that was previously rendered into each agent's +`handoffs:` frontmatter block. The workflow contract is therefore the single source of +truth for both the planner-driven pipeline and the manual handoff buttons. + +## alternatives considered + +**`scripts/runner.py` process runner (original ADR-004 plan).** A Python script outside +the editor invokes agents via the VS Code CLI or a subprocess. Rejected because the +platform now provides the same capability natively, with better context passing, model +selection, and user visibility. A process runner would also violate ADR-006 (no runtime +dependency). + +**MCP server as orchestrator.** An MCP server exposes a `run_pipeline` tool that drives +stage dispatch. Rejected because it requires an external process and a network transport, +adds a runtime dependency, and provides no user experience benefit over native subagents. + +**Planner-only, no direct role invocation.** Make the role agents non-user-invocable +(`user-invocable: false`) and require all use to go through the planner. Rejected because +direct invocation is the primary use pattern for teams that want focused single-role +assistance without running a full pipeline. + +**Separate orchestrator skill instead of a new agent.** A skill that can be added to any +agent to give it planner behaviour. Rejected because the planner has a distinct identity, +a specific tool set, and a workflow-contract dependency that makes it a natural agent, not +a reusable skill. + +## rationale + +Using VS Code native subagents keeps the orchestrator inside the editor context, preserving +workspace access, model selection, and the user review loop. No external process, no new +runtime dependency (ADR-006 preserved). The coordinator/worker pattern documented by +Microsoft maps directly onto vstack's role model: one coordinator (planner), six workers +(the existing role agents). + +The workflow contract from ADR-023 already carries everything the planner needs. Implementing +the planner is a template authoring task — no changes to the vstack Python package, CLI, or +generator are required. + +## impact on the pipeline + +The planner agent is the concrete realisation of the Option B orchestrated pipeline +described in ADR-004. The original decision to keep stage boundaries explicit and +artifacts canonical (ADR-004, "Preparation done now") is validated: those properties are +exactly what makes the subagent handoff model work without additional scaffolding. + +Parallel stage execution (e.g. simultaneous security and performance tests inside +`tester`) remains deferred. It requires a `depends_on` model and prompt composition when +multiple upstream results arrive simultaneously. This is reserved for a follow-on ADR. diff --git a/docs/architecture/overview.md b/docs/architecture/overview.md index d791b84..07bf385 100644 --- a/docs/architecture/overview.md +++ b/docs/architecture/overview.md @@ -215,8 +215,8 @@ flowchart TD GR --> PR[PR opened] ``` -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. +See `docs/architecture/adr/023-workflow-contract.md`, `docs/architecture/adr/024-subagent-orchestration.md`, +`docs/architecture/adr/010-artifact-flow.md`, and `docs/design/workflow.md` for pipeline and gate detail. ______________________________________________________________________ @@ -230,7 +230,7 @@ See individual files for context, decision, alternatives, and rationale. | 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 | | +| 004 | Direct execution and orchestrated pipeline | superseded | Superseded by ADR-024 | | 005 | VS Code prompt format | accepted | | | 006 | No runtime dependency on external binaries | accepted | | | 007 | Python runtime | accepted | | @@ -249,3 +249,5 @@ See individual files for context, decision, alternatives, and rationale. | 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) | +| 023 | Workflow contract in `.vstack/config.yaml` | accepted | Pipeline order, gate, hitl, handoffs | +| 024 | Subagent orchestration via VS Code native subagents | accepted | Supersedes ADR-004; planner coordinator | diff --git a/docs/product/roadmap.md b/docs/product/roadmap.md index 8f8e277..495f13e 100644 --- a/docs/product/roadmap.md +++ b/docs/product/roadmap.md @@ -29,8 +29,8 @@ ______________________________________________________________________ | agent hooks support | t.b.d. | candidate | Generate `.github/hooks/.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 | +| workflow contract source-of-truth | t.b.d. | in progress | Central contract file in `.vstack/config.yaml`; `gate`, `hitl`, `handoffs` schema (ADR-023) | +| optional orchestrated role pipeline | t.b.d. | in progress | `planner` coordinator agent using VS Code native subagents (ADR-024); supersedes ADR-004 | | 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 | From ad1d1b49fd3b4aa1a31dd58884b1807bd6ee5612 Mon Sep 17 00:00:00 2001 From: Erik Schaareman Date: Sat, 9 May 2026 01:29:52 +0200 Subject: [PATCH 05/25] chore(install): regenerate artifacts; update manifest and changelog formatting Regenerate all .github/ artifacts from updated templates: - Agent files reflect the new defaults: config structure, baseline artifact tables, and workflow-derived handoff targets - All skill/instruction/prompt VSTACK-META footers bumped to vstack_version 3.0.0 Update .vstack/vstack.json with new checksums and install timestamp. Fix CHANGELOG.md formatting: replace * bullets with - bullets and remove double blank lines between sections (mdformat compliance). --- .github/agents/architect.agent.md | 13 +- .github/agents/designer.agent.md | 13 +- .github/agents/engineer.agent.md | 6 +- .github/agents/product.agent.md | 14 +- .github/agents/release.agent.md | 10 +- .github/agents/tester.agent.md | 6 +- .github/instructions/git.instructions.md | 2 +- .github/instructions/helm.instructions.md | 2 +- .github/instructions/java.instructions.md | 2 +- .github/instructions/k8s.instructions.md | 2 +- .github/instructions/markdown.instructions.md | 2 +- .github/instructions/python.instructions.md | 2 +- .github/instructions/rancher.instructions.md | 2 +- .github/instructions/security.instructions.md | 2 +- .../instructions/terraform.instructions.md | 2 +- .../instructions/terragrunt.instructions.md | 2 +- .github/instructions/testing.instructions.md | 2 +- .../instructions/typescript.instructions.md | 2 +- .github/prompts/api-design-review.prompt.md | 2 +- .github/prompts/architecture-risk.prompt.md | 2 +- .github/prompts/code-review.prompt.md | 2 +- .github/prompts/dependency-audit.prompt.md | 2 +- .github/prompts/incident-timeline.prompt.md | 2 +- .github/prompts/migration-safety.prompt.md | 2 +- .github/prompts/release-readiness.prompt.md | 2 +- .github/skills/adr/SKILL.md | 2 +- .github/skills/analyse/SKILL.md | 2 +- .github/skills/architecture/SKILL.md | 2 +- .github/skills/aws-cli/SKILL.md | 2 +- .github/skills/cicd/SKILL.md | 2 +- .github/skills/cloudformation/SKILL.md | 2 +- .github/skills/code-review/SKILL.md | 2 +- .github/skills/codeql/SKILL.md | 2 +- .github/skills/concise/SKILL.md | 2 +- .github/skills/consult/SKILL.md | 2 +- .github/skills/container/SKILL.md | 2 +- .github/skills/conventional-commit/SKILL.md | 2 +- .github/skills/debug/SKILL.md | 2 +- .github/skills/dependabot/SKILL.md | 2 +- .github/skills/dependency/SKILL.md | 2 +- .github/skills/design/SKILL.md | 2 +- .github/skills/docs/SKILL.md | 2 +- .github/skills/explore/SKILL.md | 2 +- .github/skills/gdpr/SKILL.md | 2 +- .github/skills/gh-issues/SKILL.md | 2 +- .github/skills/gh-release/SKILL.md | 2 +- .github/skills/guardrails/SKILL.md | 2 +- .github/skills/helm/SKILL.md | 2 +- .github/skills/incident/SKILL.md | 2 +- .github/skills/inspect/SKILL.md | 2 +- .github/skills/k8s/SKILL.md | 2 +- .github/skills/migrate/SKILL.md | 2 +- .github/skills/onboard/SKILL.md | 2 +- .github/skills/openapi/SKILL.md | 2 +- .github/skills/performance/SKILL.md | 2 +- .github/skills/postmortem/SKILL.md | 2 +- .github/skills/pr/SKILL.md | 2 +- .github/skills/rancher/SKILL.md | 2 +- .github/skills/rca/SKILL.md | 2 +- .github/skills/refactor/SKILL.md | 2 +- .github/skills/release-notes/SKILL.md | 2 +- .github/skills/requirements/SKILL.md | 2 +- .github/skills/secret-scan/SKILL.md | 2 +- .github/skills/security/SKILL.md | 2 +- .github/skills/terraform/SKILL.md | 2 +- .github/skills/terragrunt/SKILL.md | 2 +- .github/skills/threat-model/SKILL.md | 2 +- .github/skills/verify/SKILL.md | 2 +- .github/skills/vision/SKILL.md | 2 +- .vstack/vstack.json | 142 +++++++++--------- CHANGELOG.md | 51 +++---- 71 files changed, 208 insertions(+), 173 deletions(-) diff --git a/.github/agents/architect.agent.md b/.github/agents/architect.agent.md index e708bbe..0f66b1e 100644 --- a/.github/agents/architect.agent.md +++ b/.github/agents/architect.agent.md @@ -23,7 +23,7 @@ model: user-invocable: true target: vscode handoffs: - - label: 'Go to next stage: Design' + - label: 'Go to next stage: Designer' agent: designer prompt: >- Architecture outputs are approved. Assess the current state and produce design specifications as @@ -150,6 +150,15 @@ what work is needed: | `docs/architecture/overview.md` | | `docs/architecture/adr/*.md` | +### baseline docs you maintain + +Keep these files current. Update them whenever the relevant scope, design, or implementation changes — do not let them go stale. + +| Artifact | +| ------------------------------- | +| `docs/architecture/overview.md` | +| `docs/architecture/adr/*.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. @@ -172,4 +181,4 @@ that requires changes to upstream artifacts, flag it and trigger a reverse hando - `@#gdpr` — privacy by design and data processing architecture review - + diff --git a/.github/agents/designer.agent.md b/.github/agents/designer.agent.md index 4151ba0..9e8a244 100644 --- a/.github/agents/designer.agent.md +++ b/.github/agents/designer.agent.md @@ -21,7 +21,7 @@ model: user-invocable: true target: vscode handoffs: - - label: 'Go to next stage: Engineering' + - label: 'Go to next stage: Engineer' agent: engineer prompt: >- Design outputs are approved. Assess the current state and implement code and tests as needed. If @@ -165,6 +165,15 @@ what work is needed: | `docs/design/ux.md` | frontend/fullstack scope only | | `docs/design/**/*.md` | additional detail docs per component, model, system, or domain (when scope warrants it) | +### baseline docs you maintain + +Keep these files current. Update them whenever the relevant scope, design, or implementation changes — do not let them go stale. + +| Artifact | Notes | +| ------------------------- | ----------------------------- | +| `docs/design/overview.md` | | +| `docs/design/ux.md` | 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. @@ -185,4 +194,4 @@ that requires changes to upstream artifacts, flag it and trigger a reverse hando - `@#openapi` — OpenAPI 3.1 spec writing and review - + diff --git a/.github/agents/engineer.agent.md b/.github/agents/engineer.agent.md index a2078ab..627a80e 100644 --- a/.github/agents/engineer.agent.md +++ b/.github/agents/engineer.agent.md @@ -22,7 +22,7 @@ model: user-invocable: true target: vscode handoffs: - - label: 'Go to next stage: Verification' + - label: 'Go to next stage: Tester' agent: tester prompt: >- Implementation is approved. Assess the current state and verify the implementation as needed — run @@ -160,6 +160,8 @@ what work is needed: | `issues/{id}-{slug}-rca.md` | when working on an issue | | `issues/{id}-{slug}-postmortem.md` | when stakeholder impact is significant | + + 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. @@ -202,4 +204,4 @@ that requires changes to upstream artifacts, flag it and trigger a reverse hando - `@#rancher` — Rancher and Fleet multi-cluster operations and governance - + diff --git a/.github/agents/product.agent.md b/.github/agents/product.agent.md index c030281..443f89e 100644 --- a/.github/agents/product.agent.md +++ b/.github/agents/product.agent.md @@ -22,7 +22,7 @@ model: user-invocable: true target: vscode handoffs: - - label: 'Go to next stage: Architecture' + - label: 'Go to next stage: Architect' agent: architect prompt: >- Product outputs are approved. Assess the current state and produce or update the architecture as @@ -133,6 +133,16 @@ Handoffs you own: | `docs/product/changes/*.md` | | `docs/product/issues/*.md` | +### baseline docs you maintain + +Keep these files current. Update them whenever the relevant scope, design, or implementation changes — do not let them go stale. + +| Artifact | +| ------------------------------ | +| `docs/product/vision.md` | +| `docs/product/requirements.md` | +| `docs/product/roadmap.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. @@ -155,4 +165,4 @@ that requires changes to upstream artifacts, flag it and trigger a reverse hando - `@#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 e0a0c85..edf3a36 100644 --- a/.github/agents/release.agent.md +++ b/.github/agents/release.agent.md @@ -123,6 +123,14 @@ and wait for explicit user routing decisions. | -------------------- | ------------------------------------------ | | `docs/releases/*.md` | includes release notes and sign-off record | +### baseline docs you maintain + +Keep these files current. Update them whenever the relevant scope, design, or implementation changes — do not let them go stale. + +| Artifact | Notes | +| -------------------- | ------------------------------------------ | +| `docs/releases/*.md` | includes release notes and sign-off record | + 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. @@ -146,4 +154,4 @@ that requires changes to upstream artifacts, flag it and trigger a reverse hando - `@#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 9403956..ac91351 100644 --- a/.github/agents/tester.agent.md +++ b/.github/agents/tester.agent.md @@ -22,7 +22,7 @@ model: user-invocable: true target: vscode handoffs: - - label: 'Go to next stage: Release readiness' + - label: 'Go to next stage: Release' agent: release prompt: >- Verification outputs are approved. Assess the current state and prepare the release as needed. @@ -145,6 +145,8 @@ what work is needed: | `docs/reports/**/*.md` | | `tests/**/*` | + + 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. @@ -179,4 +181,4 @@ that requires changes to upstream artifacts, flag it and trigger a reverse hando - `@#rancher` — Rancher/Fleet configuration and multi-cluster governance review - + diff --git a/.github/instructions/git.instructions.md b/.github/instructions/git.instructions.md index ee0ecde..1dd020c 100644 --- a/.github/instructions/git.instructions.md +++ b/.github/instructions/git.instructions.md @@ -41,4 +41,4 @@ Use these Git and release hygiene conventions in this project. 1. Prefer local verification before pushing release-impacting changes. - + diff --git a/.github/instructions/helm.instructions.md b/.github/instructions/helm.instructions.md index 1b37194..0548b18 100644 --- a/.github/instructions/helm.instructions.md +++ b/.github/instructions/helm.instructions.md @@ -45,4 +45,4 @@ Use these Helm conventions in this project. - [Helm chart best practices](https://helm.sh/docs/chart_best_practices/) - + diff --git a/.github/instructions/java.instructions.md b/.github/instructions/java.instructions.md index 98a498a..f247582 100644 --- a/.github/instructions/java.instructions.md +++ b/.github/instructions/java.instructions.md @@ -56,4 +56,4 @@ Use these Java conventions in this project. 1. Do not suppress static analysis warnings without a documented, task-specific reason. - + diff --git a/.github/instructions/k8s.instructions.md b/.github/instructions/k8s.instructions.md index 5bcc17e..0d95b18 100644 --- a/.github/instructions/k8s.instructions.md +++ b/.github/instructions/k8s.instructions.md @@ -51,4 +51,4 @@ Use these Kubernetes conventions in this project. - [Kubernetes API reference](https://kubernetes.io/docs/reference/kubernetes-api/) - + diff --git a/.github/instructions/markdown.instructions.md b/.github/instructions/markdown.instructions.md index 64b29d5..da913f7 100644 --- a/.github/instructions/markdown.instructions.md +++ b/.github/instructions/markdown.instructions.md @@ -51,4 +51,4 @@ Use these Markdown conventions in this project. 1. Keep examples accurate and runnable — a broken example is worse than no example. - + diff --git a/.github/instructions/python.instructions.md b/.github/instructions/python.instructions.md index a9a809c..6df4436 100644 --- a/.github/instructions/python.instructions.md +++ b/.github/instructions/python.instructions.md @@ -42,4 +42,4 @@ Use these Python conventions in this project. 1. Do not silence lint/type errors unless there is a documented, task-specific reason. - + diff --git a/.github/instructions/rancher.instructions.md b/.github/instructions/rancher.instructions.md index 3f52dd8..48c34e1 100644 --- a/.github/instructions/rancher.instructions.md +++ b/.github/instructions/rancher.instructions.md @@ -44,4 +44,4 @@ Use these Rancher conventions in this project. - [Fleet docs](https://fleet.rancher.io/) - + diff --git a/.github/instructions/security.instructions.md b/.github/instructions/security.instructions.md index dd02e2c..eb13422 100644 --- a/.github/instructions/security.instructions.md +++ b/.github/instructions/security.instructions.md @@ -42,4 +42,4 @@ Apply these security policies in this project. 1. Isolate privileged logic; keep it minimal, auditable, and separate from business logic. - + diff --git a/.github/instructions/terraform.instructions.md b/.github/instructions/terraform.instructions.md index da8fe19..84d52f2 100644 --- a/.github/instructions/terraform.instructions.md +++ b/.github/instructions/terraform.instructions.md @@ -60,4 +60,4 @@ Use these Terraform conventions in this project. - [tfsec](https://aquasecurity.github.io/tfsec/) · [checkov](https://www.checkov.io/) - + diff --git a/.github/instructions/terragrunt.instructions.md b/.github/instructions/terragrunt.instructions.md index 9770cb6..d46e25b 100644 --- a/.github/instructions/terragrunt.instructions.md +++ b/.github/instructions/terragrunt.instructions.md @@ -57,4 +57,4 @@ Use these Terragrunt conventions in this project. - [Terragrunt CLI reference](https://terragrunt.gruntwork.io/docs/reference/cli-options/) - + diff --git a/.github/instructions/testing.instructions.md b/.github/instructions/testing.instructions.md index e673737..bef5ac4 100644 --- a/.github/instructions/testing.instructions.md +++ b/.github/instructions/testing.instructions.md @@ -43,4 +43,4 @@ Use these testing conventions in this project. 1. Treat flaky tests as bugs; do not merge code with known test reliability issues. - + diff --git a/.github/instructions/typescript.instructions.md b/.github/instructions/typescript.instructions.md index adb935d..3844641 100644 --- a/.github/instructions/typescript.instructions.md +++ b/.github/instructions/typescript.instructions.md @@ -49,4 +49,4 @@ Use these TypeScript conventions in this project. 1. Do not suppress lint or type errors with inline disable comments unless there is a documented, task-specific reason. - + diff --git a/.github/prompts/api-design-review.prompt.md b/.github/prompts/api-design-review.prompt.md index 70e505c..0f7babc 100644 --- a/.github/prompts/api-design-review.prompt.md +++ b/.github/prompts/api-design-review.prompt.md @@ -55,4 +55,4 @@ List fields or objects that are missing required constraints, descriptions, or e - top priority fix in one sentence - + diff --git a/.github/prompts/architecture-risk.prompt.md b/.github/prompts/architecture-risk.prompt.md index 9d9f919..843f659 100644 --- a/.github/prompts/architecture-risk.prompt.md +++ b/.github/prompts/architecture-risk.prompt.md @@ -52,4 +52,4 @@ List security-specific risks not covered above: auth boundaries, sensitive data - one-sentence rationale - + diff --git a/.github/prompts/code-review.prompt.md b/.github/prompts/code-review.prompt.md index 4f6287e..747ec5b 100644 --- a/.github/prompts/code-review.prompt.md +++ b/.github/prompts/code-review.prompt.md @@ -51,4 +51,4 @@ End with: - Biggest remaining risk: one sentence - + diff --git a/.github/prompts/dependency-audit.prompt.md b/.github/prompts/dependency-audit.prompt.md index d8744b5..482dda3 100644 --- a/.github/prompts/dependency-audit.prompt.md +++ b/.github/prompts/dependency-audit.prompt.md @@ -60,4 +60,4 @@ List packages with unusual provenance concerns: abandoned maintainers, single-ma Ordered list of actions by priority (critical first). - + diff --git a/.github/prompts/incident-timeline.prompt.md b/.github/prompts/incident-timeline.prompt.md index ebe3980..be81fb0 100644 --- a/.github/prompts/incident-timeline.prompt.md +++ b/.github/prompts/incident-timeline.prompt.md @@ -57,4 +57,4 @@ For each action: List the minimum controls needed to reduce repeat probability. - + diff --git a/.github/prompts/migration-safety.prompt.md b/.github/prompts/migration-safety.prompt.md index e602ce8..9b41a5f 100644 --- a/.github/prompts/migration-safety.prompt.md +++ b/.github/prompts/migration-safety.prompt.md @@ -52,4 +52,4 @@ List missing migration tests (forward, backward, data invariants, load-sensitive - biggest remaining risk in one sentence - + diff --git a/.github/prompts/release-readiness.prompt.md b/.github/prompts/release-readiness.prompt.md index 8552f4f..f8600d0 100644 --- a/.github/prompts/release-readiness.prompt.md +++ b/.github/prompts/release-readiness.prompt.md @@ -46,4 +46,4 @@ For each expected artifact that is missing, flag it explicitly as: MISSING — [ One clear next step for the team. - + diff --git a/.github/skills/adr/SKILL.md b/.github/skills/adr/SKILL.md index e0e9a3a..938ceec 100644 --- a/.github/skills/adr/SKILL.md +++ b/.github/skills/adr/SKILL.md @@ -179,4 +179,4 @@ is a kebab-case title. After writing, state the file path and summary so the architect or product role can review. - + diff --git a/.github/skills/analyse/SKILL.md b/.github/skills/analyse/SKILL.md index aca2109..5b999da 100644 --- a/.github/skills/analyse/SKILL.md +++ b/.github/skills/analyse/SKILL.md @@ -213,4 +213,4 @@ State conclusions with confidence level: ``` - + diff --git a/.github/skills/architecture/SKILL.md b/.github/skills/architecture/SKILL.md index 2e4243e..fb51b24 100644 --- a/.github/skills/architecture/SKILL.md +++ b/.github/skills/architecture/SKILL.md @@ -277,4 +277,4 @@ For each significant structural decision made during this review (technology cho - Update `docs/architecture/overview.md` to reflect the final decisions. - + diff --git a/.github/skills/aws-cli/SKILL.md b/.github/skills/aws-cli/SKILL.md index 25586cb..674c4d5 100644 --- a/.github/skills/aws-cli/SKILL.md +++ b/.github/skills/aws-cli/SKILL.md @@ -373,4 +373,4 @@ aws ce get-cost-and-usage \ - [AWS CLI named profiles](https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-profiles.html) - + diff --git a/.github/skills/cicd/SKILL.md b/.github/skills/cicd/SKILL.md index 70be947..5fa77e8 100644 --- a/.github/skills/cicd/SKILL.md +++ b/.github/skills/cicd/SKILL.md @@ -220,4 +220,4 @@ Configure these in GitHub → Settings → Branches. - [GitHub-hosted runners](https://docs.github.com/en/actions/using-github-hosted-runners/using-github-hosted-runners/about-github-hosted-runners) - + diff --git a/.github/skills/cloudformation/SKILL.md b/.github/skills/cloudformation/SKILL.md index a015aa9..1ca4601 100644 --- a/.github/skills/cloudformation/SKILL.md +++ b/.github/skills/cloudformation/SKILL.md @@ -343,4 +343,4 @@ AppSecurityGroup: - [AWS SAM documentation](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/) - + diff --git a/.github/skills/code-review/SKILL.md b/.github/skills/code-review/SKILL.md index 979c681..aab6705 100644 --- a/.github/skills/code-review/SKILL.md +++ b/.github/skills/code-review/SKILL.md @@ -217,4 +217,4 @@ Confidence: [HIGH/MEDIUM/LOW — explain if not HIGH] ``` - + diff --git a/.github/skills/codeql/SKILL.md b/.github/skills/codeql/SKILL.md index ce8f67a..5fa8372 100644 --- a/.github/skills/codeql/SKILL.md +++ b/.github/skills/codeql/SKILL.md @@ -250,4 +250,4 @@ GITHUB_TOKEN= codeql github upload-results \ - [Supported languages and frameworks](https://docs.github.com/en/code-security/code-scanning/introduction-to-code-scanning/codeql-code-scanning-for-compiled-languages) - + diff --git a/.github/skills/concise/SKILL.md b/.github/skills/concise/SKILL.md index 8ff9138..6968d01 100644 --- a/.github/skills/concise/SKILL.md +++ b/.github/skills/concise/SKILL.md @@ -146,4 +146,4 @@ Current mode unchanged: - [ ] User confirmation/status returned in deterministic format - + diff --git a/.github/skills/consult/SKILL.md b/.github/skills/consult/SKILL.md index 6532b9f..ea8e1d0 100644 --- a/.github/skills/consult/SKILL.md +++ b/.github/skills/consult/SKILL.md @@ -217,4 +217,4 @@ reason: [one sentence] ``` - + diff --git a/.github/skills/container/SKILL.md b/.github/skills/container/SKILL.md index 8f0a472..613b8f6 100644 --- a/.github/skills/container/SKILL.md +++ b/.github/skills/container/SKILL.md @@ -152,4 +152,4 @@ For production-like local testing, write a separate `docker-compose.prod.yml` wi - [Docker official images](https://hub.docker.com/search?image_filter=official) - + diff --git a/.github/skills/conventional-commit/SKILL.md b/.github/skills/conventional-commit/SKILL.md index 5f66f2d..87db593 100644 --- a/.github/skills/conventional-commit/SKILL.md +++ b/.github/skills/conventional-commit/SKILL.md @@ -154,4 +154,4 @@ Remaining changes: If commit is blocked, report exact reason and proposed fix. - + diff --git a/.github/skills/debug/SKILL.md b/.github/skills/debug/SKILL.md index d808802..9cce4a9 100644 --- a/.github/skills/debug/SKILL.md +++ b/.github/skills/debug/SKILL.md @@ -257,4 +257,4 @@ Prevention: [any follow-up items] ``` - + diff --git a/.github/skills/dependabot/SKILL.md b/.github/skills/dependabot/SKILL.md index 5fd9934..0084c1a 100644 --- a/.github/skills/dependabot/SKILL.md +++ b/.github/skills/dependabot/SKILL.md @@ -319,4 +319,4 @@ updates: - [Dependabot security updates](https://docs.github.com/en/code-security/dependabot/dependabot-security-updates/about-dependabot-security-updates) - + diff --git a/.github/skills/dependency/SKILL.md b/.github/skills/dependency/SKILL.md index 1b5db8d..26941e9 100644 --- a/.github/skills/dependency/SKILL.md +++ b/.github/skills/dependency/SKILL.md @@ -317,4 +317,4 @@ Action items (priority order): - [PyPI / npm / crates.io / Maven Central](https://pypi.org) (replace with the relevant registry) - + diff --git a/.github/skills/design/SKILL.md b/.github/skills/design/SKILL.md index 3d8428c..c851cd9 100644 --- a/.github/skills/design/SKILL.md +++ b/.github/skills/design/SKILL.md @@ -242,4 +242,4 @@ Output a complete design document to `docs/design/overview.md` or `openapi.yaml` ``` - + diff --git a/.github/skills/docs/SKILL.md b/.github/skills/docs/SKILL.md index 98ef5c2..f058b4a 100644 --- a/.github/skills/docs/SKILL.md +++ b/.github/skills/docs/SKILL.md @@ -150,4 +150,4 @@ Skipped (n/a): ``` - + diff --git a/.github/skills/explore/SKILL.md b/.github/skills/explore/SKILL.md index 5ef71cf..da7b7f6 100644 --- a/.github/skills/explore/SKILL.md +++ b/.github/skills/explore/SKILL.md @@ -223,4 +223,4 @@ Stack: [language, framework, runtime versions] ``` - + diff --git a/.github/skills/gdpr/SKILL.md b/.github/skills/gdpr/SKILL.md index 387f14a..615329c 100644 --- a/.github/skills/gdpr/SKILL.md +++ b/.github/skills/gdpr/SKILL.md @@ -245,4 +245,4 @@ Use `@example.com` for all test email addresses. - [EDPB guidelines](https://www.edpb.europa.eu/our-work-tools/general-guidance/guidelines-recommendations-best-practices_en) - + diff --git a/.github/skills/gh-issues/SKILL.md b/.github/skills/gh-issues/SKILL.md index 9a6bede..82630ee 100644 --- a/.github/skills/gh-issues/SKILL.md +++ b/.github/skills/gh-issues/SKILL.md @@ -229,4 +229,4 @@ https://github.com///issues/ - [GitHub Issues documentation](https://docs.github.com/en/issues) - + diff --git a/.github/skills/gh-release/SKILL.md b/.github/skills/gh-release/SKILL.md index 11960e8..8c7fb2f 100644 --- a/.github/skills/gh-release/SKILL.md +++ b/.github/skills/gh-release/SKILL.md @@ -213,4 +213,4 @@ If blocked, report exact blocker and required user action. - [GitHub Releases documentation](https://docs.github.com/en/repositories/releasing-projects-on-github/about-releases) - + diff --git a/.github/skills/guardrails/SKILL.md b/.github/skills/guardrails/SKILL.md index 3a85808..f2ef32e 100644 --- a/.github/skills/guardrails/SKILL.md +++ b/.github/skills/guardrails/SKILL.md @@ -71,4 +71,4 @@ Activate careful mode for this session. Two behaviors are now enabled. Explicitly ask to "disable guardrails". - + diff --git a/.github/skills/helm/SKILL.md b/.github/skills/helm/SKILL.md index acea595..7dad35e 100644 --- a/.github/skills/helm/SKILL.md +++ b/.github/skills/helm/SKILL.md @@ -139,4 +139,4 @@ Practices: - [Chart best practices](https://helm.sh/docs/chart_best_practices/) - + diff --git a/.github/skills/incident/SKILL.md b/.github/skills/incident/SKILL.md index 7b17180..1293932 100644 --- a/.github/skills/incident/SKILL.md +++ b/.github/skills/incident/SKILL.md @@ -250,4 +250,4 @@ Next: invoke @#rca and @#postmortem to produce written artifacts. ``` - + diff --git a/.github/skills/inspect/SKILL.md b/.github/skills/inspect/SKILL.md index c1dba2d..3e1a676 100644 --- a/.github/skills/inspect/SKILL.md +++ b/.github/skills/inspect/SKILL.md @@ -154,4 +154,4 @@ Confirm for changed paths: ``` - + diff --git a/.github/skills/k8s/SKILL.md b/.github/skills/k8s/SKILL.md index 7375f27..e678a59 100644 --- a/.github/skills/k8s/SKILL.md +++ b/.github/skills/k8s/SKILL.md @@ -143,4 +143,4 @@ Common failure classes: - [Kubernetes API reference](https://kubernetes.io/docs/reference/kubernetes-api/) - + diff --git a/.github/skills/migrate/SKILL.md b/.github/skills/migrate/SKILL.md index 21f26b6..85f470c 100644 --- a/.github/skills/migrate/SKILL.md +++ b/.github/skills/migrate/SKILL.md @@ -319,4 +319,4 @@ Pre-deploy checklist: ``` - + diff --git a/.github/skills/onboard/SKILL.md b/.github/skills/onboard/SKILL.md index 929214c..295344c 100644 --- a/.github/skills/onboard/SKILL.md +++ b/.github/skills/onboard/SKILL.md @@ -301,4 +301,4 @@ Gaps remaining (if any): ``` - + diff --git a/.github/skills/openapi/SKILL.md b/.github/skills/openapi/SKILL.md index 976d3ee..6931474 100644 --- a/.github/skills/openapi/SKILL.md +++ b/.github/skills/openapi/SKILL.md @@ -402,4 +402,4 @@ Summary: [N critical, N warnings, N info] - [Redocly CLI (linting)](https://redocly.com/docs/cli/) - + diff --git a/.github/skills/performance/SKILL.md b/.github/skills/performance/SKILL.md index 26c45d2..a9fc9d2 100644 --- a/.github/skills/performance/SKILL.md +++ b/.github/skills/performance/SKILL.md @@ -241,4 +241,4 @@ For each bottleneck identified: ``` - + diff --git a/.github/skills/postmortem/SKILL.md b/.github/skills/postmortem/SKILL.md index 07df9dc..789ecec 100644 --- a/.github/skills/postmortem/SKILL.md +++ b/.github/skills/postmortem/SKILL.md @@ -183,4 +183,4 @@ Status: Draft — ready for team review ``` - + diff --git a/.github/skills/pr/SKILL.md b/.github/skills/pr/SKILL.md index 23c4910..d5b395c 100644 --- a/.github/skills/pr/SKILL.md +++ b/.github/skills/pr/SKILL.md @@ -150,4 +150,4 @@ Next steps depend on the repository CI/CD configuration: ``` - + diff --git a/.github/skills/rancher/SKILL.md b/.github/skills/rancher/SKILL.md index 17f1727..1b4a001 100644 --- a/.github/skills/rancher/SKILL.md +++ b/.github/skills/rancher/SKILL.md @@ -112,4 +112,4 @@ Checks: - [Fleet documentation](https://fleet.rancher.io/) - + diff --git a/.github/skills/rca/SKILL.md b/.github/skills/rca/SKILL.md index e5114e9..bfa145f 100644 --- a/.github/skills/rca/SKILL.md +++ b/.github/skills/rca/SKILL.md @@ -206,4 +206,4 @@ Status: Draft — ready for review ``` - + diff --git a/.github/skills/refactor/SKILL.md b/.github/skills/refactor/SKILL.md index efb2840..5c40c13 100644 --- a/.github/skills/refactor/SKILL.md +++ b/.github/skills/refactor/SKILL.md @@ -371,4 +371,4 @@ Behavior changed: No ``` - + diff --git a/.github/skills/release-notes/SKILL.md b/.github/skills/release-notes/SKILL.md index 19081b7..8d4cb92 100644 --- a/.github/skills/release-notes/SKILL.md +++ b/.github/skills/release-notes/SKILL.md @@ -146,4 +146,4 @@ Prepend a new entry at the top of `CHANGELOG.md`: Keep existing entries intact. - + diff --git a/.github/skills/requirements/SKILL.md b/.github/skills/requirements/SKILL.md index efe5ee4..e2ac283 100644 --- a/.github/skills/requirements/SKILL.md +++ b/.github/skills/requirements/SKILL.md @@ -198,4 +198,4 @@ Write all findings to `docs/product/requirements.md`: After writing, summarize what was decided so the architect role can start. - + diff --git a/.github/skills/secret-scan/SKILL.md b/.github/skills/secret-scan/SKILL.md index a378d37..7ba5bd7 100644 --- a/.github/skills/secret-scan/SKILL.md +++ b/.github/skills/secret-scan/SKILL.md @@ -239,4 +239,4 @@ credential formats. - [Supported secret patterns](https://docs.github.com/en/code-security/secret-scanning/introduction/supported-secret-scanning-patterns) - + diff --git a/.github/skills/security/SKILL.md b/.github/skills/security/SKILL.md index a237075..b8d50aa 100644 --- a/.github/skills/security/SKILL.md +++ b/.github/skills/security/SKILL.md @@ -294,4 +294,4 @@ Scope: [full/diff/dependency/config] - [STRIDE threat modeling (Microsoft)](https://learn.microsoft.com/en-us/azure/security/develop/threat-modeling-tool-threats) - + diff --git a/.github/skills/terraform/SKILL.md b/.github/skills/terraform/SKILL.md index 2554e0c..86e1238 100644 --- a/.github/skills/terraform/SKILL.md +++ b/.github/skills/terraform/SKILL.md @@ -334,4 +334,4 @@ Run `terraform plan` after every state operation to verify the outcome. - [tfsec rules](https://aquasecurity.github.io/tfsec/latest/checks/aws/) · [checkov checks](https://www.checkov.io/5.Policy%20Index/terraform.html) - + diff --git a/.github/skills/terragrunt/SKILL.md b/.github/skills/terragrunt/SKILL.md index baccafa..7665466 100644 --- a/.github/skills/terragrunt/SKILL.md +++ b/.github/skills/terragrunt/SKILL.md @@ -306,4 +306,4 @@ Use `--terragrunt-non-interactive` in CI to prevent hanging on prompts. - [Gruntwork module registry](https://www.gruntwork.io/) - + diff --git a/.github/skills/threat-model/SKILL.md b/.github/skills/threat-model/SKILL.md index fb8e7ce..205e46f 100644 --- a/.github/skills/threat-model/SKILL.md +++ b/.github/skills/threat-model/SKILL.md @@ -244,4 +244,4 @@ For each high-priority threat include: - Final report is written to `docs/architecture/threat-model.md`. - + diff --git a/.github/skills/verify/SKILL.md b/.github/skills/verify/SKILL.md index 55cd71c..ba57b9f 100644 --- a/.github/skills/verify/SKILL.md +++ b/.github/skills/verify/SKILL.md @@ -265,4 +265,4 @@ scope: [path/component/full] ``` - + diff --git a/.github/skills/vision/SKILL.md b/.github/skills/vision/SKILL.md index 2e0d25f..49d989a 100644 --- a/.github/skills/vision/SKILL.md +++ b/.github/skills/vision/SKILL.md @@ -205,4 +205,4 @@ For each finding: explain the tradeoff, give an opinionated recommendation, ask Present as: "Overall assessment: [READY/NEEDS REVISION/SCOPE CHANGE] because [1-2 sentence reason]." - + diff --git a/.vstack/vstack.json b/.vstack/vstack.json index c85cee4..836639f 100644 --- a/.vstack/vstack.json +++ b/.vstack/vstack.json @@ -1,316 +1,316 @@ { "manifest_version": 2, "hash_algorithm": "sha256", - "vstack_version": "0.0.0.post3.dev0+df3fe6e", - "installed_at": "2026-05-06T22:03:21.641306+00:00", + "vstack_version": "3.0.0", + "installed_at": "2026-05-08T23:06:53.578784+00:00", "artifacts": { "skills": [ { "name": "adr", "file": "skills/adr/SKILL.md", "version": "20260421003", - "checksum": "0838ffc14c5b86ea8b3df93cf6ce76c9bcd27b6c55c4ba47a3d01cc000fd7ee0", + "checksum": "c4d99dbcbaac68c11749c7aae802445b4a3f3c57bd2baa24bbfff3481d897657", "checksum_algorithm": "sha256" }, { "name": "analyse", "file": "skills/analyse/SKILL.md", "version": "20260421004", - "checksum": "8ff12f1d1f12ac9c46a2cb36981b85aeea97a8bc0876bbef37300511ea0eb7b3", + "checksum": "09780d00803baf7b9d6170a4fb0a409049721e70b1f3bc0f2426d20b604a1eb3", "checksum_algorithm": "sha256" }, { "name": "architecture", "file": "skills/architecture/SKILL.md", "version": "20260421005", - "checksum": "4ed22957e392aa89cf4949d2a88e707bf913948c2805a257039f5376810af754", + "checksum": "ac0ef4525b0f264305e7d7f2144a9c72dd375973408151713140e7a368ce9389", "checksum_algorithm": "sha256" }, { "name": "aws-cli", "file": "skills/aws-cli/SKILL.md", "version": "20260502033", - "checksum": "e5b2688de029ab0cc6d3e3862237c5bdb7f3ad4aab9baa4e1bceac433d699b79", + "checksum": "2a7702778c4a6d9de85496afdc13e7ab5b68dfc20198a5d48d4ef0134f262500", "checksum_algorithm": "sha256" }, { "name": "cicd", "file": "skills/cicd/SKILL.md", "version": "20260421006", - "checksum": "ffe0df7fe8c425844e9fc1a20976a9d6c116d828af96c1a568317728bfdefdcf", + "checksum": "1b94088cfda1b653959e0dc7ddf09a3c49e879ee9e11d50877d8bb935b30cda8", "checksum_algorithm": "sha256" }, { "name": "cloudformation", "file": "skills/cloudformation/SKILL.md", "version": "20260502032", - "checksum": "d172ffc2b30c75986446a72b625baecae123d1cc3882b20ca803c60b46dcd75d", + "checksum": "c4755265f6b6ff0bbd275a4f4e0153d7c08d80cb2711b821f5eacb511cca1f38", "checksum_algorithm": "sha256" }, { "name": "code-review", "file": "skills/code-review/SKILL.md", "version": "20260421007", - "checksum": "5bcdddc03ce0a54997037210b38e7b4ed22828cfba6a76153afaa94cf616527a", + "checksum": "17292327b093a21cd3845d67ea367480c3eedfdaacdb7752e027e05b1ef0031f", "checksum_algorithm": "sha256" }, { "name": "codeql", "file": "skills/codeql/SKILL.md", "version": "20260502026", - "checksum": "1b1b5800be204cc0e5dc6b4a8fcb9a7b916cfba2f96d5f99dbb6d7e213b8e95c", + "checksum": "199f3317399ce02ad2f80b8a51e075a060d2b2df9a46fd387892d260f39ef651", "checksum_algorithm": "sha256" }, { "name": "concise", "file": "skills/concise/SKILL.md", "version": "20260421008", - "checksum": "3a07860ba6c83a97c9ad5496e124be4e5277fc0b811fe25dcd5bfdb7d8252b98", + "checksum": "a7688530c1bf7656d78611b3bc82221bc68e29f1977ec9ae722a41d4bdfba168", "checksum_algorithm": "sha256" }, { "name": "consult", "file": "skills/consult/SKILL.md", "version": "20260421009", - "checksum": "90e1b0d0ff757c8e5879832c5ac6c41ec4054e9bff5b12a14531d427c09bcbad", + "checksum": "b21a457a4d8c615208aef85735f77524192c22de69ee52edab2da4b94c1b2871", "checksum_algorithm": "sha256" }, { "name": "container", "file": "skills/container/SKILL.md", "version": "20260421010", - "checksum": "e135b18cb972d30b77de4492db0437853ff1f49bad60a9113a034e39154d30e1", + "checksum": "0e530588b322bfbe7d40d510817ada6395234d9188ef348dc98e82d7dc9641ab", "checksum_algorithm": "sha256" }, { "name": "conventional-commit", "file": "skills/conventional-commit/SKILL.md", "version": "20260502024", - "checksum": "76477b42b17c9f6eec92baa36dc172e7ee98b20a86399eabdd2b7d2a90923509", + "checksum": "f00f8d0521a7b296284bb486c469f4b39cf19ca212e2fc01671f0785d979507b", "checksum_algorithm": "sha256" }, { "name": "debug", "file": "skills/debug/SKILL.md", "version": "20260421011", - "checksum": "8e46a2723004bc86f6aee50f492b73045acdab66c964787c58a988c98684750b", + "checksum": "286a17b8a7fe00e2702ed042d5101d1355f0373bbd3599df6a2098937ae54fe6", "checksum_algorithm": "sha256" }, { "name": "dependabot", "file": "skills/dependabot/SKILL.md", "version": "20260502027", - "checksum": "3ce5836bf870f73805800f672d379aa10f5aff5a522017a77875d9339bb99984", + "checksum": "f9bc59d261d6a589703e3d80e6c07d694552ced37e1746f683517fdf0ab757d8", "checksum_algorithm": "sha256" }, { "name": "dependency", "file": "skills/dependency/SKILL.md", "version": "20260421012", - "checksum": "46e75e8a28b1af5a60da7d9b3d1e46f0914b2f10d8733b37e6577e62b0074724", + "checksum": "82e51170a8ff0015cc613342b2adbc6ffb44e004ed41632ab0d5602271bcb92b", "checksum_algorithm": "sha256" }, { "name": "design", "file": "skills/design/SKILL.md", "version": "20260421013", - "checksum": "e21a674324244412f7b4a5bc010da4a7b9dfcf3522d54281bc9cf69f0b1126bd", + "checksum": "880e577dfe3a7c6f51c682f0e230b33486ebbe5fc882a2a784f34457365eab77", "checksum_algorithm": "sha256" }, { "name": "docs", "file": "skills/docs/SKILL.md", "version": "20260421014", - "checksum": "7d34421800bf04fe5a0782abfc5702a421419d31a5c66fd495d7dc8a6e2c64e3", + "checksum": "0007ac18f21ab5d57276c8b706f0dae2a03535609d68f536d7fc4156d8d52064", "checksum_algorithm": "sha256" }, { "name": "explore", "file": "skills/explore/SKILL.md", "version": "20260421015", - "checksum": "a257941d7b577f782b5d41703b1fbd25c5ab13f664e696d83cc3a9fd91a5565f", + "checksum": "74cffe4c27d739deae08fa580c0e80ac8e86f44692a5f91b9323e192ba8205d5", "checksum_algorithm": "sha256" }, { "name": "gdpr", "file": "skills/gdpr/SKILL.md", "version": "20260502029", - "checksum": "94207650498b56f246392449f57b4af4659bdfe511e7a6c2f23022ff2f68b08d", + "checksum": "69840bdc2bfe4fe97e7aabed28a72610b40a8107759ed7abc652a2b3c097aa1a", "checksum_algorithm": "sha256" }, { "name": "gh-issues", "file": "skills/gh-issues/SKILL.md", "version": "20260502025", - "checksum": "7a2b9b7463a40436fe77c77d066dd455ddb4abef877726ba041b89cbaf030280", + "checksum": "8890f165be2fdde02cb7c2c35879c59f172630a9f26f27ded8e2560c7494509b", "checksum_algorithm": "sha256" }, { "name": "gh-release", "file": "skills/gh-release/SKILL.md", "version": "20260502023", - "checksum": "beed7f98cb52222f688fed5a83d376acea1215c6654c570487412792bb577462", + "checksum": "fa755f6e24e8d9d06284b9e6838ca7ac373c162aa0a3796b15ba5bfcdc61e12e", "checksum_algorithm": "sha256" }, { "name": "guardrails", "file": "skills/guardrails/SKILL.md", "version": "20260421016", - "checksum": "8ec7213e1f8c85b4975ebb032d897e84e372c0279e3265da1098fc86cf98695f", + "checksum": "b5c0f993f26692f117a9103e3101a132133dee78e45a18b2692f66ce557735e9", "checksum_algorithm": "sha256" }, { "name": "helm", "file": "skills/helm/SKILL.md", "version": "20260502037", - "checksum": "13600308860723f683802431ac7a8c2c3834c40e521055854cdf3d3290570689", + "checksum": "508d33f75bd7d2c8a243b5feb94b020186ff89abc871f6f6690852dd0ce75b33", "checksum_algorithm": "sha256" }, { "name": "incident", "file": "skills/incident/SKILL.md", "version": "20260503002", - "checksum": "d337c4f145c09af856f4a05f41b11f2e46f3822f77e067e1a6e39de5f2af1fe8", + "checksum": "4ae13444c51626b7b9f92849be08951995c6b558f42d6a963598b93da228185e", "checksum_algorithm": "sha256" }, { "name": "inspect", "file": "skills/inspect/SKILL.md", "version": "20260421018", - "checksum": "7997cb4477f9b1a8a6753082a4f3ea0c9b5a1a77520a3f20527f8c153c968a27", + "checksum": "31256c09d7583c57b1c3b1e0ee9aa993218ed671c3ad797427a1e8b4bd6acc5f", "checksum_algorithm": "sha256" }, { "name": "k8s", "file": "skills/k8s/SKILL.md", "version": "20260502036", - "checksum": "d178a4d86f6ca5f4144cd8212eaf09979bb48afa55cb482409609aa50c5f0f88", + "checksum": "3c15b01cd5283bb3eebe3a3bfdc03f7db6869dde0ea5cb47123f569fc73f2d2c", "checksum_algorithm": "sha256" }, { "name": "migrate", "file": "skills/migrate/SKILL.md", "version": "20260421019", - "checksum": "c6c18c750319c0feb14526d637cb591f51ffde341b1385b4063aeb1edf12fd8b", + "checksum": "9aa0d50ddf33695f840f09e5914fcd3e3b1fb7826708f6a141880c411cae64fd", "checksum_algorithm": "sha256" }, { "name": "onboard", "file": "skills/onboard/SKILL.md", "version": "20260421020", - "checksum": "b96fb4067121eabbdf686641b55d32f253282e0b734fa4b2df6e4a9e85938cde", + "checksum": "f5db644175563ed046668984fe3c5575f8b3d94e608114b2a8e43e298a0dab5e", "checksum_algorithm": "sha256" }, { "name": "openapi", "file": "skills/openapi/SKILL.md", "version": "20260421021", - "checksum": "75d7cab59b1cde6dc91490867eb8537821059960957ba21ebd24d922e9fe7a91", + "checksum": "13435e37d104ae8fc7ce98bfd39bf3226ba2cc44dcce7c29d0c2214320a94aa7", "checksum_algorithm": "sha256" }, { "name": "performance", "file": "skills/performance/SKILL.md", "version": "20260421022", - "checksum": "e8cf2eba2a3fdc162d03afeaba260bd0c2f0aac47121c26dc9d63614bff971ec", + "checksum": "7bfe8f08b57a6b16eb5b7d048684abd88bce6bad48021825930ed677c010513b", "checksum_algorithm": "sha256" }, { "name": "postmortem", "file": "skills/postmortem/SKILL.md", "version": "20260503001", - "checksum": "ead97f2a5b28672f4ac483e467eec5b0d2a7e3b96eceaaa2017c918972c2d827", + "checksum": "ed01914fd3782d5380edb2452ce886849a5431ce67a23f24c5ddcaaca43f1fd1", "checksum_algorithm": "sha256" }, { "name": "pr", "file": "skills/pr/SKILL.md", "version": "20260502013", - "checksum": "ed4b13b69b325b21d9abf17ae9be34c2b2fa6ff1dd78cc6ee04af73d428438b8", + "checksum": "32551868293c30dcdbd2ba390823dbef36b1d4421d42912480d1ad8a991b49cd", "checksum_algorithm": "sha256" }, { "name": "rancher", "file": "skills/rancher/SKILL.md", "version": "20260502038", - "checksum": "c86759f257553554f2069cb6d0eda93bd98fd5c2fb251b1c8cc423302f33b01d", + "checksum": "b7b61eb2cbc76bdeba886e04faf596023cc95f33c1128c1ed1c318f5e01bb604", "checksum_algorithm": "sha256" }, { "name": "rca", "file": "skills/rca/SKILL.md", "version": "20260503001", - "checksum": "4d2347e1bab1d0717669b78160a5703ba4d1bb3486a9499924ca569e6f049f0e", + "checksum": "480e4ded32dce4d9e558d6cb0cb323412e2a260950f8feb874891f185fbe06c3", "checksum_algorithm": "sha256" }, { "name": "refactor", "file": "skills/refactor/SKILL.md", "version": "20260421023", - "checksum": "7559d9148ee0ed7434162f672fe47329a417fd332af92a3637c47030e6b8f271", + "checksum": "4159e7f0a769afae1ae85851fba644e06c71c47205a9b5ad0db502d7af47630f", "checksum_algorithm": "sha256" }, { "name": "release-notes", "file": "skills/release-notes/SKILL.md", "version": "20260502014", - "checksum": "81fd326b93789aad264e1ad27a73d00efd76c4486b5a20935bafcb18ebd43758", + "checksum": "58e9a6e4bf52181ae7e512362033280ab8fd3354beb2587811b560c71bccbd88", "checksum_algorithm": "sha256" }, { "name": "requirements", "file": "skills/requirements/SKILL.md", "version": "20260421024", - "checksum": "312e1c24bc43828798e86b881204196ebcad4f90ff5cfa63d099b8836e4f1e49", + "checksum": "fb596a45f7a39902b72149f41ad79abf654c2ae2e42951d0cdd2e5a5f7da255a", "checksum_algorithm": "sha256" }, { "name": "secret-scan", "file": "skills/secret-scan/SKILL.md", "version": "20260502028", - "checksum": "7a83d376b062bcf497ea89dd1e44a8f9fb945349d6b66513018ce1abe265e277", + "checksum": "f9147ea425731643e0a0daef9c3836994093dcf4e5d0a7ff43de636e6a16b55d", "checksum_algorithm": "sha256" }, { "name": "security", "file": "skills/security/SKILL.md", "version": "20260421025", - "checksum": "f58a629804bec6f6b5839eea1e7754458bf85cceb7403b9470f80513119a306d", + "checksum": "2895a95ae6826064094f272da634cd49038040c8e3b53aa7ab2a8148ef0cfa92", "checksum_algorithm": "sha256" }, { "name": "terraform", "file": "skills/terraform/SKILL.md", "version": "20260502030", - "checksum": "b24b008c171aafbf81902b3f52661b4acc09e8980e42ebf579ac3566dc138de5", + "checksum": "355520d94f5b0af10db7aa2a9fe4bc23eeb64f119377f23ae52e2e4cf4d13448", "checksum_algorithm": "sha256" }, { "name": "terragrunt", "file": "skills/terragrunt/SKILL.md", "version": "20260502031", - "checksum": "33a208ccc4dfff01b59110445e0fd851e5c9d5e094711456c1cd1efdc68d440d", + "checksum": "e81301a82b0aeea974a9e2e6d02ab8915ad328421d8314b33ab1412929ec32e7", "checksum_algorithm": "sha256" }, { "name": "threat-model", "file": "skills/threat-model/SKILL.md", "version": "20260502021", - "checksum": "11504d508ddfc6e9e2ded8dfac8fe6fb7af691c7ef27fdc539c5f8f870fc06bb", + "checksum": "aea7fccd1ed184d005c8ecc9bc807ae7c3389561ca9e850abafa88a974ab5501", "checksum_algorithm": "sha256" }, { "name": "verify", "file": "skills/verify/SKILL.md", "version": "20260421026", - "checksum": "b8f80fda903c0d55c556374626e308dcc61787d59e2e642f9c9513d06b0ac2b9", + "checksum": "59fbdac950bf0a82d33f13dda8afb71819045d4b2e18f37ecb7eb4e0ce590875", "checksum_algorithm": "sha256" }, { "name": "vision", "file": "skills/vision/SKILL.md", "version": "20260421027", - "checksum": "a74271c10816e3cee75fc1489d0b2defb391035b22f8d3da0cc951ae1afb7ef2", + "checksum": "46cdb79519bf700e6fdce208d11093ac732edd2325018524f4f7b85a90b0d968", "checksum_algorithm": "sha256" } ], @@ -319,42 +319,42 @@ "name": "architect", "file": "agents/architect.agent.md", "version": "20260503022", - "checksum": "524572e506ea4836237141a1bb2f9eee36de9f7342bf42f41442576b032e916a", + "checksum": "a2fffcbf0fd416ffcfa36964c97b436e10943345dcf359c1a42d2b6cf6aeacb9", "checksum_algorithm": "sha256" }, { "name": "designer", "file": "agents/designer.agent.md", "version": "20260503024", - "checksum": "a7afb743e80494e6a4a4fa2a93cc469b9eff87eafb5bde1a27ab90391f590aca", + "checksum": "513bdd6f182bb33d874953359340bece823befedd34945d3dd03405391f7b53b", "checksum_algorithm": "sha256" }, { "name": "engineer", "file": "agents/engineer.agent.md", "version": "20260503024", - "checksum": "1d06a70f56a98b30b0148a6b6d7df5b75f8cefb34aaa5100521faa7afe068ebf", + "checksum": "1d3b1b16b88f291439ec9807138671e38d5bcd6bdcbf1d7f8dcc5f84e28e7125", "checksum_algorithm": "sha256" }, { "name": "product", "file": "agents/product.agent.md", "version": "20260503021", - "checksum": "ac22faaeb1577458a88d6314e83d0c79b1479c45b066f7f989afb59f86b190a5", + "checksum": "d313ffa4acaf0b8b58c64621389be97c62328abd5ec4e1d58f56a019102eddba", "checksum_algorithm": "sha256" }, { "name": "release", "file": "agents/release.agent.md", "version": "20260503020", - "checksum": "5dcee462a58ec95b03110fdf59986ded0f86c1cc1db6b817e6c9a44fc5ba5f14", + "checksum": "773dd65312007d80f5dd95d26858bee60599ba6f4515f3531f5384c6710c0468", "checksum_algorithm": "sha256" }, { "name": "tester", "file": "agents/tester.agent.md", "version": "20260503026", - "checksum": "ecce547f703fcee918d1b4991f5ce5e850392d4f3fbf01b186723cc0a8961ecf", + "checksum": "e36ea99aa29200b0d1b89d5958980186f96e948afc5ea43c34500c2a09b7b83c", "checksum_algorithm": "sha256" } ], @@ -363,84 +363,84 @@ "name": "git", "file": "instructions/git.instructions.md", "version": "20260421001", - "checksum": "ed6a191176e32631e2d572cb21278b555f6696a859839579821631156a8b35a5", + "checksum": "4cb957fd493b9f0c6fa0845b6a2743ff0eec02e335491b9a02d7ec62544eacc5", "checksum_algorithm": "sha256" }, { "name": "helm", "file": "instructions/helm.instructions.md", "version": "20260502040", - "checksum": "a60520853b79751517f089136ba7fb182feb1ea8e9c9495890e5c14ff32afd7b", + "checksum": "805353d22a08577058f5d13d0c281ac7a8f098c7d7408dd044696028ac696cc5", "checksum_algorithm": "sha256" }, { "name": "java", "file": "instructions/java.instructions.md", "version": "20260502001", - "checksum": "69dc2bf3a5428ed03d77c2789871985ea7c5881af89c2b414548fca6e9488464", + "checksum": "65c1ba8c0705b77b207ccc980bf6183310b4774030db6e0e88e9bced40b8a0f9", "checksum_algorithm": "sha256" }, { "name": "k8s", "file": "instructions/k8s.instructions.md", "version": "20260502039", - "checksum": "f19f1060bf2a1424950496290a43a3535f9749e4ed838cf7475fe35eb9487d67", + "checksum": "db9fe194a3a50dcacf278886a8278dce98a57f1eba46cf5a68cf40d64511ba6b", "checksum_algorithm": "sha256" }, { "name": "markdown", "file": "instructions/markdown.instructions.md", "version": "20260502002", - "checksum": "59c93c5b0e63360aff18ab3a7a207f4e798efd1f2c26b0a561a4ef4ea688c5c5", + "checksum": "cb197a6f1c67716d21c269094df82b7f675c68e5f390ad1d4f61e58d52080297", "checksum_algorithm": "sha256" }, { "name": "python", "file": "instructions/python.instructions.md", "version": "20260421002", - "checksum": "ac40fffd3d3a3f9f8ca43e10ca603a187578c9d517533529a3d647b61cedf56c", + "checksum": "33298cb94c45ff7a934466d0fdaa885341f63336844fb047e953825810dc83dd", "checksum_algorithm": "sha256" }, { "name": "rancher", "file": "instructions/rancher.instructions.md", "version": "20260502041", - "checksum": "4e643b1e078e9e8f127f1f2cf7707b36109e28b8697e208d8d3804848b867eec", + "checksum": "9d4886f04b633f968a33469de8520cced602792073106760e01ed965bdc284bd", "checksum_algorithm": "sha256" }, { "name": "security", "file": "instructions/security.instructions.md", "version": "20260502003", - "checksum": "a943df637e44cb23c8e82f8b39bf70558bc502ece5af9a03a15d8492cffb51a4", + "checksum": "f2424e0dbee186186f1488837e1aa411bba6a613260c4b967b80295bb5d3084a", "checksum_algorithm": "sha256" }, { "name": "terraform", "file": "instructions/terraform.instructions.md", "version": "20260502034", - "checksum": "1bada82da46a6359bb1814b6642074be6c0b7de9c2c6ca59e031a4d05ea4ea8c", + "checksum": "09a54830d24cf3c103f835206358f197962d484b9bca3cac0e92db332811c75a", "checksum_algorithm": "sha256" }, { "name": "terragrunt", "file": "instructions/terragrunt.instructions.md", "version": "20260502035", - "checksum": "13c0059c366624ab482c2b3d24a353bcb0a2ffbb6bf735abd18796969a3e4020", + "checksum": "f911d207ac80fe1526e6178079faaa4c75a462a6b2b6d3b4ee7cf44d3d29b0f5", "checksum_algorithm": "sha256" }, { "name": "testing", "file": "instructions/testing.instructions.md", "version": "20260502004", - "checksum": "cbd1948f367c32c39032209e5ed9fcfff8ce6c46c6324a4aa9a8550365873ca3", + "checksum": "1cb3979aa86d86a57d78070ff6a683fecce1837719560dc430f9fa44b95b06ef", "checksum_algorithm": "sha256" }, { "name": "typescript", "file": "instructions/typescript.instructions.md", "version": "20260502005", - "checksum": "fe412ba2e60baea66d0d07ae0c153fa2a9157bd57556fa7476f46ce466160ad3", + "checksum": "696a2faf692022eae14212d2ff91c96439caa4749f51bbb62b26e460b517a6eb", "checksum_algorithm": "sha256" } ], @@ -449,49 +449,49 @@ "name": "api-design-review", "file": "prompts/api-design-review.prompt.md", "version": "20260502006", - "checksum": "1ba62a6f78b836256fe578c4ca312de24599c79b018119b81a100f6de4e0da11", + "checksum": "081340767c8d48ef302cd473ad2deab35d7dbe2e287b39096ba7c98ce4f49520", "checksum_algorithm": "sha256" }, { "name": "architecture-risk", "file": "prompts/architecture-risk.prompt.md", "version": "20260502007", - "checksum": "14fa36e36948309827c1c1296ff2c8b1f1306cfb19057ef190a5e60f6cc04a61", + "checksum": "f1676e2a7b5f6612639863cf2c5839befa97aaa1b20837c3b8a848b6f625b216", "checksum_algorithm": "sha256" }, { "name": "code-review", "file": "prompts/code-review.prompt.md", "version": "20260502008", - "checksum": "b6499ca66706a08ced7b9bf69bd288c2fecb684e1c85e333ee91d36ba7e66c3f", + "checksum": "e0703ddfdd6fa9f10ee0006eb3601428c5458742b5fd9582acd7295aa769c867", "checksum_algorithm": "sha256" }, { "name": "dependency-audit", "file": "prompts/dependency-audit.prompt.md", "version": "20260502009", - "checksum": "51cead168dbbe52b455813cc620a8426ec287ceb2994831aa6ae115b4e49d0b0", + "checksum": "8b44c09c72f3531c3d5f6b17fcb6c7f3436add6e224abe18474d61d59864db1a", "checksum_algorithm": "sha256" }, { "name": "incident-timeline", "file": "prompts/incident-timeline.prompt.md", "version": "20260502010", - "checksum": "28627dc7c362d510f312281fe0bda30e1927998bae440808af3a92d06c455393", + "checksum": "991380657d904673918a6a42ab36cee839eaec9f5b27a3aac3518d6d4f429e62", "checksum_algorithm": "sha256" }, { "name": "migration-safety", "file": "prompts/migration-safety.prompt.md", "version": "20260502011", - "checksum": "814258882774ff98724b14caa0bc9ee35dc68f46e72e62b24884052e79ffacfa", + "checksum": "a514704586f271cba25ee285df76acb34d1ac71cd1bee4280eb4887cca1bafcc", "checksum_algorithm": "sha256" }, { "name": "release-readiness", "file": "prompts/release-readiness.prompt.md", "version": "20260502012", - "checksum": "49898a07169504acba1c2195abf730be77d1e17298cb4676f3ed926a27e31dc6", + "checksum": "99dcf393a8f1cc474f3f9c50421834d134b621da28afbc4a1ddc7c944fdd2920", "checksum_algorithm": "sha256" } ] diff --git a/CHANGELOG.md b/CHANGELOG.md index f58630f..6de4fcd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,51 +4,46 @@ ## [3.0.0](https://github.com/eschaar/vstack/compare/2.2.0...3.0.0) (2026-05-06) - ### ⚠ BREAKING CHANGES -* **manifest:** vstack.json moves from .github/vstack.json to .vstack/vstack.json; run `vstack manifest upgrade` to migrate existing projects +- **manifest:** vstack.json moves from .github/vstack.json to .vstack/vstack.json; run `vstack manifest upgrade` to migrate existing projects ### Features -* **agents:** generate artifacts section from config.yaml ([b47f410](https://github.com/eschaar/vstack/commit/b47f4108f2c6165f0f2d37913774fd46061af65c)) -* **config:** implement exclude filter in .vstack/config.yaml ([036b9ae](https://github.com/eschaar/vstack/commit/036b9ae51d322da0aa27ef2056a06ee011792480)) -* **config:** support artifacts_root override in .vstack/config.yaml ([2a7de61](https://github.com/eschaar/vstack/commit/2a7de61df40317109a80f0857ddc3106d84192f2)) -* **install:** write .vstack/.gitignore on every install ([526c6c7](https://github.com/eschaar/vstack/commit/526c6c7dfdecb233eed3e057b6b02c5842c1ef16)) -* **manifest:** move install manifest from .github to .vstack ([3a11063](https://github.com/eschaar/vstack/commit/3a11063f537bc307e70675d94421029791d30ad3)) -* **workflow:** update skill templates, ADRs, and agent configs for genericity ([db0b2f1](https://github.com/eschaar/vstack/commit/db0b2f124019464140545f7a1c2eeeda3388c3cf)) - +- **agents:** generate artifacts section from config.yaml ([b47f410](https://github.com/eschaar/vstack/commit/b47f4108f2c6165f0f2d37913774fd46061af65c)) +- **config:** implement exclude filter in .vstack/config.yaml ([036b9ae](https://github.com/eschaar/vstack/commit/036b9ae51d322da0aa27ef2056a06ee011792480)) +- **config:** support artifacts_root override in .vstack/config.yaml ([2a7de61](https://github.com/eschaar/vstack/commit/2a7de61df40317109a80f0857ddc3106d84192f2)) +- **install:** write .vstack/.gitignore on every install ([526c6c7](https://github.com/eschaar/vstack/commit/526c6c7dfdecb233eed3e057b6b02c5842c1ef16)) +- **manifest:** move install manifest from .github to .vstack ([3a11063](https://github.com/eschaar/vstack/commit/3a11063f537bc307e70675d94421029791d30ad3)) +- **workflow:** update skill templates, ADRs, and agent configs for genericity ([db0b2f1](https://github.com/eschaar/vstack/commit/db0b2f124019464140545f7a1c2eeeda3388c3cf)) ### Fixes -* **exclude:** raise ValueError when agents excluded; add ADR-021/022 to overview ([3298340](https://github.com/eschaar/vstack/commit/3298340b4f5af7ac4d82b3f13a5b36f8cd2a502a)) -* **review:** address PR review comments ([771ab31](https://github.com/eschaar/vstack/commit/771ab31dff70178cde83d27edfb0145e9ff7ff66)) - +- **exclude:** raise ValueError when agents excluded; add ADR-021/022 to overview ([3298340](https://github.com/eschaar/vstack/commit/3298340b4f5af7ac4d82b3f13a5b36f8cd2a502a)) +- **review:** address PR review comments ([771ab31](https://github.com/eschaar/vstack/commit/771ab31dff70178cde83d27edfb0145e9ff7ff66)) ### Refactoring -* **config:** rename artifacts_root to artifacts.root in config.yaml ([e9c14b4](https://github.com/eschaar/vstack/commit/e9c14b449fe7179333e96eca7fccf8165a9440a0)) - +- **config:** rename artifacts_root to artifacts.root in config.yaml ([e9c14b4](https://github.com/eschaar/vstack/commit/e9c14b449fe7179333e96eca7fccf8165a9440a0)) ### Documentation -* ADR-022 selective exclude filter ([56771d3](https://github.com/eschaar/vstack/commit/56771d3d50bafada8751f790b442026a05e4d513)) -* **agents:** update artifacts schema docs to reflect dir/ARTIFACTS_DOCS_ROOT design ([a408468](https://github.com/eschaar/vstack/commit/a4084684e7449ce55f569d11d0dccbd535f2f141)) -* document exclude filter, install vs init flow, and config.yaml schema ([3296020](https://github.com/eschaar/vstack/commit/3296020229a950ce6932a5a8322e9e81959587f7)) -* **readme:** add no-target variants for install and init to CLI table ([9c06f5a](https://github.com/eschaar/vstack/commit/9c06f5ab10e79eab5bd110cf9bdf756ba617257f)) -* rename Option A/B to direct execution / orchestrated pipeline in ADRs 017, 018, 020, 021, 022 ([56771d3](https://github.com/eschaar/vstack/commit/56771d3d50bafada8751f790b442026a05e4d513)) -* **roadmap:** rationalise candidate items and restructure roadmap ([831e3e0](https://github.com/eschaar/vstack/commit/831e3e02f87d61c279d62ad5726474fb06217f59)) -* **roadmap:** record install target override as not planned ([168f499](https://github.com/eschaar/vstack/commit/168f499c9081b6b4620683c4d5c8c611f1e3634e)) -* split roadmap row into selective install (shipped) and template overlays (candidate) ([56771d3](https://github.com/eschaar/vstack/commit/56771d3d50bafada8751f790b442026a05e4d513)) - +- ADR-022 selective exclude filter ([56771d3](https://github.com/eschaar/vstack/commit/56771d3d50bafada8751f790b442026a05e4d513)) +- **agents:** update artifacts schema docs to reflect dir/ARTIFACTS_DOCS_ROOT design ([a408468](https://github.com/eschaar/vstack/commit/a4084684e7449ce55f569d11d0dccbd535f2f141)) +- document exclude filter, install vs init flow, and config.yaml schema ([3296020](https://github.com/eschaar/vstack/commit/3296020229a950ce6932a5a8322e9e81959587f7)) +- **readme:** add no-target variants for install and init to CLI table ([9c06f5a](https://github.com/eschaar/vstack/commit/9c06f5ab10e79eab5bd110cf9bdf756ba617257f)) +- rename Option A/B to direct execution / orchestrated pipeline in ADRs 017, 018, 020, 021, 022 ([56771d3](https://github.com/eschaar/vstack/commit/56771d3d50bafada8751f790b442026a05e4d513)) +- **roadmap:** rationalise candidate items and restructure roadmap ([831e3e0](https://github.com/eschaar/vstack/commit/831e3e02f87d61c279d62ad5726474fb06217f59)) +- **roadmap:** record install target override as not planned ([168f499](https://github.com/eschaar/vstack/commit/168f499c9081b6b4620683c4d5c8c611f1e3634e)) +- split roadmap row into selective install (shipped) and template overlays (candidate) ([56771d3](https://github.com/eschaar/vstack/commit/56771d3d50bafada8751f790b442026a05e4d513)) ### Maintenance -* **cleanup:** remove stale .github/vstack.json and add missing init command ([99ded6b](https://github.com/eschaar/vstack/commit/99ded6b428a661a184b73d592da0df27dfd5f9ee)) -* **install:** regenerate artifacts and populate manifest ([56771d3](https://github.com/eschaar/vstack/commit/56771d3d50bafada8751f790b442026a05e4d513)) -* **lint:** disable MD012 for release-please CHANGELOG double blank lines ([5044cd7](https://github.com/eschaar/vstack/commit/5044cd73e248be3a0dc53a50adde3e39ec968f55)) -* **release:** add v3.0.0 release notes and mark roadmap rows as shipped ([55bfb69](https://github.com/eschaar/vstack/commit/55bfb6956758b3f4703905d4e337ce20f2a64cd1)) -* **release:** update reports, finalise v3.0.0 release notes for 2026-05-06 ([ad641f0](https://github.com/eschaar/vstack/commit/ad641f091324a1f9428e0cace67dca61acd41ce1)) +- **cleanup:** remove stale .github/vstack.json and add missing init command ([99ded6b](https://github.com/eschaar/vstack/commit/99ded6b428a661a184b73d592da0df27dfd5f9ee)) +- **install:** regenerate artifacts and populate manifest ([56771d3](https://github.com/eschaar/vstack/commit/56771d3d50bafada8751f790b442026a05e4d513)) +- **lint:** disable MD012 for release-please CHANGELOG double blank lines ([5044cd7](https://github.com/eschaar/vstack/commit/5044cd73e248be3a0dc53a50adde3e39ec968f55)) +- **release:** add v3.0.0 release notes and mark roadmap rows as shipped ([55bfb69](https://github.com/eschaar/vstack/commit/55bfb6956758b3f4703905d4e337ce20f2a64cd1)) +- **release:** update reports, finalise v3.0.0 release notes for 2026-05-06 ([ad641f0](https://github.com/eschaar/vstack/commit/ad641f091324a1f9428e0cace67dca61acd41ce1)) ## [2.2.0](https://github.com/eschaar/vstack/compare/2.1.0...2.2.0) (2026-05-02) From 9ed33063938d104a9e49782af6492224d1b1243d Mon Sep 17 00:00:00 2001 From: Erik Schaareman Date: Sat, 9 May 2026 02:22:00 +0200 Subject: [PATCH 06/25] feat(parser): replace hand-rolled YAML parser with PyYAML Introduce pyyaml>=6.0 as the sole runtime dependency and replace the hand-rolled state-machine parser (~200 lines) in frontmatter/parser.py with yaml.safe_load. Keeps the public API unchanged (parse, parse_yaml). Pre-processes bare '- *' list items to '- ''*''' before passing to yaml.safe_load, since VS Code uses '*' as a wildcard in agents: lists and PyYAML treats bare '*' as an alias marker. Also quote YAML-special leading characters ('*', '&', '!') in list items emitted by the serializer, fix int version comparison in cli/init.py (PyYAML parses 'version: 20260421027' as int), and remove dead-code string-reparse fallbacks from agents/generator.py and cli/interface.py. Add ADR-025 (pyyaml-runtime-dependency) and amend ADR-006 with a scope note clarifying it covers skill template content only, not pip deps. pyproject.toml: add pyyaml>=6.0 to [project] dependencies and to [testenv] deps (tox uses package=skip so it does not auto-install). --- .../adr/006-no-runtime-dependency.md | 6 +- .../adr/025-pyyaml-runtime-dependency.md | 77 ++++++ pyproject.toml | 4 +- src/vstack/agents/generator.py | 16 -- src/vstack/cli/init.py | 2 +- src/vstack/cli/interface.py | 23 +- src/vstack/frontmatter/parser.py | 246 ++---------------- src/vstack/frontmatter/serializer.py | 16 +- tests/vstack/artifacts/test_generator.py | 2 +- tests/vstack/frontmatter/test_parser.py | 67 ++--- 10 files changed, 156 insertions(+), 303 deletions(-) create mode 100644 docs/architecture/adr/025-pyyaml-runtime-dependency.md diff --git a/docs/architecture/adr/006-no-runtime-dependency.md b/docs/architecture/adr/006-no-runtime-dependency.md index e8112e6..ea69fd3 100644 --- a/docs/architecture/adr/006-no-runtime-dependency.md +++ b/docs/architecture/adr/006-no-runtime-dependency.md @@ -3,7 +3,11 @@ > Maintained by: **architect** role **date:** 2026-03-27\ -**status:** accepted +**status:** accepted\ +**scope note:** This ADR covers **skill template content** only — the Markdown files +the AI agent executes. It does not govern the Python package's own `pip` dependencies. +The decision to introduce `pyyaml` as a runtime package dependency is documented in +ADR-025. ## context diff --git a/docs/architecture/adr/025-pyyaml-runtime-dependency.md b/docs/architecture/adr/025-pyyaml-runtime-dependency.md new file mode 100644 index 0000000..3c1f002 --- /dev/null +++ b/docs/architecture/adr/025-pyyaml-runtime-dependency.md @@ -0,0 +1,77 @@ +# ADR-025: Introduce PyYAML as Sole Runtime Dependency + +> Maintained by: **architect** role + +**date:** 2026-05-09\ +**status:** accepted\ +**amends:** ADR-006 (scope clarification — see that document) + +## context + +vstack was initially released with `dependencies = []`, keeping `pip install vstack` +free of any transitive pulls. The frontmatter parser (`FrontmatterParser._parse_yaml_block`) +was hand-rolled specifically to preserve that property. + +The YAML subset that vstack must parse has grown steadily as new config features were +added — `defaults:` blocks, `baseline:` flags, `hitl:` fields, `workflow:` blocks with +nested `stages` / `handoffs` object-lists, and raw mapping blocks for MCP-server config. +The parser now handles eight distinct structural cases, each represented by its own +state-machine flag, and relies on fragile indentation-level checks +(`line.startswith(" ")`, `" "`, `" "`) with no named constants and no +protection against tabs or irregular indentation. + +Continuing to extend the hand-rolled parser as the config schema grows is a maintenance +liability that outweighs the benefit of zero runtime dependencies. + +## decision + +- Add `pyyaml` as the sole entry in `[project] dependencies` in `pyproject.toml`. +- Replace the `_parse_yaml_block` state-machine body with a single `yaml.safe_load` call. +- Keep the `FrontmatterContent` dataclass and `FrontmatterParser.parse` / `parse_yaml` + public API surface unchanged so all callers remain unaffected. +- Retain `FrontmatterParser` as a class and its static methods; only the internal + `_parse_yaml_block` implementation changes. + +## alternatives considered + +- **Keep the hand-rolled parser** — avoids the dependency but means every new config + key may require new state-machine branches and indentation heuristics. Risk grows + linearly with schema complexity. +- **Use `ruamel.yaml`** — preserves comments and round-trips YAML faithfully; valuable + for tools that rewrite config files. vstack only reads config at install time, so + round-trip fidelity is not needed. `ruamel.yaml` also carries more transitive weight + than PyYAML. +- **Restrict the config schema** — deliberately keep frontmatter simple enough for the + hand-rolled parser. This constrains future workflow and orchestration features + (ADR-023, ADR-024) that already depend on nested YAML structures. +- **Optional dependency with stdlib fallback** — adds dead code paths and makes test + coverage of the fallback artificial. The fallback would need its own maintenance, + negating most of the benefit. + +## rationale + +PyYAML is the reference YAML 1.1 implementation for Python. It ships as a single +compiled wheel, has no runtime dependencies of its own on CPython 3.x, and is already +a transitive dependency of the vast majority of Python projects. `yaml.safe_load` has +a strong security track record when object deserialization is disabled (which `safe_load` +enforces by construction). + +The trade-off is clear: one well-audited, widely-deployed library in exchange for +permanently removing a class of parser fragility from the codebase. + +## impact + +- `pip install vstack` will pull `pyyaml` (≈ 143 kB wheel on CPython). +- `pyproject.toml`: `dependencies = ["pyyaml>=6.0"]`. +- `FrontmatterParser._parse_yaml_block`: implementation replaced; public API unchanged. +- Tests: existing parser tests continue to pass against the new implementation; + test_parser coverage remains at 100 %. +- ADR-006 is amended with a scope note; its decision (no external binaries in skill + content) is unaffected. + +## impact on future orchestrated pipeline + +The orchestrated pipeline runner (ADR-024) generates and reads `config.yaml` files +for each agent. Having a proper YAML parser available as a runtime dependency means +those config files can use the full YAML 1.1 feature set without risking parser +regressions. diff --git a/pyproject.toml b/pyproject.toml index d4d9764..4dd3e73 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,7 +5,7 @@ description = "VS Code-native AI engineering workflow system for microservices, license = "MIT" readme = "README-pypi.md" requires-python = ">=3.11,<3.15" -dependencies = [] +dependencies = ["pyyaml>=6.0"] keywords = [ "github-copilot", "vscode", @@ -69,6 +69,7 @@ ruff = ">=0.15.12" mypy = ">=1.20.2" pre-commit = ">=4.6.0" tox = ">=4.15" +types-PyYAML = ">=6.0" # --------------------------------------------------------------------------- @@ -163,6 +164,7 @@ package = skip deps = pytest>=9.0 pytest-cov>=6.0 + pyyaml>=6.0 setenv = PYTHONPATH = {tox_root}/src commands = diff --git a/src/vstack/agents/generator.py b/src/vstack/agents/generator.py index 4887964..7ef7433 100644 --- a/src/vstack/agents/generator.py +++ b/src/vstack/agents/generator.py @@ -86,19 +86,10 @@ def template_partials(self, tmpl_dir: Path) -> dict[str, str]: ``AGENT_ARTIFACTS_INPUT_COMMENTS``, ``AGENT_ARTIFACTS_OUTPUT_COMMENTS``, and ``AGENT_ARTIFACTS_BASELINE``. """ - 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 = {} @@ -173,13 +164,6 @@ def load_artifact_config(self, tmpl_dir: Path) -> dict: if artifacts_from_defaults: config["artifacts"] = artifacts_from_defaults handoffs_block = defaults.get("handoffs") or {} - if isinstance(handoffs_block, str) and handoffs_block.strip(): - from vstack.frontmatter import FrontmatterParser - - dedented = "\n".join( - line[2:] if line.startswith(" ") else line for line in handoffs_block.split("\n") - ) - handoffs_block = FrontmatterParser.parse_yaml(dedented) or {} if isinstance(handoffs_block, dict): handoff_prompt: str = str(handoffs_block.get("prompt", "") or "") else: diff --git a/src/vstack/cli/init.py b/src/vstack/cli/init.py index 5550fa8..12a3b3a 100644 --- a/src/vstack/cli/init.py +++ b/src/vstack/cli/init.py @@ -257,7 +257,7 @@ def _install_single_artifact( ) -> 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 + new_version = str((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 diff --git a/src/vstack/cli/interface.py b/src/vstack/cli/interface.py index e181f2d..1a1cdf0 100644 --- a/src/vstack/cli/interface.py +++ b/src/vstack/cli/interface.py @@ -102,13 +102,6 @@ def _read_exclude( 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() @@ -146,13 +139,6 @@ def _read_artifacts_root(install_dir: Path | None) -> str: 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", "") @@ -180,11 +166,6 @@ def _read_workflow_stages(install_dir: Path | None) -> list[dict]: return [] parsed = FrontmatterParser.parse_yaml(config_path.read_text(encoding="utf-8")) workflow = parsed.get("workflow", "") - if isinstance(workflow, str) and workflow.strip(): - dedented = "\n".join( - line[2:] if line.startswith(" ") else line for line in workflow.split("\n") - ) - workflow = FrontmatterParser.parse_yaml(dedented) or {} if not isinstance(workflow, dict): return [] stages_raw = workflow.get("stages", []) @@ -219,9 +200,7 @@ def _parse_stage_handoffs(item: dict) -> list[dict[str, str]]: raw = item.get("handoffs", "") parsed_block: dict | list | None = None - if isinstance(raw, str) and raw.strip(): - parsed_block = FrontmatterParser.parse_yaml(raw.strip()) - elif isinstance(raw, (dict, list)): + if isinstance(raw, (dict, list)): parsed_block = raw if isinstance(parsed_block, dict): diff --git a/src/vstack/frontmatter/parser.py b/src/vstack/frontmatter/parser.py index cc04c01..7ec3dac 100644 --- a/src/vstack/frontmatter/parser.py +++ b/src/vstack/frontmatter/parser.py @@ -1,11 +1,8 @@ -"""YAML frontmatter parser — no external dependencies. +"""YAML frontmatter parser. -Supports: -- String scalars (quoted and unquoted) -- Inline lists ``[a, b, c]`` -- Block lists ``\n - item`` -- Block sequences of mappings (object-lists) ``\n - key: val\n key2: val2`` -- Block scalars ``|`` +Delegates YAML parsing to :func:`yaml.safe_load` (PyYAML). Supports the full +YAML 1.1 feature set used by vstack config files, including nested mappings, +block sequences, block scalars, and inline lists. """ from __future__ import annotations @@ -13,6 +10,8 @@ import re from dataclasses import dataclass, field +import yaml + _FRONTMATTER_RE = re.compile(r"^---\n(.*?)\n---\n(.*)", re.DOTALL) @@ -50,40 +49,7 @@ def __bool__(self) -> bool: class FrontmatterParser: - """Parse the repository's supported subset of YAML frontmatter.""" - - @staticmethod - def _is_current_object_list_item(meta: dict, current_key: str) -> bool: - """Return ``True`` when ``current_key`` points to the active object-list item.""" - return ( - bool(current_key) - and isinstance(meta.get(current_key), list) - and bool(meta[current_key]) - and isinstance(meta[current_key][-1], dict) - ) - - @staticmethod - def _flush_object_block_scalar( - *, - meta: dict, - current_key: str, - object_scalar_field: str, - object_block_lines: list[str], - ) -> None: - """Flush buffered block-scalar content into the active object-list item.""" - text = " ".join(b for b in object_block_lines if b).strip() - if FrontmatterParser._is_current_object_list_item(meta, current_key): - meta[current_key][-1][object_scalar_field] = text - - @staticmethod - def _flush_raw_block(*, meta: dict, current_key: str, raw_lines: list[str]) -> None: - """Flush buffered raw block content into the current top-level key.""" - meta[current_key] = "\n".join(raw_lines).rstrip() - - @staticmethod - def _flush_block_scalar(*, meta: dict, current_key: str, block_lines: list[str]) -> None: - """Flush a buffered top-level block scalar into the current key.""" - meta[current_key] = " ".join(b for b in block_lines if b).strip() + """Parse YAML frontmatter using :func:`yaml.safe_load`.""" @staticmethod def parse(content: str) -> FrontmatterContent: @@ -113,192 +79,20 @@ def parse_yaml(raw: str) -> dict: # ── Internal ────────────────────────────────────────────────────────────── - @staticmethod - def _parse_scalar(val: str) -> str: - """Strip surrounding quotes from a YAML scalar string.""" - return val.strip().strip("\"'") - @staticmethod def _parse_yaml_block(raw: str) -> dict: - """Parse a minimal YAML subset (no external dependencies). + """Delegate YAML parsing to :func:`yaml.safe_load`. - Supports: string values, inline lists ``[a, b]``, - block lists ``\n - item``, block scalars ``|``, - block sequences of mappings (object-lists): - ``\n - key: val\n key2: val2``, - raw mapping blocks where the value is indented non-list YAML content: - ``\n server:\n type: local`` (used for ``mcp-servers``, ``hooks``, etc.), and - nested blocks inside object-list items: an empty-value 4-space key - (`` key:``) accumulates subsequent 6-space lines as a stripped raw - string that callers can re-parse (e.g. ``workflow.stages[].handoffs``). - """ - meta: dict = {} - current_key = "" - in_block_scalar = False - block_lines: list[str] = [] - in_raw_block = False - raw_lines: list[str] = [] - in_object_block_scalar = False - object_scalar_field = "" - object_block_lines: list[str] = [] - in_object_nested_block = False - object_nested_key = "" - object_nested_lines: list[str] = [] + Pre-processes ``- *`` (VS Code wildcard list items) into quoted form + so that PyYAML does not interpret the bare ``*`` as a YAML alias. - for line in raw.split("\n"): - if line.strip().startswith("#"): - continue - - # ── Nested block inside object-list item ───────────────────────── - if in_object_nested_block: - if line.startswith(" ") or line == "": - # Strip 6 leading spaces so the stored content is parseable - # as top-level YAML (for dict) or as a 0-indent list. - object_nested_lines.append(line[6:] if len(line) > 6 else "") - continue - else: - # Non-6-space line closes the nested block. - meta[current_key][-1][object_nested_key] = "\n".join( - object_nested_lines - ).rstrip() - in_object_nested_block = False - object_nested_key = "" - object_nested_lines = [] - # Fall through to process current line. - - if in_object_block_scalar: - if line.startswith(" ") or line == "": - object_block_lines.append(line.strip()) - continue - else: - FrontmatterParser._flush_object_block_scalar( - meta=meta, - current_key=current_key, - object_scalar_field=object_scalar_field, - object_block_lines=object_block_lines, - ) - in_object_block_scalar = False - object_scalar_field = "" - object_block_lines = [] - - # ── Raw block accumulation ──────────────────────────────────────── - if in_raw_block: - if line == "" or line.startswith(" "): - raw_lines.append(line) - continue - else: - # Non-indented line closes the raw block; fall through to process it - FrontmatterParser._flush_raw_block( - meta=meta, - current_key=current_key, - raw_lines=raw_lines, - ) - in_raw_block = False - raw_lines = [] - - if in_block_scalar: - if line.startswith(" ") or line == "": - block_lines.append(line.strip()) - continue - else: - FrontmatterParser._flush_block_scalar( - meta=meta, - current_key=current_key, - block_lines=block_lines, - ) - in_block_scalar = False - block_lines = [] - - # 4-space key: continuation of an object-list item - obj_kv = re.match(r"^ ([a-zA-Z_-]+):\s*(.*)$", line) - if obj_kv and FrontmatterParser._is_current_object_list_item(meta, current_key): - obj_key = obj_kv.group(1) - obj_val = obj_kv.group(2).strip() - if obj_val in ("|", "|-", "|+", ">", ">-", ">+"): - in_object_block_scalar = True - object_scalar_field = obj_key - object_block_lines = [] - meta[current_key][-1][obj_key] = "" - elif obj_val: - meta[current_key][-1][obj_key] = FrontmatterParser._parse_scalar(obj_val) - else: - # Empty value in an object-list item: start nested block - # accumulation for any following 6-space-indented content. - in_object_nested_block = True - object_nested_key = obj_key - object_nested_lines = [] - meta[current_key][-1][obj_key] = "" - continue - - # Raw block trigger: 2-space non-list indented line when the current key - # has an empty provisional value (set by a bare ``key:`` with no value). - if ( - line.startswith(" ") - and not line.startswith(" - ") - and current_key - and meta.get(current_key) == [] - ): - in_raw_block = True - raw_lines = [line] - meta[current_key] = "" # clear empty-list placeholder - continue - - # 2-space list item - list_match = re.match(r"^ - (.+)$", line) - if list_match and current_key: - item_str = list_match.group(1).strip() - item_kv = re.match(r"^([a-zA-Z_-]+):\s*(.*)$", item_str) - if item_kv: - # Object-list item — first key bootstraps the dict - if not isinstance(meta.get(current_key), list): - meta[current_key] = [] - meta[current_key].append( - {item_kv.group(1): FrontmatterParser._parse_scalar(item_kv.group(2))} - ) - else: - if not isinstance(meta.get(current_key), list): - meta[current_key] = [] - meta[current_key].append(item_str.strip("\"'")) - continue - - kv = re.match(r"^([a-zA-Z_-]+):\s*(.*)$", line) - if kv: - current_key = kv.group(1) - val = kv.group(2).strip() - if val.startswith("[") and val.endswith("]"): - meta[current_key] = [ - v.strip().strip("\"'") for v in val[1:-1].split(",") if v.strip() - ] - elif val in ("|", "|-", "|+", ">", ">-", ">+"): - in_block_scalar = True - block_lines = [] - meta[current_key] = "" - elif val == "": - meta[current_key] = [] # provisional: may become a raw block - else: - meta[current_key] = val.strip("\"'") - - if in_object_block_scalar: - if object_scalar_field: - FrontmatterParser._flush_object_block_scalar( - meta=meta, - current_key=current_key, - object_scalar_field=object_scalar_field, - object_block_lines=object_block_lines, - ) - if in_object_nested_block: - meta[current_key][-1][object_nested_key] = "\n".join(object_nested_lines).rstrip() - if in_raw_block and raw_lines: - FrontmatterParser._flush_raw_block( - meta=meta, - current_key=current_key, - raw_lines=raw_lines, - ) - if in_block_scalar and block_lines: - FrontmatterParser._flush_block_scalar( - meta=meta, - current_key=current_key, - block_lines=block_lines, - ) - - return meta + Returns an empty dict when *raw* is empty or parses to a non-mapping + value. + """ + # Replace bare wildcard items (`` - *``) with single-quoted form so that + # PyYAML does not treat the leading ``*`` as a YAML alias marker. + preprocessed = re.sub(r"^(\s*-\s)\*(\s*)$", r"\1'*'\2", raw, flags=re.MULTILINE) + result = yaml.safe_load(preprocessed) + if not isinstance(result, dict): + return {} + return result diff --git a/src/vstack/frontmatter/serializer.py b/src/vstack/frontmatter/serializer.py index a4ccb03..c683147 100644 --- a/src/vstack/frontmatter/serializer.py +++ b/src/vstack/frontmatter/serializer.py @@ -15,6 +15,18 @@ from vstack.frontmatter.schema import FieldSpec, FrontmatterSchema +# Characters that open a YAML alias (*), anchor (&), or tag (!) when they appear +# as the first character of a plain scalar. List items starting with these must +# be single-quoted so that PyYAML and VS Code parse them correctly. +_YAML_SPECIAL_LEADING = frozenset("*&!") + + +def _quote_list_item(item: str) -> str: + """Return *item* single-quoted when it starts with a YAML-special character.""" + if item and item[0] in _YAML_SPECIAL_LEADING: + return "'" + item.replace("'", "''") + "'" + return item + class FrontmatterSerializer: """Frontmatter serializer — converts metadata dict to YAML. @@ -90,7 +102,7 @@ def _serialize_object_field_pair( if spec.type == "list": if isinstance(value, list) and value: lines = [f"{spec.name}:"] - lines.extend(f" - {item_v}" for item_v in value) + lines.extend(f" - {_quote_list_item(str(item_v))}" for item_v in value) return lines return [] if self._should_emit_multiline(value, preserve_multiline): @@ -160,7 +172,7 @@ def _append_field_by_type( if isinstance(value, list) and value: lines.append(f"{spec.name}:") for item in value: - lines.append(f" - {item}") + lines.append(f" - {_quote_list_item(str(item))}") return if spec.type == "object-list": if isinstance(value, list) and value: diff --git a/tests/vstack/artifacts/test_generator.py b/tests/vstack/artifacts/test_generator.py index ae57576..c592346 100644 --- a/tests/vstack/artifacts/test_generator.py +++ b/tests/vstack/artifacts/test_generator.py @@ -196,7 +196,7 @@ def test_agent_verify_input_uses_schema_for_bool_field(self, tmp_path: Path) -> tmpl_dir.mkdir(parents=True) (tmpl_dir / "template.md").write_text("# writer\nbody\n", encoding="utf-8") (tmpl_dir / "config.yaml").write_text( - "name: writer\ndescription: A writer agent\nuser-invocable: yes\n", + "name: writer\ndescription: A writer agent\nuser-invocable: maybe\n", encoding="utf-8", ) gen = GenericArtifactGenerator(AGENT_TYPE, tmp_path / "templates") diff --git a/tests/vstack/frontmatter/test_parser.py b/tests/vstack/frontmatter/test_parser.py index 1db77ba..8dd1d66 100644 --- a/tests/vstack/frontmatter/test_parser.py +++ b/tests/vstack/frontmatter/test_parser.py @@ -61,11 +61,17 @@ def test_parse_no_frontmatter(self) -> None: assert result.metadata == {} assert result.content == "body-only" + def test_parse_yaml_empty_input_returns_empty_dict(self) -> None: + """Empty input and non-mapping YAML values return an empty dict.""" + assert FrontmatterParser.parse_yaml("") == {} + assert FrontmatterParser.parse_yaml("just a scalar") == {} + assert FrontmatterParser.parse_yaml("- a\n- b\n") == {} + def test_parse_yaml_raw_block(self) -> None: - """Test that parse yaml raw block.""" + """Raw block value is parsed as a nested dict by PyYAML.""" meta = FrontmatterParser.parse_yaml("mcp-servers:\n srv:\n command: cmd\n") - assert isinstance(meta["mcp-servers"], str) - assert "srv:" in meta["mcp-servers"] + assert isinstance(meta["mcp-servers"], dict) + assert meta["mcp-servers"]["srv"]["command"] == "cmd" def test_parse_yaml_ignores_comments_and_handles_object_list_continuation(self) -> None: """Test that parse yaml ignores comments and handles object list continuation.""" @@ -75,10 +81,10 @@ def test_parse_yaml_ignores_comments_and_handles_object_list_continuation(self) assert meta["handoffs"][0]["prompt"] == "hi" def test_parse_yaml_raw_block_closed_by_next_key(self) -> None: - """Test that parse yaml raw block closed by next key.""" + """Nested mapping value and subsequent sibling key are both parsed correctly.""" raw = "mcp-servers:\n srv:\n type: local\nname: x\n" meta = FrontmatterParser.parse_yaml(raw) - assert "type: local" in meta["mcp-servers"] + assert meta["mcp-servers"]["srv"]["type"] == "local" assert meta["name"] == "x" def test_parse_yaml_block_scalar_closed_by_next_key(self) -> None: @@ -110,24 +116,22 @@ def test_parse_yaml_object_list_block_scalar_value(self) -> None: assert isinstance(meta["handoffs"], list) assert "Line one" in meta["handoffs"][0]["prompt"] assert "Line two" in meta["handoffs"][0]["prompt"] - assert meta["handoffs"][0]["send"] == "false" + assert meta["handoffs"][0]["send"] is False - def test_parse_yaml_object_list_coerces_from_scalar(self) -> None: - """Test that parse yaml object list coerces from scalar.""" - raw = "handoffs: value\n - label: A\n" + def test_parse_yaml_wildcard_list_item(self) -> None: + """Bare ``*`` list items (VS Code wildcard) are pre-processed so PyYAML parses them as strings.""" + raw = "agents:\n - '*'\n - architect\n" meta = FrontmatterParser.parse_yaml(raw) - assert isinstance(meta["handoffs"], list) - assert meta["handoffs"][0]["label"] == "A" + assert meta["agents"] == ["*", "architect"] - def test_parse_yaml_string_list_coerces_from_scalar(self) -> None: - """Test that parse yaml string list coerces from scalar.""" - raw = "tools: value\n - read\n" + def test_parse_yaml_unquoted_wildcard_list_item(self) -> None: + """Unquoted ``- *`` in existing generated files is pre-processed before PyYAML.""" + raw = "agents:\n - *\n - architect\n" meta = FrontmatterParser.parse_yaml(raw) - assert isinstance(meta["tools"], list) - assert meta["tools"] == ["read"] + assert meta["agents"] == ["*", "architect"] def test_parse_yaml_object_list_nested_block_dict(self) -> None: - """Nested dict block inside an object-list item is accumulated and stored as a string.""" + """Nested dict block inside an object-list item is parsed as a dict by PyYAML.""" raw = ( "stages:\n" " - role: architect\n" @@ -141,13 +145,13 @@ def test_parse_yaml_object_list_nested_block_dict(self) -> None: stage = meta["stages"][0] assert stage["role"] == "architect" assert stage["gate"] == "required" - # handoffs is stored as a raw string stripped of 6-space indent - assert isinstance(stage["handoffs"], str) - assert "prompt: Architecture done." in stage["handoffs"] - assert "agent: designer" in stage["handoffs"] + # PyYAML parses the nested mapping directly as a dict + assert isinstance(stage["handoffs"], dict) + assert stage["handoffs"]["prompt"] == "Architecture done." + assert stage["handoffs"]["agent"] == "designer" def test_parse_yaml_object_list_nested_block_with_block_scalar(self) -> None: - """Nested block inside an object-list item handles block scalar prompts.""" + """Nested block inside an object-list item: folded scalar prompt is parsed directly.""" raw = ( "stages:\n" " - role: architect\n" @@ -160,19 +164,16 @@ def test_parse_yaml_object_list_nested_block_with_block_scalar(self) -> None: ) meta = FrontmatterParser.parse_yaml(raw) stage = meta["stages"][0] - assert isinstance(stage["handoffs"], str) - # Re-parsing the stored raw string should yield the folded prompt - from vstack.frontmatter import FrontmatterParser as FP - - reparsed = FP.parse_yaml(stage["handoffs"]) - assert "Line one" in reparsed["prompt"] - assert "Line two" in reparsed["prompt"] - # The key after handoffs block is parsed correctly + # PyYAML parses the nested mapping directly as a dict with the folded scalar resolved + assert isinstance(stage["handoffs"], dict) + assert "Line one" in stage["handoffs"]["prompt"] + assert "Line two" in stage["handoffs"]["prompt"] + # The sibling key is parsed correctly assert stage["other"] == "value" - def test_parse_yaml_object_list_empty_nested_block_is_empty_string(self) -> None: - """An empty-value nested key in an object-list item stores an empty string.""" + def test_parse_yaml_object_list_empty_nested_block_is_none(self) -> None: + """An empty-value nested key in an object-list item is None (PyYAML null).""" raw = "stages:\n - role: release\n gate: required\n handoffs:\n" meta = FrontmatterParser.parse_yaml(raw) stage = meta["stages"][0] - assert stage["handoffs"] == "" + assert stage["handoffs"] is None From 444bf0adcd14d1d5b5936ff5b7469c5e97bd82ee Mon Sep 17 00:00:00 2001 From: Erik Schaareman Date: Sat, 9 May 2026 02:23:32 +0200 Subject: [PATCH 07/25] chore(install): regenerate artifacts with quoted wildcard agents Re-run 'vstack install' after serializer fix: all six role agent files now emit '- ''*''' instead of '- *' in the agents: list, which is valid YAML and safe for downstream yaml.safe_load parsing. --- .github/agents/architect.agent.md | 4 +- .github/agents/designer.agent.md | 4 +- .github/agents/engineer.agent.md | 4 +- .github/agents/product.agent.md | 4 +- .github/agents/release.agent.md | 10 +- .github/agents/tester.agent.md | 4 +- .github/instructions/git.instructions.md | 2 +- .github/instructions/helm.instructions.md | 2 +- .github/instructions/java.instructions.md | 2 +- .github/instructions/k8s.instructions.md | 2 +- .github/instructions/markdown.instructions.md | 2 +- .github/instructions/python.instructions.md | 2 +- .github/instructions/rancher.instructions.md | 2 +- .github/instructions/security.instructions.md | 2 +- .../instructions/terraform.instructions.md | 2 +- .../instructions/terragrunt.instructions.md | 2 +- .github/instructions/testing.instructions.md | 2 +- .../instructions/typescript.instructions.md | 2 +- .github/prompts/api-design-review.prompt.md | 2 +- .github/prompts/architecture-risk.prompt.md | 2 +- .github/prompts/code-review.prompt.md | 2 +- .github/prompts/dependency-audit.prompt.md | 2 +- .github/prompts/incident-timeline.prompt.md | 2 +- .github/prompts/migration-safety.prompt.md | 2 +- .github/prompts/release-readiness.prompt.md | 2 +- .github/skills/adr/SKILL.md | 5 +- .github/skills/analyse/SKILL.md | 5 +- .github/skills/architecture/SKILL.md | 5 +- .github/skills/aws-cli/SKILL.md | 5 +- .github/skills/cicd/SKILL.md | 5 +- .github/skills/cloudformation/SKILL.md | 5 +- .github/skills/code-review/SKILL.md | 5 +- .github/skills/codeql/SKILL.md | 5 +- .github/skills/concise/SKILL.md | 5 +- .github/skills/consult/SKILL.md | 5 +- .github/skills/container/SKILL.md | 5 +- .github/skills/conventional-commit/SKILL.md | 5 +- .github/skills/debug/SKILL.md | 5 +- .github/skills/dependabot/SKILL.md | 5 +- .github/skills/dependency/SKILL.md | 5 +- .github/skills/design/SKILL.md | 5 +- .github/skills/docs/SKILL.md | 5 +- .github/skills/explore/SKILL.md | 5 +- .github/skills/gdpr/SKILL.md | 5 +- .github/skills/gh-issues/SKILL.md | 5 +- .github/skills/gh-release/SKILL.md | 5 +- .github/skills/guardrails/SKILL.md | 5 +- .github/skills/helm/SKILL.md | 5 +- .github/skills/incident/SKILL.md | 5 +- .github/skills/inspect/SKILL.md | 5 +- .github/skills/k8s/SKILL.md | 5 +- .github/skills/migrate/SKILL.md | 5 +- .github/skills/onboard/SKILL.md | 5 +- .github/skills/openapi/SKILL.md | 5 +- .github/skills/performance/SKILL.md | 5 +- .github/skills/postmortem/SKILL.md | 5 +- .github/skills/pr/SKILL.md | 5 +- .github/skills/rancher/SKILL.md | 5 +- .github/skills/rca/SKILL.md | 5 +- .github/skills/refactor/SKILL.md | 5 +- .github/skills/release-notes/SKILL.md | 5 +- .github/skills/requirements/SKILL.md | 5 +- .github/skills/secret-scan/SKILL.md | 5 +- .github/skills/security/SKILL.md | 5 +- .github/skills/terraform/SKILL.md | 5 +- .github/skills/terragrunt/SKILL.md | 5 +- .github/skills/threat-model/SKILL.md | 5 +- .github/skills/verify/SKILL.md | 5 +- .github/skills/vision/SKILL.md | 5 +- .vstack/vstack.json | 142 +++++++++--------- 70 files changed, 190 insertions(+), 240 deletions(-) diff --git a/.github/agents/architect.agent.md b/.github/agents/architect.agent.md index 0f66b1e..e705c96 100644 --- a/.github/agents/architect.agent.md +++ b/.github/agents/architect.agent.md @@ -15,7 +15,7 @@ tools: - todo - agent agents: - - * + - '*' model: - Claude Sonnet 4.6 (copilot) - GPT-5.3-Codex (copilot) @@ -181,4 +181,4 @@ that requires changes to upstream artifacts, flag it and trigger a reverse hando - `@#gdpr` — privacy by design and data processing architecture review - + diff --git a/.github/agents/designer.agent.md b/.github/agents/designer.agent.md index 9e8a244..adec29b 100644 --- a/.github/agents/designer.agent.md +++ b/.github/agents/designer.agent.md @@ -14,7 +14,7 @@ tools: - todo - agent agents: - - * + - '*' model: - Claude Sonnet 4.6 (copilot) - GPT-5.3-Codex (copilot) @@ -194,4 +194,4 @@ that requires changes to upstream artifacts, flag it and trigger a reverse hando - `@#openapi` — OpenAPI 3.1 spec writing and review - + diff --git a/.github/agents/engineer.agent.md b/.github/agents/engineer.agent.md index 627a80e..b89a799 100644 --- a/.github/agents/engineer.agent.md +++ b/.github/agents/engineer.agent.md @@ -15,7 +15,7 @@ tools: - todo - agent agents: - - * + - '*' model: - GPT-5.3-Codex (copilot) - Claude Sonnet 4.6 (copilot) @@ -204,4 +204,4 @@ that requires changes to upstream artifacts, flag it and trigger a reverse hando - `@#rancher` — Rancher and Fleet multi-cluster operations and governance - + diff --git a/.github/agents/product.agent.md b/.github/agents/product.agent.md index 443f89e..9587f6c 100644 --- a/.github/agents/product.agent.md +++ b/.github/agents/product.agent.md @@ -14,7 +14,7 @@ tools: - todo - agent agents: - - * + - '*' model: - Claude Sonnet 4.6 (copilot) - GPT-5.3-Codex (copilot) @@ -165,4 +165,4 @@ that requires changes to upstream artifacts, flag it and trigger a reverse hando - `@#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 edf3a36..8911309 100644 --- a/.github/agents/release.agent.md +++ b/.github/agents/release.agent.md @@ -15,7 +15,7 @@ tools: - todo - agent agents: - - * + - '*' model: - Claude Sonnet 4.6 (copilot) - GPT-5.3-Codex (copilot) @@ -123,13 +123,7 @@ and wait for explicit user routing decisions. | -------------------- | ------------------------------------------ | | `docs/releases/*.md` | includes release notes and sign-off record | -### baseline docs you maintain -Keep these files current. Update them whenever the relevant scope, design, or implementation changes — do not let them go stale. - -| Artifact | Notes | -| -------------------- | ------------------------------------------ | -| `docs/releases/*.md` | includes release notes and sign-off record | 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. @@ -154,4 +148,4 @@ that requires changes to upstream artifacts, flag it and trigger a reverse hando - `@#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 ac91351..baae432 100644 --- a/.github/agents/tester.agent.md +++ b/.github/agents/tester.agent.md @@ -15,7 +15,7 @@ tools: - todo - agent agents: - - * + - '*' model: - Claude Sonnet 4.6 (copilot) - GPT-5.3-Codex (copilot) @@ -181,4 +181,4 @@ that requires changes to upstream artifacts, flag it and trigger a reverse hando - `@#rancher` — Rancher/Fleet configuration and multi-cluster governance review - + diff --git a/.github/instructions/git.instructions.md b/.github/instructions/git.instructions.md index 1dd020c..ee0ecde 100644 --- a/.github/instructions/git.instructions.md +++ b/.github/instructions/git.instructions.md @@ -41,4 +41,4 @@ Use these Git and release hygiene conventions in this project. 1. Prefer local verification before pushing release-impacting changes. - + diff --git a/.github/instructions/helm.instructions.md b/.github/instructions/helm.instructions.md index 0548b18..1b37194 100644 --- a/.github/instructions/helm.instructions.md +++ b/.github/instructions/helm.instructions.md @@ -45,4 +45,4 @@ Use these Helm conventions in this project. - [Helm chart best practices](https://helm.sh/docs/chart_best_practices/) - + diff --git a/.github/instructions/java.instructions.md b/.github/instructions/java.instructions.md index f247582..98a498a 100644 --- a/.github/instructions/java.instructions.md +++ b/.github/instructions/java.instructions.md @@ -56,4 +56,4 @@ Use these Java conventions in this project. 1. Do not suppress static analysis warnings without a documented, task-specific reason. - + diff --git a/.github/instructions/k8s.instructions.md b/.github/instructions/k8s.instructions.md index 0d95b18..5bcc17e 100644 --- a/.github/instructions/k8s.instructions.md +++ b/.github/instructions/k8s.instructions.md @@ -51,4 +51,4 @@ Use these Kubernetes conventions in this project. - [Kubernetes API reference](https://kubernetes.io/docs/reference/kubernetes-api/) - + diff --git a/.github/instructions/markdown.instructions.md b/.github/instructions/markdown.instructions.md index da913f7..64b29d5 100644 --- a/.github/instructions/markdown.instructions.md +++ b/.github/instructions/markdown.instructions.md @@ -51,4 +51,4 @@ Use these Markdown conventions in this project. 1. Keep examples accurate and runnable — a broken example is worse than no example. - + diff --git a/.github/instructions/python.instructions.md b/.github/instructions/python.instructions.md index 6df4436..a9a809c 100644 --- a/.github/instructions/python.instructions.md +++ b/.github/instructions/python.instructions.md @@ -42,4 +42,4 @@ Use these Python conventions in this project. 1. Do not silence lint/type errors unless there is a documented, task-specific reason. - + diff --git a/.github/instructions/rancher.instructions.md b/.github/instructions/rancher.instructions.md index 48c34e1..3f52dd8 100644 --- a/.github/instructions/rancher.instructions.md +++ b/.github/instructions/rancher.instructions.md @@ -44,4 +44,4 @@ Use these Rancher conventions in this project. - [Fleet docs](https://fleet.rancher.io/) - + diff --git a/.github/instructions/security.instructions.md b/.github/instructions/security.instructions.md index eb13422..dd02e2c 100644 --- a/.github/instructions/security.instructions.md +++ b/.github/instructions/security.instructions.md @@ -42,4 +42,4 @@ Apply these security policies in this project. 1. Isolate privileged logic; keep it minimal, auditable, and separate from business logic. - + diff --git a/.github/instructions/terraform.instructions.md b/.github/instructions/terraform.instructions.md index 84d52f2..da8fe19 100644 --- a/.github/instructions/terraform.instructions.md +++ b/.github/instructions/terraform.instructions.md @@ -60,4 +60,4 @@ Use these Terraform conventions in this project. - [tfsec](https://aquasecurity.github.io/tfsec/) · [checkov](https://www.checkov.io/) - + diff --git a/.github/instructions/terragrunt.instructions.md b/.github/instructions/terragrunt.instructions.md index d46e25b..9770cb6 100644 --- a/.github/instructions/terragrunt.instructions.md +++ b/.github/instructions/terragrunt.instructions.md @@ -57,4 +57,4 @@ Use these Terragrunt conventions in this project. - [Terragrunt CLI reference](https://terragrunt.gruntwork.io/docs/reference/cli-options/) - + diff --git a/.github/instructions/testing.instructions.md b/.github/instructions/testing.instructions.md index bef5ac4..e673737 100644 --- a/.github/instructions/testing.instructions.md +++ b/.github/instructions/testing.instructions.md @@ -43,4 +43,4 @@ Use these testing conventions in this project. 1. Treat flaky tests as bugs; do not merge code with known test reliability issues. - + diff --git a/.github/instructions/typescript.instructions.md b/.github/instructions/typescript.instructions.md index 3844641..adb935d 100644 --- a/.github/instructions/typescript.instructions.md +++ b/.github/instructions/typescript.instructions.md @@ -49,4 +49,4 @@ Use these TypeScript conventions in this project. 1. Do not suppress lint or type errors with inline disable comments unless there is a documented, task-specific reason. - + diff --git a/.github/prompts/api-design-review.prompt.md b/.github/prompts/api-design-review.prompt.md index 0f7babc..70e505c 100644 --- a/.github/prompts/api-design-review.prompt.md +++ b/.github/prompts/api-design-review.prompt.md @@ -55,4 +55,4 @@ List fields or objects that are missing required constraints, descriptions, or e - top priority fix in one sentence - + diff --git a/.github/prompts/architecture-risk.prompt.md b/.github/prompts/architecture-risk.prompt.md index 843f659..9d9f919 100644 --- a/.github/prompts/architecture-risk.prompt.md +++ b/.github/prompts/architecture-risk.prompt.md @@ -52,4 +52,4 @@ List security-specific risks not covered above: auth boundaries, sensitive data - one-sentence rationale - + diff --git a/.github/prompts/code-review.prompt.md b/.github/prompts/code-review.prompt.md index 747ec5b..4f6287e 100644 --- a/.github/prompts/code-review.prompt.md +++ b/.github/prompts/code-review.prompt.md @@ -51,4 +51,4 @@ End with: - Biggest remaining risk: one sentence - + diff --git a/.github/prompts/dependency-audit.prompt.md b/.github/prompts/dependency-audit.prompt.md index 482dda3..d8744b5 100644 --- a/.github/prompts/dependency-audit.prompt.md +++ b/.github/prompts/dependency-audit.prompt.md @@ -60,4 +60,4 @@ List packages with unusual provenance concerns: abandoned maintainers, single-ma Ordered list of actions by priority (critical first). - + diff --git a/.github/prompts/incident-timeline.prompt.md b/.github/prompts/incident-timeline.prompt.md index be81fb0..ebe3980 100644 --- a/.github/prompts/incident-timeline.prompt.md +++ b/.github/prompts/incident-timeline.prompt.md @@ -57,4 +57,4 @@ For each action: List the minimum controls needed to reduce repeat probability. - + diff --git a/.github/prompts/migration-safety.prompt.md b/.github/prompts/migration-safety.prompt.md index 9b41a5f..e602ce8 100644 --- a/.github/prompts/migration-safety.prompt.md +++ b/.github/prompts/migration-safety.prompt.md @@ -52,4 +52,4 @@ List missing migration tests (forward, backward, data invariants, load-sensitive - biggest remaining risk in one sentence - + diff --git a/.github/prompts/release-readiness.prompt.md b/.github/prompts/release-readiness.prompt.md index f8600d0..8552f4f 100644 --- a/.github/prompts/release-readiness.prompt.md +++ b/.github/prompts/release-readiness.prompt.md @@ -46,4 +46,4 @@ For each expected artifact that is missing, flag it explicitly as: MISSING — [ One clear next step for the team. - + diff --git a/.github/skills/adr/SKILL.md b/.github/skills/adr/SKILL.md index 938ceec..8a08e20 100644 --- a/.github/skills/adr/SKILL.md +++ b/.github/skills/adr/SKILL.md @@ -4,8 +4,7 @@ description: 'Architecture Decision Record writing. Documents a significant arch license: 'MIT' compatibility: 'Requires a skills-compatible agent with repository file access and terminal command execution when needed.' metadata: - owner: vstack - maturity: stable +{'owner': 'vstack', 'maturity': 'stable'} argument-hint: '[decision to record]' user-invocable: true disable-model-invocation: false @@ -179,4 +178,4 @@ is a kebab-case title. After writing, state the file path and summary so the architect or product role can review. - + diff --git a/.github/skills/analyse/SKILL.md b/.github/skills/analyse/SKILL.md index 5b999da..e7c422c 100644 --- a/.github/skills/analyse/SKILL.md +++ b/.github/skills/analyse/SKILL.md @@ -4,8 +4,7 @@ description: 'Cross-cutting technical analysis. Investigates impact, tradeoffs, license: 'MIT' compatibility: 'Requires a skills-compatible agent with repository file access and terminal command execution when needed.' metadata: - owner: vstack - maturity: stable +{'owner': 'vstack', 'maturity': 'stable'} argument-hint: '[topic, change, or question to analyse]' user-invocable: true disable-model-invocation: false @@ -213,4 +212,4 @@ State conclusions with confidence level: ``` - + diff --git a/.github/skills/architecture/SKILL.md b/.github/skills/architecture/SKILL.md index fb51b24..10dab85 100644 --- a/.github/skills/architecture/SKILL.md +++ b/.github/skills/architecture/SKILL.md @@ -4,8 +4,7 @@ description: 'Engineering-lead plan review. Lock in the execution plan — servi license: 'MIT' compatibility: 'Requires a skills-compatible agent with repository file access and terminal command execution when needed.' metadata: - owner: vstack - maturity: stable +{'owner': 'vstack', 'maturity': 'stable'} argument-hint: '[plan or system to review]' user-invocable: true disable-model-invocation: false @@ -277,4 +276,4 @@ For each significant structural decision made during this review (technology cho - Update `docs/architecture/overview.md` to reflect the final decisions. - + diff --git a/.github/skills/aws-cli/SKILL.md b/.github/skills/aws-cli/SKILL.md index 674c4d5..8791d6f 100644 --- a/.github/skills/aws-cli/SKILL.md +++ b/.github/skills/aws-cli/SKILL.md @@ -4,8 +4,7 @@ description: 'AWS CLI command reference and workflow patterns for backend engine license: 'MIT' compatibility: 'Requires AWS CLI v2 installed and configured (aws configure or environment variables). IAM permissions vary by operation — principle of least privilege applies.' metadata: - owner: vstack - maturity: stable +{'owner': 'vstack', 'maturity': 'stable'} argument-hint: '[service: iam | ec2 | s3 | rds | ecs | lambda | cloudwatch | ssm | secrets]' user-invocable: true disable-model-invocation: false @@ -373,4 +372,4 @@ aws ce get-cost-and-usage \ - [AWS CLI named profiles](https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-profiles.html) - + diff --git a/.github/skills/cicd/SKILL.md b/.github/skills/cicd/SKILL.md index 5fa77e8..c9e38a5 100644 --- a/.github/skills/cicd/SKILL.md +++ b/.github/skills/cicd/SKILL.md @@ -4,8 +4,7 @@ description: 'Write GitHub Actions CI/CD workflow configuration. Covers build, t license: 'MIT' compatibility: 'Requires a skills-compatible agent with repository file access and terminal command execution when needed.' metadata: - owner: vstack - maturity: stable +{'owner': 'vstack', 'maturity': 'stable'} argument-hint: '[service or workflow to configure]' user-invocable: true disable-model-invocation: false @@ -220,4 +219,4 @@ Configure these in GitHub → Settings → Branches. - [GitHub-hosted runners](https://docs.github.com/en/actions/using-github-hosted-runners/using-github-hosted-runners/about-github-hosted-runners) - + diff --git a/.github/skills/cloudformation/SKILL.md b/.github/skills/cloudformation/SKILL.md index 1ca4601..3ce8f8e 100644 --- a/.github/skills/cloudformation/SKILL.md +++ b/.github/skills/cloudformation/SKILL.md @@ -4,8 +4,7 @@ description: 'Write, review, and refactor AWS CloudFormation templates. Covers t license: 'MIT' compatibility: 'Requires a skills-compatible agent with repository file access. Requires AWS CLI with appropriate IAM permissions for deploy and drift operations.' metadata: - owner: vstack - maturity: stable +{'owner': 'vstack', 'maturity': 'stable'} argument-hint: '[resource type or stack name, e.g. VPC | RDS | ECS service | Lambda function]' user-invocable: true disable-model-invocation: false @@ -343,4 +342,4 @@ AppSecurityGroup: - [AWS SAM documentation](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/) - + diff --git a/.github/skills/code-review/SKILL.md b/.github/skills/code-review/SKILL.md index aab6705..41f83c9 100644 --- a/.github/skills/code-review/SKILL.md +++ b/.github/skills/code-review/SKILL.md @@ -4,8 +4,7 @@ description: 'Pre-landing code review. Finds bugs that pass CI but break in prod license: 'MIT' compatibility: 'Requires a skills-compatible agent with repository file access and terminal command execution when needed.' metadata: - owner: vstack - maturity: stable +{'owner': 'vstack', 'maturity': 'stable'} argument-hint: '[files, PR, or change to review]' user-invocable: true disable-model-invocation: false @@ -217,4 +216,4 @@ Confidence: [HIGH/MEDIUM/LOW — explain if not HIGH] ``` - + diff --git a/.github/skills/codeql/SKILL.md b/.github/skills/codeql/SKILL.md index 5fa8372..ac86f7f 100644 --- a/.github/skills/codeql/SKILL.md +++ b/.github/skills/codeql/SKILL.md @@ -4,8 +4,7 @@ description: 'Set up and configure CodeQL code scanning via GitHub Actions or th license: 'MIT' compatibility: 'Requires a skills-compatible agent with repository file access and terminal command execution. GitHub Advanced Security or public repository required for alert upload.' metadata: - owner: vstack - maturity: stable +{'owner': 'vstack', 'maturity': 'stable'} argument-hint: '[languages and setup type: default or advanced]' user-invocable: true disable-model-invocation: false @@ -250,4 +249,4 @@ GITHUB_TOKEN= codeql github upload-results \ - [Supported languages and frameworks](https://docs.github.com/en/code-security/code-scanning/introduction-to-code-scanning/codeql-code-scanning-for-compiled-languages) - + diff --git a/.github/skills/concise/SKILL.md b/.github/skills/concise/SKILL.md index 6968d01..25c8a8e 100644 --- a/.github/skills/concise/SKILL.md +++ b/.github/skills/concise/SKILL.md @@ -4,8 +4,7 @@ description: 'Runtime response-style controller for concise communication. Switc license: 'MIT' compatibility: 'Requires a skills-compatible agent with session memory and repository context.' metadata: - owner: vstack - maturity: stable +{'owner': 'vstack', 'maturity': 'stable'} argument-hint: '[normal|compact|ultra|status|on|off]' user-invocable: true disable-model-invocation: false @@ -146,4 +145,4 @@ Current mode unchanged: - [ ] User confirmation/status returned in deterministic format - + diff --git a/.github/skills/consult/SKILL.md b/.github/skills/consult/SKILL.md index ea8e1d0..bc2a4d4 100644 --- a/.github/skills/consult/SKILL.md +++ b/.github/skills/consult/SKILL.md @@ -4,8 +4,7 @@ description: 'DX triage and focused review. First classifies whether the request license: 'MIT' compatibility: 'Requires a skills-compatible agent with repository file access and terminal command execution when needed.' metadata: - owner: vstack - maturity: stable +{'owner': 'vstack', 'maturity': 'stable'} argument-hint: '[API, tool, or workflow to consult]' user-invocable: true disable-model-invocation: false @@ -217,4 +216,4 @@ reason: [one sentence] ``` - + diff --git a/.github/skills/container/SKILL.md b/.github/skills/container/SKILL.md index 613b8f6..bdc6d49 100644 --- a/.github/skills/container/SKILL.md +++ b/.github/skills/container/SKILL.md @@ -4,8 +4,7 @@ description: 'Write and review Dockerfile, docker-compose, and container configu license: 'MIT' compatibility: 'Requires a skills-compatible agent with repository file access and terminal command execution when needed.' metadata: - owner: vstack - maturity: stable +{'owner': 'vstack', 'maturity': 'stable'} argument-hint: '[service to containerise]' user-invocable: true disable-model-invocation: false @@ -152,4 +151,4 @@ For production-like local testing, write a separate `docker-compose.prod.yml` wi - [Docker official images](https://hub.docker.com/search?image_filter=official) - + diff --git a/.github/skills/conventional-commit/SKILL.md b/.github/skills/conventional-commit/SKILL.md index 87db593..2943ab6 100644 --- a/.github/skills/conventional-commit/SKILL.md +++ b/.github/skills/conventional-commit/SKILL.md @@ -4,8 +4,7 @@ description: 'Prepare high-quality Conventional Commit messages from current sta license: 'MIT' compatibility: 'Requires a skills-compatible agent with repository file access and terminal command execution.' metadata: - owner: vstack - maturity: stable +{'owner': 'vstack', 'maturity': 'stable'} argument-hint: '[changes to commit and desired release intent]' user-invocable: true disable-model-invocation: false @@ -154,4 +153,4 @@ Remaining changes: If commit is blocked, report exact reason and proposed fix. - + diff --git a/.github/skills/debug/SKILL.md b/.github/skills/debug/SKILL.md index 9cce4a9..462e380 100644 --- a/.github/skills/debug/SKILL.md +++ b/.github/skills/debug/SKILL.md @@ -4,8 +4,7 @@ description: 'Systematic root-cause debugging for backend services, APIs, and li license: 'MIT' compatibility: 'Requires a skills-compatible agent with repository file access and terminal command execution when needed.' metadata: - owner: vstack - maturity: stable +{'owner': 'vstack', 'maturity': 'stable'} argument-hint: '[issue or error to debug]' user-invocable: true disable-model-invocation: false @@ -257,4 +256,4 @@ Prevention: [any follow-up items] ``` - + diff --git a/.github/skills/dependabot/SKILL.md b/.github/skills/dependabot/SKILL.md index 0084c1a..4db96bd 100644 --- a/.github/skills/dependabot/SKILL.md +++ b/.github/skills/dependabot/SKILL.md @@ -4,8 +4,7 @@ description: 'Create or optimize a Dependabot configuration file (.github/depend license: 'MIT' compatibility: 'Requires a skills-compatible agent with repository file access. Dependabot requires GitHub repository access (public or private with GitHub Advanced Security for private).' metadata: - owner: vstack - maturity: stable +{'owner': 'vstack', 'maturity': 'stable'} argument-hint: '[repository type: library | service | monorepo, and ecosystems to cover]' user-invocable: true disable-model-invocation: false @@ -319,4 +318,4 @@ updates: - [Dependabot security updates](https://docs.github.com/en/code-security/dependabot/dependabot-security-updates/about-dependabot-security-updates) - + diff --git a/.github/skills/dependency/SKILL.md b/.github/skills/dependency/SKILL.md index 26941e9..33a9478 100644 --- a/.github/skills/dependency/SKILL.md +++ b/.github/skills/dependency/SKILL.md @@ -4,8 +4,7 @@ description: 'Dependency health audit. Covers vulnerability scanning, outdated p license: 'MIT' compatibility: 'Requires a skills-compatible agent with repository file access and terminal command execution when needed.' metadata: - owner: vstack - maturity: stable +{'owner': 'vstack', 'maturity': 'stable'} argument-hint: '[project or package manifest to audit]' user-invocable: true disable-model-invocation: false @@ -317,4 +316,4 @@ Action items (priority order): - [PyPI / npm / crates.io / Maven Central](https://pypi.org) (replace with the relevant registry) - + diff --git a/.github/skills/design/SKILL.md b/.github/skills/design/SKILL.md index c851cd9..d8a1942 100644 --- a/.github/skills/design/SKILL.md +++ b/.github/skills/design/SKILL.md @@ -4,8 +4,7 @@ description: 'Build a complete API design or service design from scratch. Produc license: 'MIT' compatibility: 'Requires a skills-compatible agent with repository file access and terminal command execution when needed.' metadata: - owner: vstack - maturity: stable +{'owner': 'vstack', 'maturity': 'stable'} argument-hint: '[API or service to design]' user-invocable: true disable-model-invocation: false @@ -242,4 +241,4 @@ Output a complete design document to `docs/design/overview.md` or `openapi.yaml` ``` - + diff --git a/.github/skills/docs/SKILL.md b/.github/skills/docs/SKILL.md index f058b4a..7b1dbd7 100644 --- a/.github/skills/docs/SKILL.md +++ b/.github/skills/docs/SKILL.md @@ -4,8 +4,7 @@ description: 'Post-release documentation alignment. Updates README, API docs, mi license: 'MIT' compatibility: 'Requires a skills-compatible agent with repository file access and terminal command execution when needed.' metadata: - owner: vstack - maturity: stable +{'owner': 'vstack', 'maturity': 'stable'} argument-hint: '[release or change to document]' user-invocable: true disable-model-invocation: false @@ -150,4 +149,4 @@ Skipped (n/a): ``` - + diff --git a/.github/skills/explore/SKILL.md b/.github/skills/explore/SKILL.md index da7b7f6..a4f93f6 100644 --- a/.github/skills/explore/SKILL.md +++ b/.github/skills/explore/SKILL.md @@ -4,8 +4,7 @@ description: 'Repository and system discovery. Maps the architecture, understand license: 'MIT' compatibility: 'Requires a skills-compatible agent with repository file access and terminal command execution when needed.' metadata: - owner: vstack - maturity: stable +{'owner': 'vstack', 'maturity': 'stable'} argument-hint: '[repository or system to explore]' user-invocable: true disable-model-invocation: false @@ -223,4 +222,4 @@ Stack: [language, framework, runtime versions] ``` - + diff --git a/.github/skills/gdpr/SKILL.md b/.github/skills/gdpr/SKILL.md index 615329c..551408b 100644 --- a/.github/skills/gdpr/SKILL.md +++ b/.github/skills/gdpr/SKILL.md @@ -4,8 +4,7 @@ description: 'GDPR-compliant engineering practices for APIs, data models, authen license: 'MIT' compatibility: 'Requires a skills-compatible agent with repository file access.' metadata: - owner: vstack - maturity: stable +{'owner': 'vstack', 'maturity': 'stable'} argument-hint: '[component or feature: data model | API | logging | retention | erasure | infra | PR review]' user-invocable: true disable-model-invocation: false @@ -245,4 +244,4 @@ Use `@example.com` for all test email addresses. - [EDPB guidelines](https://www.edpb.europa.eu/our-work-tools/general-guidance/guidelines-recommendations-best-practices_en) - + diff --git a/.github/skills/gh-issues/SKILL.md b/.github/skills/gh-issues/SKILL.md index 82630ee..1fb4d25 100644 --- a/.github/skills/gh-issues/SKILL.md +++ b/.github/skills/gh-issues/SKILL.md @@ -4,8 +4,7 @@ description: 'Create, update, and manage GitHub issues using the gh CLI. Covers license: 'MIT' compatibility: 'Requires a skills-compatible agent with terminal command execution and GitHub CLI authentication (`gh auth status`).' metadata: - owner: vstack - maturity: stable +{'owner': 'vstack', 'maturity': 'stable'} argument-hint: '[what to create or which issue number to update]' user-invocable: true disable-model-invocation: false @@ -229,4 +228,4 @@ https://github.com///issues/ - [GitHub Issues documentation](https://docs.github.com/en/issues) - + diff --git a/.github/skills/gh-release/SKILL.md b/.github/skills/gh-release/SKILL.md index 8c7fb2f..c48d4d6 100644 --- a/.github/skills/gh-release/SKILL.md +++ b/.github/skills/gh-release/SKILL.md @@ -4,8 +4,7 @@ description: 'Create or update a GitHub Release using the gh CLI from prepared r license: 'MIT' compatibility: 'Requires a skills-compatible agent with repository file access, terminal command execution, and GitHub CLI authentication (`gh auth status`).' metadata: - owner: vstack - maturity: stable +{'owner': 'vstack', 'maturity': 'stable'} argument-hint: '[version/tag and release notes source]' user-invocable: true disable-model-invocation: false @@ -213,4 +212,4 @@ If blocked, report exact blocker and required user action. - [GitHub Releases documentation](https://docs.github.com/en/repositories/releasing-projects-on-github/about-releases) - + diff --git a/.github/skills/guardrails/SKILL.md b/.github/skills/guardrails/SKILL.md index f2ef32e..11e9a58 100644 --- a/.github/skills/guardrails/SKILL.md +++ b/.github/skills/guardrails/SKILL.md @@ -4,8 +4,7 @@ description: 'Activate safety guardrails for the current session. Before any des license: 'MIT' compatibility: 'Requires a skills-compatible agent with repository file access and terminal command execution when needed.' metadata: - owner: vstack - maturity: stable +{'owner': 'vstack', 'maturity': 'stable'} argument-hint: '[task]' user-invocable: true disable-model-invocation: true @@ -71,4 +70,4 @@ Activate careful mode for this session. Two behaviors are now enabled. Explicitly ask to "disable guardrails". - + diff --git a/.github/skills/helm/SKILL.md b/.github/skills/helm/SKILL.md index 7dad35e..f26f8c6 100644 --- a/.github/skills/helm/SKILL.md +++ b/.github/skills/helm/SKILL.md @@ -4,8 +4,7 @@ description: 'Write, review, and operate Helm charts and release lifecycles. Cov license: 'MIT' compatibility: 'Requires a skills-compatible agent with repository file access. Requires Helm CLI and target cluster access for live release operations.' metadata: - owner: vstack - maturity: stable +{'owner': 'vstack', 'maturity': 'stable'} argument-hint: '[chart path, release name, namespace, and scope: chart review | install | upgrade | rollback]' user-invocable: true disable-model-invocation: false @@ -139,4 +138,4 @@ Practices: - [Chart best practices](https://helm.sh/docs/chart_best_practices/) - + diff --git a/.github/skills/incident/SKILL.md b/.github/skills/incident/SKILL.md index 1293932..2d1b5b5 100644 --- a/.github/skills/incident/SKILL.md +++ b/.github/skills/incident/SKILL.md @@ -4,8 +4,7 @@ description: 'Incident analysis and coordination. Guides timeline reconstruction license: 'MIT' compatibility: 'Requires a skills-compatible agent with repository file access and terminal command execution when needed.' metadata: - owner: vstack - maturity: stable +{'owner': 'vstack', 'maturity': 'stable'} argument-hint: '[incident or outage to analyse]' user-invocable: true disable-model-invocation: false @@ -250,4 +249,4 @@ Next: invoke @#rca and @#postmortem to produce written artifacts. ``` - + diff --git a/.github/skills/inspect/SKILL.md b/.github/skills/inspect/SKILL.md index 3e1a676..b725b68 100644 --- a/.github/skills/inspect/SKILL.md +++ b/.github/skills/inspect/SKILL.md @@ -4,8 +4,7 @@ description: 'Read-only verification audit. Runs baseline plus optional extended license: 'MIT' compatibility: 'Requires a skills-compatible agent with repository file access and terminal command execution when needed.' metadata: - owner: vstack - maturity: stable +{'owner': 'vstack', 'maturity': 'stable'} argument-hint: '[component or service to inspect]' user-invocable: true disable-model-invocation: false @@ -154,4 +153,4 @@ Confirm for changed paths: ``` - + diff --git a/.github/skills/k8s/SKILL.md b/.github/skills/k8s/SKILL.md index e678a59..836db23 100644 --- a/.github/skills/k8s/SKILL.md +++ b/.github/skills/k8s/SKILL.md @@ -4,8 +4,7 @@ description: 'Write, review, and troubleshoot Kubernetes manifests and operation license: 'MIT' compatibility: 'Requires a skills-compatible agent with repository file access. Requires kubectl access to a target cluster for live operations.' metadata: - owner: vstack - maturity: stable +{'owner': 'vstack', 'maturity': 'stable'} argument-hint: '[cluster/context, namespace, and scope: manifest review | deploy | rollout debug | hardening]' user-invocable: true disable-model-invocation: false @@ -143,4 +142,4 @@ Common failure classes: - [Kubernetes API reference](https://kubernetes.io/docs/reference/kubernetes-api/) - + diff --git a/.github/skills/migrate/SKILL.md b/.github/skills/migrate/SKILL.md index 85f470c..baf6b37 100644 --- a/.github/skills/migrate/SKILL.md +++ b/.github/skills/migrate/SKILL.md @@ -4,8 +4,7 @@ description: 'Database migration review and authoring. Covers forwards/backwards license: 'MIT' compatibility: 'Requires a skills-compatible agent with repository file access and terminal command execution when needed.' metadata: - owner: vstack - maturity: stable +{'owner': 'vstack', 'maturity': 'stable'} argument-hint: '[migration file or schema change to review]' user-invocable: true disable-model-invocation: false @@ -319,4 +318,4 @@ Pre-deploy checklist: ``` - + diff --git a/.github/skills/onboard/SKILL.md b/.github/skills/onboard/SKILL.md index 295344c..cadc773 100644 --- a/.github/skills/onboard/SKILL.md +++ b/.github/skills/onboard/SKILL.md @@ -4,8 +4,7 @@ description: 'Generate a contributor onboarding guide for a repository. Covers p license: 'MIT' compatibility: 'Requires a skills-compatible agent with repository file access and terminal command execution when needed.' metadata: - owner: vstack - maturity: stable +{'owner': 'vstack', 'maturity': 'stable'} argument-hint: '[repository or service to document]' user-invocable: true disable-model-invocation: false @@ -301,4 +300,4 @@ Gaps remaining (if any): ``` - + diff --git a/.github/skills/openapi/SKILL.md b/.github/skills/openapi/SKILL.md index 6931474..2fbbad7 100644 --- a/.github/skills/openapi/SKILL.md +++ b/.github/skills/openapi/SKILL.md @@ -4,8 +4,7 @@ description: 'Write and review OpenAPI 3.1 specifications. Covers resource namin license: 'MIT' compatibility: 'Requires a skills-compatible agent with repository file access and terminal command execution when needed.' metadata: - owner: vstack - maturity: stable +{'owner': 'vstack', 'maturity': 'stable'} argument-hint: '[API or spec file to write or review]' user-invocable: true disable-model-invocation: false @@ -402,4 +401,4 @@ Summary: [N critical, N warnings, N info] - [Redocly CLI (linting)](https://redocly.com/docs/cli/) - + diff --git a/.github/skills/performance/SKILL.md b/.github/skills/performance/SKILL.md index a9fc9d2..e696ddf 100644 --- a/.github/skills/performance/SKILL.md +++ b/.github/skills/performance/SKILL.md @@ -4,8 +4,7 @@ description: 'Performance profiling and regression detection. Establishes baseli license: 'MIT' compatibility: 'Requires a skills-compatible agent with repository file access and terminal command execution when needed.' metadata: - owner: vstack - maturity: stable +{'owner': 'vstack', 'maturity': 'stable'} argument-hint: '[endpoint or function to profile]' user-invocable: true disable-model-invocation: false @@ -241,4 +240,4 @@ For each bottleneck identified: ``` - + diff --git a/.github/skills/postmortem/SKILL.md b/.github/skills/postmortem/SKILL.md index 789ecec..39ddb48 100644 --- a/.github/skills/postmortem/SKILL.md +++ b/.github/skills/postmortem/SKILL.md @@ -4,8 +4,7 @@ description: 'Blameless post-mortem writing for incidents. Produces a stakeholde license: 'MIT' compatibility: 'Requires a skills-compatible agent with repository file access and terminal command execution when needed.' metadata: - owner: vstack - maturity: stable +{'owner': 'vstack', 'maturity': 'stable'} argument-hint: '[incident to write a post-mortem for]' user-invocable: true disable-model-invocation: false @@ -183,4 +182,4 @@ Status: Draft — ready for team review ``` - + diff --git a/.github/skills/pr/SKILL.md b/.github/skills/pr/SKILL.md index d5b395c..a84beb4 100644 --- a/.github/skills/pr/SKILL.md +++ b/.github/skills/pr/SKILL.md @@ -4,8 +4,7 @@ description: 'Commit, push, and open a pull request from the current branch to m license: 'MIT' compatibility: 'Requires a skills-compatible agent with repository file access and terminal command execution when needed.' metadata: - owner: vstack - maturity: stable +{'owner': 'vstack', 'maturity': 'stable'} argument-hint: '[task]' user-invocable: true disable-model-invocation: false @@ -150,4 +149,4 @@ Next steps depend on the repository CI/CD configuration: ``` - + diff --git a/.github/skills/rancher/SKILL.md b/.github/skills/rancher/SKILL.md index 1b4a001..661d49e 100644 --- a/.github/skills/rancher/SKILL.md +++ b/.github/skills/rancher/SKILL.md @@ -4,8 +4,7 @@ description: 'Operate Kubernetes workloads and governance through Rancher. Cover license: 'MIT' compatibility: 'Requires a skills-compatible agent with repository file access. Requires Rancher UI/API access or Rancher CLI where applicable.' metadata: - owner: vstack - maturity: stable +{'owner': 'vstack', 'maturity': 'stable'} argument-hint: '[rancher server/context, cluster/project, and scope: deploy | governance | fleet | troubleshooting]' user-invocable: true disable-model-invocation: false @@ -112,4 +111,4 @@ Checks: - [Fleet documentation](https://fleet.rancher.io/) - + diff --git a/.github/skills/rca/SKILL.md b/.github/skills/rca/SKILL.md index bfa145f..3faf325 100644 --- a/.github/skills/rca/SKILL.md +++ b/.github/skills/rca/SKILL.md @@ -4,8 +4,7 @@ description: 'Root cause analysis for incidents and bugs. Guides a systematic te license: 'MIT' compatibility: 'Requires a skills-compatible agent with repository file access and terminal command execution when needed.' metadata: - owner: vstack - maturity: stable +{'owner': 'vstack', 'maturity': 'stable'} argument-hint: '[incident or issue to analyse]' user-invocable: true disable-model-invocation: false @@ -206,4 +205,4 @@ Status: Draft — ready for review ``` - + diff --git a/.github/skills/refactor/SKILL.md b/.github/skills/refactor/SKILL.md index 5c40c13..328d628 100644 --- a/.github/skills/refactor/SKILL.md +++ b/.github/skills/refactor/SKILL.md @@ -4,8 +4,7 @@ description: 'Structured refactoring for backend services, APIs, and libraries. license: 'MIT' compatibility: 'Requires a skills-compatible agent with repository file access and terminal command execution when needed.' metadata: - owner: vstack - maturity: stable +{'owner': 'vstack', 'maturity': 'stable'} argument-hint: '[module, file, or area to refactor]' user-invocable: true disable-model-invocation: false @@ -371,4 +370,4 @@ Behavior changed: No ``` - + diff --git a/.github/skills/release-notes/SKILL.md b/.github/skills/release-notes/SKILL.md index 8d4cb92..5be74fb 100644 --- a/.github/skills/release-notes/SKILL.md +++ b/.github/skills/release-notes/SKILL.md @@ -4,8 +4,7 @@ description: 'Prepare release artifacts: verify all docs are present, write rele license: 'MIT' compatibility: 'Requires a skills-compatible agent with repository file access and terminal command execution when needed.' metadata: - owner: vstack - maturity: stable +{'owner': 'vstack', 'maturity': 'stable'} argument-hint: '[version or changes to release]' user-invocable: true disable-model-invocation: false @@ -146,4 +145,4 @@ Prepend a new entry at the top of `CHANGELOG.md`: Keep existing entries intact. - + diff --git a/.github/skills/requirements/SKILL.md b/.github/skills/requirements/SKILL.md index e2ac283..b8add1f 100644 --- a/.github/skills/requirements/SKILL.md +++ b/.github/skills/requirements/SKILL.md @@ -4,8 +4,7 @@ description: 'Collaborative requirements gathering and documentation. Clarifies license: 'MIT' compatibility: 'Requires a skills-compatible agent with repository file access and terminal command execution when needed.' metadata: - owner: vstack - maturity: stable +{'owner': 'vstack', 'maturity': 'stable'} argument-hint: '[feature or system to document]' user-invocable: true disable-model-invocation: false @@ -198,4 +197,4 @@ Write all findings to `docs/product/requirements.md`: After writing, summarize what was decided so the architect role can start. - + diff --git a/.github/skills/secret-scan/SKILL.md b/.github/skills/secret-scan/SKILL.md index 7ba5bd7..8850777 100644 --- a/.github/skills/secret-scan/SKILL.md +++ b/.github/skills/secret-scan/SKILL.md @@ -4,8 +4,7 @@ description: 'Configure and manage GitHub secret scanning and push protection. C license: 'MIT' compatibility: 'Requires repository access and GitHub Advanced Security (private repos) or public repository. Alert management requires gh CLI authentication.' metadata: - owner: vstack - maturity: stable +{'owner': 'vstack', 'maturity': 'stable'} argument-hint: '[scope: enable | configure push-protection | custom-pattern | triage alerts | remediate]' user-invocable: true disable-model-invocation: false @@ -239,4 +238,4 @@ credential formats. - [Supported secret patterns](https://docs.github.com/en/code-security/secret-scanning/introduction/supported-secret-scanning-patterns) - + diff --git a/.github/skills/security/SKILL.md b/.github/skills/security/SKILL.md index b8d50aa..73296d3 100644 --- a/.github/skills/security/SKILL.md +++ b/.github/skills/security/SKILL.md @@ -4,8 +4,7 @@ description: 'OWASP Top 10 + STRIDE security audit for APIs, services, and libra license: 'MIT' compatibility: 'Requires a skills-compatible agent with repository file access and terminal command execution when needed.' metadata: - owner: vstack - maturity: stable +{'owner': 'vstack', 'maturity': 'stable'} argument-hint: '[component or service to audit]' user-invocable: true disable-model-invocation: false @@ -294,4 +293,4 @@ Scope: [full/diff/dependency/config] - [STRIDE threat modeling (Microsoft)](https://learn.microsoft.com/en-us/azure/security/develop/threat-modeling-tool-threats) - + diff --git a/.github/skills/terraform/SKILL.md b/.github/skills/terraform/SKILL.md index 86e1238..b8fe8d3 100644 --- a/.github/skills/terraform/SKILL.md +++ b/.github/skills/terraform/SKILL.md @@ -4,8 +4,7 @@ description: 'Write, review, and refactor Terraform infrastructure-as-code. Cove license: 'MIT' compatibility: 'Requires a skills-compatible agent with repository file access. Requires Terraform CLI installed for plan/apply operations.' metadata: - owner: vstack - maturity: stable +{'owner': 'vstack', 'maturity': 'stable'} argument-hint: '[provider: aws | azure | gcp | generic, and scope: new resource | module | state migration | security review]' user-invocable: true disable-model-invocation: false @@ -334,4 +333,4 @@ Run `terraform plan` after every state operation to verify the outcome. - [tfsec rules](https://aquasecurity.github.io/tfsec/latest/checks/aws/) · [checkov checks](https://www.checkov.io/5.Policy%20Index/terraform.html) - + diff --git a/.github/skills/terragrunt/SKILL.md b/.github/skills/terragrunt/SKILL.md index 7665466..120791f 100644 --- a/.github/skills/terragrunt/SKILL.md +++ b/.github/skills/terragrunt/SKILL.md @@ -4,8 +4,7 @@ description: 'Write, review, and refactor Terragrunt configurations for DRY mult license: 'MIT' compatibility: 'Requires a skills-compatible agent with repository file access. Requires Terraform CLI and Terragrunt installed for plan/apply operations.' metadata: - owner: vstack - maturity: stable +{'owner': 'vstack', 'maturity': 'stable'} argument-hint: '[scope: new layout | dependency graph | state migration | run-all workflow | security review]' user-invocable: true disable-model-invocation: false @@ -306,4 +305,4 @@ Use `--terragrunt-non-interactive` in CI to prevent hanging on prompts. - [Gruntwork module registry](https://www.gruntwork.io/) - + diff --git a/.github/skills/threat-model/SKILL.md b/.github/skills/threat-model/SKILL.md index 205e46f..cdf43c9 100644 --- a/.github/skills/threat-model/SKILL.md +++ b/.github/skills/threat-model/SKILL.md @@ -4,8 +4,7 @@ description: 'Threat modeling for APIs, services, and systems using a practical license: 'MIT' compatibility: 'Requires a skills-compatible agent with repository file access and terminal command execution when needed.' metadata: - owner: vstack - maturity: stable +{'owner': 'vstack', 'maturity': 'stable'} argument-hint: '[system, component, or architecture to threat model]' user-invocable: true disable-model-invocation: false @@ -244,4 +243,4 @@ For each high-priority threat include: - Final report is written to `docs/architecture/threat-model.md`. - + diff --git a/.github/skills/verify/SKILL.md b/.github/skills/verify/SKILL.md index ba57b9f..88f2212 100644 --- a/.github/skills/verify/SKILL.md +++ b/.github/skills/verify/SKILL.md @@ -4,8 +4,7 @@ description: 'Verification fix-loop skill. Routes by mode (quick/standard/exhaus license: 'MIT' compatibility: 'Requires a skills-compatible agent with repository file access and terminal command execution when needed.' metadata: - owner: vstack - maturity: stable +{'owner': 'vstack', 'maturity': 'stable'} argument-hint: '[component or feature to verify]' user-invocable: true disable-model-invocation: false @@ -265,4 +264,4 @@ scope: [path/component/full] ``` - + diff --git a/.github/skills/vision/SKILL.md b/.github/skills/vision/SKILL.md index 49d989a..8e2ae65 100644 --- a/.github/skills/vision/SKILL.md +++ b/.github/skills/vision/SKILL.md @@ -4,8 +4,7 @@ description: 'CEO/founder-mode plan review. Rethink the problem from first princ license: 'MIT' compatibility: 'Requires a skills-compatible agent with repository file access and terminal command execution when needed.' metadata: - owner: vstack - maturity: stable +{'owner': 'vstack', 'maturity': 'stable'} argument-hint: '[plan or idea to review]' user-invocable: true disable-model-invocation: false @@ -205,4 +204,4 @@ For each finding: explain the tradeoff, give an opinionated recommendation, ask Present as: "Overall assessment: [READY/NEEDS REVISION/SCOPE CHANGE] because [1-2 sentence reason]." - + diff --git a/.vstack/vstack.json b/.vstack/vstack.json index 836639f..dd42799 100644 --- a/.vstack/vstack.json +++ b/.vstack/vstack.json @@ -1,316 +1,316 @@ { "manifest_version": 2, "hash_algorithm": "sha256", - "vstack_version": "3.0.0", - "installed_at": "2026-05-08T23:06:53.578784+00:00", + "vstack_version": "0.0.0.post3.dev0+df3fe6e", + "installed_at": "2026-05-09T00:23:22.770927+00:00", "artifacts": { "skills": [ { "name": "adr", "file": "skills/adr/SKILL.md", "version": "20260421003", - "checksum": "c4d99dbcbaac68c11749c7aae802445b4a3f3c57bd2baa24bbfff3481d897657", + "checksum": "5ec10193062263b26df22b03af901b099ccc376e6b97be154e6d3fbc0a264152", "checksum_algorithm": "sha256" }, { "name": "analyse", "file": "skills/analyse/SKILL.md", "version": "20260421004", - "checksum": "09780d00803baf7b9d6170a4fb0a409049721e70b1f3bc0f2426d20b604a1eb3", + "checksum": "64aaf1b0f43af9fc47795b68810373ebacbce947792b385455753b6cde3c6d64", "checksum_algorithm": "sha256" }, { "name": "architecture", "file": "skills/architecture/SKILL.md", "version": "20260421005", - "checksum": "ac0ef4525b0f264305e7d7f2144a9c72dd375973408151713140e7a368ce9389", + "checksum": "a9f2eb0147b829fa8006d3af771035aa3bc1e396297d87ba8a51fe6fac064a12", "checksum_algorithm": "sha256" }, { "name": "aws-cli", "file": "skills/aws-cli/SKILL.md", "version": "20260502033", - "checksum": "2a7702778c4a6d9de85496afdc13e7ab5b68dfc20198a5d48d4ef0134f262500", + "checksum": "782db163e9675417f4207a98fb49c935149bf20ac5bb03a0bf3f582181e64460", "checksum_algorithm": "sha256" }, { "name": "cicd", "file": "skills/cicd/SKILL.md", "version": "20260421006", - "checksum": "1b94088cfda1b653959e0dc7ddf09a3c49e879ee9e11d50877d8bb935b30cda8", + "checksum": "7e9613dfff757c57cbc1caf98d1833a3e480a601a0291abc69843f6ce730ee70", "checksum_algorithm": "sha256" }, { "name": "cloudformation", "file": "skills/cloudformation/SKILL.md", "version": "20260502032", - "checksum": "c4755265f6b6ff0bbd275a4f4e0153d7c08d80cb2711b821f5eacb511cca1f38", + "checksum": "d330ede035e42d98bb062a8ffa8583935101bf2090ffcc8ab716f490f5b14e85", "checksum_algorithm": "sha256" }, { "name": "code-review", "file": "skills/code-review/SKILL.md", "version": "20260421007", - "checksum": "17292327b093a21cd3845d67ea367480c3eedfdaacdb7752e027e05b1ef0031f", + "checksum": "bc514f210d6af855d0a8ed48004d086b9edc48b65d3935d27049c38c04555586", "checksum_algorithm": "sha256" }, { "name": "codeql", "file": "skills/codeql/SKILL.md", "version": "20260502026", - "checksum": "199f3317399ce02ad2f80b8a51e075a060d2b2df9a46fd387892d260f39ef651", + "checksum": "a00605dfa317d9aecdaa43141a105313739c5400d02807f3f6f38ec3b586363f", "checksum_algorithm": "sha256" }, { "name": "concise", "file": "skills/concise/SKILL.md", "version": "20260421008", - "checksum": "a7688530c1bf7656d78611b3bc82221bc68e29f1977ec9ae722a41d4bdfba168", + "checksum": "ca07cbf74c7e48ef71d0f7d027b2439bb3add4e452790c6a424a4c185637a92c", "checksum_algorithm": "sha256" }, { "name": "consult", "file": "skills/consult/SKILL.md", "version": "20260421009", - "checksum": "b21a457a4d8c615208aef85735f77524192c22de69ee52edab2da4b94c1b2871", + "checksum": "6c091de408c12a22500691d5fc83a3859feb3605e21608422a9d2e1b8413e22b", "checksum_algorithm": "sha256" }, { "name": "container", "file": "skills/container/SKILL.md", "version": "20260421010", - "checksum": "0e530588b322bfbe7d40d510817ada6395234d9188ef348dc98e82d7dc9641ab", + "checksum": "f010f66e2ac5c56b280f7daa47fcd4ea47660829765cb0bd57bb8c310fb08e86", "checksum_algorithm": "sha256" }, { "name": "conventional-commit", "file": "skills/conventional-commit/SKILL.md", "version": "20260502024", - "checksum": "f00f8d0521a7b296284bb486c469f4b39cf19ca212e2fc01671f0785d979507b", + "checksum": "78a41fa28e30b079d049e38651cf939c2a692f81bc33221d672a3f56d1ddd5cd", "checksum_algorithm": "sha256" }, { "name": "debug", "file": "skills/debug/SKILL.md", "version": "20260421011", - "checksum": "286a17b8a7fe00e2702ed042d5101d1355f0373bbd3599df6a2098937ae54fe6", + "checksum": "4c3de6cd912ed32a3b862aa561976cb2a8dc996db713ac7b6b173357967db94d", "checksum_algorithm": "sha256" }, { "name": "dependabot", "file": "skills/dependabot/SKILL.md", "version": "20260502027", - "checksum": "f9bc59d261d6a589703e3d80e6c07d694552ced37e1746f683517fdf0ab757d8", + "checksum": "ea8b610b811bd86de09e26518971e3cb089854e18be21c280c91bd81901a2522", "checksum_algorithm": "sha256" }, { "name": "dependency", "file": "skills/dependency/SKILL.md", "version": "20260421012", - "checksum": "82e51170a8ff0015cc613342b2adbc6ffb44e004ed41632ab0d5602271bcb92b", + "checksum": "e930b6807ec3cc270414a6d9d8b750202375ae70f88cd776a57a5be0d0a078bf", "checksum_algorithm": "sha256" }, { "name": "design", "file": "skills/design/SKILL.md", "version": "20260421013", - "checksum": "880e577dfe3a7c6f51c682f0e230b33486ebbe5fc882a2a784f34457365eab77", + "checksum": "6be33bc6967e9e3477d8cf1a7cb197cb2732b3db17fa7e67cf859bf109cfa40a", "checksum_algorithm": "sha256" }, { "name": "docs", "file": "skills/docs/SKILL.md", "version": "20260421014", - "checksum": "0007ac18f21ab5d57276c8b706f0dae2a03535609d68f536d7fc4156d8d52064", + "checksum": "bbf7626d35e90ed66be9637c5aa8cf1612a86e961956dc825ba43d1ac17ef202", "checksum_algorithm": "sha256" }, { "name": "explore", "file": "skills/explore/SKILL.md", "version": "20260421015", - "checksum": "74cffe4c27d739deae08fa580c0e80ac8e86f44692a5f91b9323e192ba8205d5", + "checksum": "60b3ebf52e96703b4cf625098a4b4b89720c5f265f863faf306148515109147f", "checksum_algorithm": "sha256" }, { "name": "gdpr", "file": "skills/gdpr/SKILL.md", "version": "20260502029", - "checksum": "69840bdc2bfe4fe97e7aabed28a72610b40a8107759ed7abc652a2b3c097aa1a", + "checksum": "2d72a91b16b94957996da61a84dac58481d5b11d1f38a44f81440ddb5a4c39e0", "checksum_algorithm": "sha256" }, { "name": "gh-issues", "file": "skills/gh-issues/SKILL.md", "version": "20260502025", - "checksum": "8890f165be2fdde02cb7c2c35879c59f172630a9f26f27ded8e2560c7494509b", + "checksum": "5fc71ec5dfad0012ef4ead9badef7ab45e2bed09950e831bbcadb1be1a3a4ef7", "checksum_algorithm": "sha256" }, { "name": "gh-release", "file": "skills/gh-release/SKILL.md", "version": "20260502023", - "checksum": "fa755f6e24e8d9d06284b9e6838ca7ac373c162aa0a3796b15ba5bfcdc61e12e", + "checksum": "97a7e2bfb578613ac02b2e420a527604ff7e336918bdc49b0778e25b90442687", "checksum_algorithm": "sha256" }, { "name": "guardrails", "file": "skills/guardrails/SKILL.md", "version": "20260421016", - "checksum": "b5c0f993f26692f117a9103e3101a132133dee78e45a18b2692f66ce557735e9", + "checksum": "b116ea94fcea4264a1ebc28d42ad6b410271ddf7c85382411199a6f49eecf139", "checksum_algorithm": "sha256" }, { "name": "helm", "file": "skills/helm/SKILL.md", "version": "20260502037", - "checksum": "508d33f75bd7d2c8a243b5feb94b020186ff89abc871f6f6690852dd0ce75b33", + "checksum": "2b0377609295dc99820d7c1ef53258e1b2f4e7f1e75ec648558b82c4a7339d6d", "checksum_algorithm": "sha256" }, { "name": "incident", "file": "skills/incident/SKILL.md", "version": "20260503002", - "checksum": "4ae13444c51626b7b9f92849be08951995c6b558f42d6a963598b93da228185e", + "checksum": "a7a9c217e9df69a72a900c53c3668f8490660b891c31dbc09524dc38cd584699", "checksum_algorithm": "sha256" }, { "name": "inspect", "file": "skills/inspect/SKILL.md", "version": "20260421018", - "checksum": "31256c09d7583c57b1c3b1e0ee9aa993218ed671c3ad797427a1e8b4bd6acc5f", + "checksum": "55f42917225d206f01086d2e1bbbe08397cdcb88c05f1a0b5737630cd3256f1c", "checksum_algorithm": "sha256" }, { "name": "k8s", "file": "skills/k8s/SKILL.md", "version": "20260502036", - "checksum": "3c15b01cd5283bb3eebe3a3bfdc03f7db6869dde0ea5cb47123f569fc73f2d2c", + "checksum": "4f264e77f40a607c65b404d0afc856d4c5c770257995191d8dfb3277836ace39", "checksum_algorithm": "sha256" }, { "name": "migrate", "file": "skills/migrate/SKILL.md", "version": "20260421019", - "checksum": "9aa0d50ddf33695f840f09e5914fcd3e3b1fb7826708f6a141880c411cae64fd", + "checksum": "95af7429d6ed41c5757bf7a1d9e4b7a2765d71ee4ee0ff591105c55f48126a24", "checksum_algorithm": "sha256" }, { "name": "onboard", "file": "skills/onboard/SKILL.md", "version": "20260421020", - "checksum": "f5db644175563ed046668984fe3c5575f8b3d94e608114b2a8e43e298a0dab5e", + "checksum": "506870d17780c4f2e7bb85245ab4056746e6759f5f28c00914c6f26feaefdf4f", "checksum_algorithm": "sha256" }, { "name": "openapi", "file": "skills/openapi/SKILL.md", "version": "20260421021", - "checksum": "13435e37d104ae8fc7ce98bfd39bf3226ba2cc44dcce7c29d0c2214320a94aa7", + "checksum": "dc721152115a5f347cb780511a72b09420abfeb1b67236b5f6848dd2bfad3ada", "checksum_algorithm": "sha256" }, { "name": "performance", "file": "skills/performance/SKILL.md", "version": "20260421022", - "checksum": "7bfe8f08b57a6b16eb5b7d048684abd88bce6bad48021825930ed677c010513b", + "checksum": "24dd38ad95167be5fda2744abc717da23426457a476cae0c4d6e5afb222f9d89", "checksum_algorithm": "sha256" }, { "name": "postmortem", "file": "skills/postmortem/SKILL.md", "version": "20260503001", - "checksum": "ed01914fd3782d5380edb2452ce886849a5431ce67a23f24c5ddcaaca43f1fd1", + "checksum": "a1ff3b06292f5131b751ede987a8b380a5ccced6a6a7c6b0dbfebde549d25ee9", "checksum_algorithm": "sha256" }, { "name": "pr", "file": "skills/pr/SKILL.md", "version": "20260502013", - "checksum": "32551868293c30dcdbd2ba390823dbef36b1d4421d42912480d1ad8a991b49cd", + "checksum": "8b25caca722cf9781c8d2fab87548071f3dbfd63fab847a0081849f0df6c3c38", "checksum_algorithm": "sha256" }, { "name": "rancher", "file": "skills/rancher/SKILL.md", "version": "20260502038", - "checksum": "b7b61eb2cbc76bdeba886e04faf596023cc95f33c1128c1ed1c318f5e01bb604", + "checksum": "d5f4517005e3829a3a0503fdc4e15814993b6d2bbf85a92512b6e46b25d65037", "checksum_algorithm": "sha256" }, { "name": "rca", "file": "skills/rca/SKILL.md", "version": "20260503001", - "checksum": "480e4ded32dce4d9e558d6cb0cb323412e2a260950f8feb874891f185fbe06c3", + "checksum": "cc022ebca687f463ca88002db04dbd3af720ba2c9d9810fc441cd8fc5e29e947", "checksum_algorithm": "sha256" }, { "name": "refactor", "file": "skills/refactor/SKILL.md", "version": "20260421023", - "checksum": "4159e7f0a769afae1ae85851fba644e06c71c47205a9b5ad0db502d7af47630f", + "checksum": "c7741be01bfca5bf77727cb21b6b00c939fee8a61160fe0e66d21ceedc7124f2", "checksum_algorithm": "sha256" }, { "name": "release-notes", "file": "skills/release-notes/SKILL.md", "version": "20260502014", - "checksum": "58e9a6e4bf52181ae7e512362033280ab8fd3354beb2587811b560c71bccbd88", + "checksum": "2a76cf7833e7bfd30977162102b94f7e7253b174ea263a57aa5e0b04dc669582", "checksum_algorithm": "sha256" }, { "name": "requirements", "file": "skills/requirements/SKILL.md", "version": "20260421024", - "checksum": "fb596a45f7a39902b72149f41ad79abf654c2ae2e42951d0cdd2e5a5f7da255a", + "checksum": "66716f4e09c4ab820ced6d4bf3b495d13ec2701d1c1544556f0a804c4850ab7a", "checksum_algorithm": "sha256" }, { "name": "secret-scan", "file": "skills/secret-scan/SKILL.md", "version": "20260502028", - "checksum": "f9147ea425731643e0a0daef9c3836994093dcf4e5d0a7ff43de636e6a16b55d", + "checksum": "5fcb8afd3547448ee41cba699453f4c572072a424cf2eaaf521019c79c80fd2f", "checksum_algorithm": "sha256" }, { "name": "security", "file": "skills/security/SKILL.md", "version": "20260421025", - "checksum": "2895a95ae6826064094f272da634cd49038040c8e3b53aa7ab2a8148ef0cfa92", + "checksum": "412dad2ff508306160c427e5c09cf80693d1d716d61452352dc218512581754e", "checksum_algorithm": "sha256" }, { "name": "terraform", "file": "skills/terraform/SKILL.md", "version": "20260502030", - "checksum": "355520d94f5b0af10db7aa2a9fe4bc23eeb64f119377f23ae52e2e4cf4d13448", + "checksum": "46e7c27ca0421a0862f307566cedb864fb2c4d3067a30b4c9bfa65a1b06111fa", "checksum_algorithm": "sha256" }, { "name": "terragrunt", "file": "skills/terragrunt/SKILL.md", "version": "20260502031", - "checksum": "e81301a82b0aeea974a9e2e6d02ab8915ad328421d8314b33ab1412929ec32e7", + "checksum": "b518c7e63a1a83fc82578a549caa6eac622feffb4a728046fd47ed9761f2fe2d", "checksum_algorithm": "sha256" }, { "name": "threat-model", "file": "skills/threat-model/SKILL.md", "version": "20260502021", - "checksum": "aea7fccd1ed184d005c8ecc9bc807ae7c3389561ca9e850abafa88a974ab5501", + "checksum": "43c853ab3baa99b25adec0570395a39d309222616c2844c017469302340d0b79", "checksum_algorithm": "sha256" }, { "name": "verify", "file": "skills/verify/SKILL.md", "version": "20260421026", - "checksum": "59fbdac950bf0a82d33f13dda8afb71819045d4b2e18f37ecb7eb4e0ce590875", + "checksum": "1fcec13deb1316dcc55050108f394ae8878e45c009304179a81da49bc512354c", "checksum_algorithm": "sha256" }, { "name": "vision", "file": "skills/vision/SKILL.md", "version": "20260421027", - "checksum": "46cdb79519bf700e6fdce208d11093ac732edd2325018524f4f7b85a90b0d968", + "checksum": "ca6c5fc7b3b7800e858e80631a36c4839f7364e89246fe04c412b7c7fd4a4520", "checksum_algorithm": "sha256" } ], @@ -319,42 +319,42 @@ "name": "architect", "file": "agents/architect.agent.md", "version": "20260503022", - "checksum": "a2fffcbf0fd416ffcfa36964c97b436e10943345dcf359c1a42d2b6cf6aeacb9", + "checksum": "e02b3c3f71c5081f70556971672aef2ddbc52c855185cf1a58a5357c847cafdc", "checksum_algorithm": "sha256" }, { "name": "designer", "file": "agents/designer.agent.md", "version": "20260503024", - "checksum": "513bdd6f182bb33d874953359340bece823befedd34945d3dd03405391f7b53b", + "checksum": "186f51a4f10003bc220a95c2d9d89c33546e19a970150e536c90822714c634d6", "checksum_algorithm": "sha256" }, { "name": "engineer", "file": "agents/engineer.agent.md", "version": "20260503024", - "checksum": "1d3b1b16b88f291439ec9807138671e38d5bcd6bdcbf1d7f8dcc5f84e28e7125", + "checksum": "08e905f3f6e724d02f774717c6c1cdd9c4bc5139f60053d9ec2864cf0d80f930", "checksum_algorithm": "sha256" }, { "name": "product", "file": "agents/product.agent.md", "version": "20260503021", - "checksum": "d313ffa4acaf0b8b58c64621389be97c62328abd5ec4e1d58f56a019102eddba", + "checksum": "e1928713b16fa07ca769dd62457a42d7415be6e531ae529af5a9e46e4c639518", "checksum_algorithm": "sha256" }, { "name": "release", "file": "agents/release.agent.md", "version": "20260503020", - "checksum": "773dd65312007d80f5dd95d26858bee60599ba6f4515f3531f5384c6710c0468", + "checksum": "fa76f7fea418f40ffd3a18ba02f645a196e603ceda293e629ee41ce31e27dc6f", "checksum_algorithm": "sha256" }, { "name": "tester", "file": "agents/tester.agent.md", "version": "20260503026", - "checksum": "e36ea99aa29200b0d1b89d5958980186f96e948afc5ea43c34500c2a09b7b83c", + "checksum": "6c31332714089743d7c5bfe2e45fec5471f03b21795f30edc920d110199f1d27", "checksum_algorithm": "sha256" } ], @@ -363,84 +363,84 @@ "name": "git", "file": "instructions/git.instructions.md", "version": "20260421001", - "checksum": "4cb957fd493b9f0c6fa0845b6a2743ff0eec02e335491b9a02d7ec62544eacc5", + "checksum": "ed6a191176e32631e2d572cb21278b555f6696a859839579821631156a8b35a5", "checksum_algorithm": "sha256" }, { "name": "helm", "file": "instructions/helm.instructions.md", "version": "20260502040", - "checksum": "805353d22a08577058f5d13d0c281ac7a8f098c7d7408dd044696028ac696cc5", + "checksum": "a60520853b79751517f089136ba7fb182feb1ea8e9c9495890e5c14ff32afd7b", "checksum_algorithm": "sha256" }, { "name": "java", "file": "instructions/java.instructions.md", "version": "20260502001", - "checksum": "65c1ba8c0705b77b207ccc980bf6183310b4774030db6e0e88e9bced40b8a0f9", + "checksum": "69dc2bf3a5428ed03d77c2789871985ea7c5881af89c2b414548fca6e9488464", "checksum_algorithm": "sha256" }, { "name": "k8s", "file": "instructions/k8s.instructions.md", "version": "20260502039", - "checksum": "db9fe194a3a50dcacf278886a8278dce98a57f1eba46cf5a68cf40d64511ba6b", + "checksum": "f19f1060bf2a1424950496290a43a3535f9749e4ed838cf7475fe35eb9487d67", "checksum_algorithm": "sha256" }, { "name": "markdown", "file": "instructions/markdown.instructions.md", "version": "20260502002", - "checksum": "cb197a6f1c67716d21c269094df82b7f675c68e5f390ad1d4f61e58d52080297", + "checksum": "59c93c5b0e63360aff18ab3a7a207f4e798efd1f2c26b0a561a4ef4ea688c5c5", "checksum_algorithm": "sha256" }, { "name": "python", "file": "instructions/python.instructions.md", "version": "20260421002", - "checksum": "33298cb94c45ff7a934466d0fdaa885341f63336844fb047e953825810dc83dd", + "checksum": "ac40fffd3d3a3f9f8ca43e10ca603a187578c9d517533529a3d647b61cedf56c", "checksum_algorithm": "sha256" }, { "name": "rancher", "file": "instructions/rancher.instructions.md", "version": "20260502041", - "checksum": "9d4886f04b633f968a33469de8520cced602792073106760e01ed965bdc284bd", + "checksum": "4e643b1e078e9e8f127f1f2cf7707b36109e28b8697e208d8d3804848b867eec", "checksum_algorithm": "sha256" }, { "name": "security", "file": "instructions/security.instructions.md", "version": "20260502003", - "checksum": "f2424e0dbee186186f1488837e1aa411bba6a613260c4b967b80295bb5d3084a", + "checksum": "a943df637e44cb23c8e82f8b39bf70558bc502ece5af9a03a15d8492cffb51a4", "checksum_algorithm": "sha256" }, { "name": "terraform", "file": "instructions/terraform.instructions.md", "version": "20260502034", - "checksum": "09a54830d24cf3c103f835206358f197962d484b9bca3cac0e92db332811c75a", + "checksum": "1bada82da46a6359bb1814b6642074be6c0b7de9c2c6ca59e031a4d05ea4ea8c", "checksum_algorithm": "sha256" }, { "name": "terragrunt", "file": "instructions/terragrunt.instructions.md", "version": "20260502035", - "checksum": "f911d207ac80fe1526e6178079faaa4c75a462a6b2b6d3b4ee7cf44d3d29b0f5", + "checksum": "13c0059c366624ab482c2b3d24a353bcb0a2ffbb6bf735abd18796969a3e4020", "checksum_algorithm": "sha256" }, { "name": "testing", "file": "instructions/testing.instructions.md", "version": "20260502004", - "checksum": "1cb3979aa86d86a57d78070ff6a683fecce1837719560dc430f9fa44b95b06ef", + "checksum": "cbd1948f367c32c39032209e5ed9fcfff8ce6c46c6324a4aa9a8550365873ca3", "checksum_algorithm": "sha256" }, { "name": "typescript", "file": "instructions/typescript.instructions.md", "version": "20260502005", - "checksum": "696a2faf692022eae14212d2ff91c96439caa4749f51bbb62b26e460b517a6eb", + "checksum": "fe412ba2e60baea66d0d07ae0c153fa2a9157bd57556fa7476f46ce466160ad3", "checksum_algorithm": "sha256" } ], @@ -449,49 +449,49 @@ "name": "api-design-review", "file": "prompts/api-design-review.prompt.md", "version": "20260502006", - "checksum": "081340767c8d48ef302cd473ad2deab35d7dbe2e287b39096ba7c98ce4f49520", + "checksum": "1ba62a6f78b836256fe578c4ca312de24599c79b018119b81a100f6de4e0da11", "checksum_algorithm": "sha256" }, { "name": "architecture-risk", "file": "prompts/architecture-risk.prompt.md", "version": "20260502007", - "checksum": "f1676e2a7b5f6612639863cf2c5839befa97aaa1b20837c3b8a848b6f625b216", + "checksum": "14fa36e36948309827c1c1296ff2c8b1f1306cfb19057ef190a5e60f6cc04a61", "checksum_algorithm": "sha256" }, { "name": "code-review", "file": "prompts/code-review.prompt.md", "version": "20260502008", - "checksum": "e0703ddfdd6fa9f10ee0006eb3601428c5458742b5fd9582acd7295aa769c867", + "checksum": "b6499ca66706a08ced7b9bf69bd288c2fecb684e1c85e333ee91d36ba7e66c3f", "checksum_algorithm": "sha256" }, { "name": "dependency-audit", "file": "prompts/dependency-audit.prompt.md", "version": "20260502009", - "checksum": "8b44c09c72f3531c3d5f6b17fcb6c7f3436add6e224abe18474d61d59864db1a", + "checksum": "51cead168dbbe52b455813cc620a8426ec287ceb2994831aa6ae115b4e49d0b0", "checksum_algorithm": "sha256" }, { "name": "incident-timeline", "file": "prompts/incident-timeline.prompt.md", "version": "20260502010", - "checksum": "991380657d904673918a6a42ab36cee839eaec9f5b27a3aac3518d6d4f429e62", + "checksum": "28627dc7c362d510f312281fe0bda30e1927998bae440808af3a92d06c455393", "checksum_algorithm": "sha256" }, { "name": "migration-safety", "file": "prompts/migration-safety.prompt.md", "version": "20260502011", - "checksum": "a514704586f271cba25ee285df76acb34d1ac71cd1bee4280eb4887cca1bafcc", + "checksum": "814258882774ff98724b14caa0bc9ee35dc68f46e72e62b24884052e79ffacfa", "checksum_algorithm": "sha256" }, { "name": "release-readiness", "file": "prompts/release-readiness.prompt.md", "version": "20260502012", - "checksum": "99dcf393a8f1cc474f3f9c50421834d134b621da28afbc4a1ddc7c944fdd2920", + "checksum": "49898a07169504acba1c2195abf730be77d1e17298cb4676f3ed926a27e31dc6", "checksum_algorithm": "sha256" } ] From 9aec316e5e81e15f24f3939d24a328e09f1dd936 Mon Sep 17 00:00:00 2001 From: Erik Schaareman Date: Sat, 9 May 2026 19:30:27 +0200 Subject: [PATCH 08/25] fix(review): remove dead str-fallback in _extract_defaults; sync docs with pyyaml dep --- .../adr/006-no-runtime-dependency.md | 6 ++++-- docs/architecture/overview.md | 19 ++++++++++--------- docs/product/requirements.md | 4 ++-- pyproject.toml | 1 + src/vstack/agents/generator.py | 17 +++++------------ tests/vstack/agents/test_generator.py | 17 +++++++++++------ 6 files changed, 33 insertions(+), 31 deletions(-) diff --git a/docs/architecture/adr/006-no-runtime-dependency.md b/docs/architecture/adr/006-no-runtime-dependency.md index ea69fd3..d39ed83 100644 --- a/docs/architecture/adr/006-no-runtime-dependency.md +++ b/docs/architecture/adr/006-no-runtime-dependency.md @@ -36,5 +36,7 @@ any prerequisites beyond the project's own toolchain. ## impact on future orchestrated pipeline -The future orchestrated pipeline runner will be `scripts/runner.py` (stdlib only), not a -skill runtime dependency. +The orchestrated pipeline uses VS Code native subagents via the `runSubagent` tool +(see ADR-024) — no separate `scripts/runner.py` runner is needed. +The sole runtime dependency (`pyyaml`) is a pip package, not a binary, and does +not affect the skill portability guarantee this ADR establishes. diff --git a/docs/architecture/overview.md b/docs/architecture/overview.md index 07bf385..fe4eed1 100644 --- a/docs/architecture/overview.md +++ b/docs/architecture/overview.md @@ -169,15 +169,15 @@ ______________________________________________________________________ These bind architecture decisions. Full list in `docs/product/requirements.md`. -| ID | Requirement | Architectural binding | -| ----- | --------------------------------------------------------------------------------- | ----------------------------------------- | -| NFR-1 | No runtime dependencies beyond the Python standard library | ADR-006, ADR-007 | -| NFR-2 | Python 3.11–3.14 compatibility | ADR-007 | -| NFR-3 | Manifest writes are atomic | ADR-016 | -| NFR-4 | All public behavior covered by automated tests; CI enforces test pass | `tests/` structure, `verify.yml` workflow | -| NFR-5 | CLI operates standalone; no VS Code process required for CLI operations | ADR-006, stdlib-only runtime | -| NFR-6 | Lint and type checking pass on every commit; CI gate enforces zero violations | `pyproject.toml` ruff + mypy config | -| NFR-7 | Generated output lives under `.github/` only; templates never modified at runtime | ADR-012 | +| ID | Requirement | Architectural binding | +| ----- | --------------------------------------------------------------------------------- | ---------------------------------------------------------- | +| NFR-1 | No external binary dependencies in skill template content | ADR-006; one pip dependency (`pyyaml`) allowed per ADR-025 | +| NFR-2 | Python 3.11–3.14 compatibility | ADR-007 | +| NFR-3 | Manifest writes are atomic | ADR-016 | +| NFR-4 | All public behavior covered by automated tests; CI enforces test pass | `tests/` structure, `verify.yml` workflow | +| NFR-5 | CLI operates standalone; no VS Code process required for CLI operations | ADR-006; only `pyyaml` required at runtime (ADR-025) | +| NFR-6 | Lint and type checking pass on every commit; CI gate enforces zero violations | `pyproject.toml` ruff + mypy config | +| NFR-7 | Generated output lives under `.github/` only; templates never modified at runtime | ADR-012 | ______________________________________________________________________ @@ -251,3 +251,4 @@ See individual files for context, decision, alternatives, and rationale. | 022 | Selective exclude filter in `.vstack/config.yaml` | accepted | Agents cannot be excluded (atomic unit) | | 023 | Workflow contract in `.vstack/config.yaml` | accepted | Pipeline order, gate, hitl, handoffs | | 024 | Subagent orchestration via VS Code native subagents | accepted | Supersedes ADR-004; planner coordinator | +| 025 | PyYAML as sole runtime dependency | accepted | Replaces hand-rolled frontmatter parser | diff --git a/docs/product/requirements.md b/docs/product/requirements.md index 7831834..28b7c81 100644 --- a/docs/product/requirements.md +++ b/docs/product/requirements.md @@ -10,8 +10,8 @@ ______________________________________________________________________ vstack is a VS Code-native AI engineering workflow system. It installs structured agents, skills, instructions, and prompts into `.github/` so GitHub Copilot Agent Mode has a clear operating model. vstack is distributed as a standalone Python CLI -tool (`pipx install vstack`) with no runtime dependencies beyond the Python standard -library. +tool (`pipx install vstack`) with a single runtime dependency (`pyyaml>=6.0`) +for YAML frontmatter parsing (see ADR-025). ______________________________________________________________________ diff --git a/pyproject.toml b/pyproject.toml index 4dd3e73..f2a948d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -183,6 +183,7 @@ description = Run mypy type checks package = skip deps = mypy>=1.20 + types-PyYAML>=6.0 commands = mypy src tests """ diff --git a/src/vstack/agents/generator.py b/src/vstack/agents/generator.py index 7ef7433..b0b3887 100644 --- a/src/vstack/agents/generator.py +++ b/src/vstack/agents/generator.py @@ -120,23 +120,16 @@ def template_partials(self, tmpl_dir: Path) -> dict[str, str]: } def _extract_defaults(self, config: dict) -> dict: - """Return the parsed ``defaults:`` block from *config*, or an empty dict. + """Return the ``defaults:`` block from *config*, or an empty dict. - The ``defaults:`` value may be stored as a raw indented YAML string by - the minimal frontmatter parser. This helper re-parses it when needed - and always returns a plain dict. + PyYAML always returns ``defaults:`` as a dict when the block is + present in ``config.yaml``. Non-dict values (e.g. ``None`` when the + key is absent) are normalised to ``{}``. :param config: Raw config dict as returned by ``load_artifact_config``. - :returns: Parsed ``defaults`` dict, or ``{}`` when absent or unparseable. + :returns: Parsed ``defaults`` dict, or ``{}`` when absent or not a mapping. """ - from vstack.frontmatter import FrontmatterParser - defaults = config.get("defaults") or {} - if isinstance(defaults, str) and defaults.strip(): - dedented = "\n".join( - line[2:] if line.startswith(" ") else line for line in defaults.split("\n") - ) - defaults = FrontmatterParser.parse_yaml(dedented) or {} return defaults if isinstance(defaults, dict) else {} def load_artifact_config(self, tmpl_dir: Path) -> dict: diff --git a/tests/vstack/agents/test_generator.py b/tests/vstack/agents/test_generator.py index b65154b..a5d974f 100644 --- a/tests/vstack/agents/test_generator.py +++ b/tests/vstack/agents/test_generator.py @@ -211,12 +211,17 @@ def test_returns_empty_dict_when_defaults_is_non_dict_non_string(self) -> None: """Returns empty dict when defaults is neither a string nor a dict.""" assert AgentGenerator()._extract_defaults({"defaults": 42}) == {} - def test_reparses_raw_indented_string(self) -> None: - """Re-parses a raw indented YAML string produced by the minimal parser.""" - raw_defaults = " artifacts:\n dir: design\n" - result = AgentGenerator()._extract_defaults({"defaults": raw_defaults}) - assert isinstance(result, dict) - assert "artifacts" in result + def test_returns_empty_dict_for_non_dict_string(self) -> None: + """Non-dict values (including strings) return an empty dict. + + PyYAML always produces a dict for a ``defaults:`` mapping block, so + a string value is not produced in normal operation. The method still + handles it defensively and returns ``{}``. + """ + assert ( + AgentGenerator()._extract_defaults({"defaults": " artifacts:\n dir: design\n"}) + == {} + ) class TestLoadArtifactConfig: """Tests for AgentGenerator.load_artifact_config.""" From a6c3cc997d4e07d11dc7aa45d5ca82967639bf7a Mon Sep 17 00:00:00 2001 From: Erik Schaareman Date: Sat, 9 May 2026 20:08:10 +0200 Subject: [PATCH 09/25] =?UTF-8?q?refactor:=20OOP=20cleanup=20=E2=80=94=20i?= =?UTF-8?q?nstance=20methods,=20inline=20registry,=20extracted=20utils?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/vstack/artifacts/generator.py | 7 ++- src/vstack/cli/base.py | 5 ++ src/vstack/cli/helpers.py | 8 --- src/vstack/cli/init.py | 16 +++--- src/vstack/cli/interface.py | 18 ++++-- src/vstack/cli/registry.py | 15 ----- src/vstack/cli/uninstall.py | 12 ++-- src/vstack/frontmatter/parser.py | 13 ++--- src/vstack/frontmatter/serializer.py | 17 +++--- src/vstack/manifest/__init__.py | 7 +-- src/vstack/manifest/store.py | 75 ++++++++++++------------- src/vstack/utils.py | 20 +++++++ tests/vstack/agents/test_generation.py | 2 +- tests/vstack/agents/test_role_wiring.py | 2 +- tests/vstack/cli/test_helpers.py | 20 ++++--- tests/vstack/cli/test_interface.py | 25 +++++---- tests/vstack/cli/test_registry.py | 18 +++--- tests/vstack/frontmatter/test_parser.py | 44 ++++++++------- tests/vstack/skills/test_templates.py | 4 +- 19 files changed, 166 insertions(+), 162 deletions(-) delete mode 100644 src/vstack/cli/helpers.py delete mode 100644 src/vstack/cli/registry.py create mode 100644 src/vstack/utils.py diff --git a/src/vstack/artifacts/generator.py b/src/vstack/artifacts/generator.py index 8cbcf80..cba4537 100644 --- a/src/vstack/artifacts/generator.py +++ b/src/vstack/artifacts/generator.py @@ -46,6 +46,7 @@ def __init__(self, type_config: ArtifactTypeConfig, templates_root: Path) -> Non else None ) self._partials: dict[str, str] | None = None + self._parser = FrontmatterParser() # ── Placeholder resolution ──────────────────────────────────────────────── @@ -140,7 +141,7 @@ def load_artifact_config(self, tmpl_dir: Path) -> dict: if not config_file.exists(): return {} raw = config_file.read_text(encoding="utf-8") - return FrontmatterParser.parse_yaml(raw) + return self._parser.parse_yaml(raw) # ── Rendering ───────────────────────────────────────────────────────────── @@ -172,7 +173,7 @@ def render(self, tmpl_dir: Path) -> RenderedArtifact: resolved = self.resolve_placeholders(content, partials) # Split existing frontmatter from body - parsed = FrontmatterParser.parse(resolved) + parsed = self._parser.parse(resolved) existing_fm = parsed.metadata body = parsed.content @@ -312,7 +313,7 @@ def fail(msg: str) -> None: for name, tmpl_dir in tmpl_by_name.items(): content = (tmpl_dir / self.config.template_filename).read_text(encoding="utf-8") artifact_config = self.load_artifact_config(tmpl_dir) - parsed = FrontmatterParser.parse(content) + parsed = self._parser.parse(content) existing_fm = parsed.metadata meta = {**artifact_config, **existing_fm} if existing_fm else artifact_config diff --git a/src/vstack/cli/base.py b/src/vstack/cli/base.py index 476d648..98ca027 100644 --- a/src/vstack/cli/base.py +++ b/src/vstack/cli/base.py @@ -27,6 +27,11 @@ def require_install_dir(self, command_name: str) -> Path: class BaseCommand(ABC): """Abstract base class for top-level CLI command handlers.""" + @staticmethod + def _normalize_targeted_names(names: list[str] | None) -> set[str]: + """Normalize targeted artifact names for force/adopt operations.""" + return {name.strip() for name in names or [] if name.strip()} + @abstractmethod def run( self, diff --git a/src/vstack/cli/helpers.py b/src/vstack/cli/helpers.py deleted file mode 100644 index 87526d5..0000000 --- a/src/vstack/cli/helpers.py +++ /dev/null @@ -1,8 +0,0 @@ -"""Shared internal helpers for CLI command handlers.""" - -from __future__ import annotations - - -def normalize_targeted_names(names: list[str] | None) -> set[str]: - """Normalize targeted artifact names for force/adopt operations.""" - return {name.strip() for name in names or [] if name.strip()} diff --git a/src/vstack/cli/init.py b/src/vstack/cli/init.py index 12a3b3a..b3af947 100644 --- a/src/vstack/cli/init.py +++ b/src/vstack/cli/init.py @@ -9,7 +9,6 @@ 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, @@ -17,8 +16,6 @@ Manifest, content_hash, hash_with_algorithm, - preserve_existing_entry, - preserved_manifest_entries, ) if TYPE_CHECKING: @@ -231,9 +228,10 @@ def _load_existing_manifest( 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, + new_entries = ( + existing_manifest.preserved_entries(selected_manifest_keys) + if existing_manifest is not None + else {} ) return manifest_file, existing_manifest, existing_entries, new_entries @@ -338,7 +336,7 @@ def _install_single_artifact( return action if existing_entry is not None: - preserve_existing_entry( + Manifest.preserve_existing_entry( new_entries=new_entries, manifest_key=gen.config.manifest_key, existing_entry=existing_entry, @@ -467,8 +465,8 @@ def execute( 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) + targeted_force_names = InitCommand._normalize_targeted_names(force_names) + targeted_adopt_names = InitCommand._normalize_targeted_names(adopt_names) # Validate workflow stages against known agent names (warning only). from vstack.agents.generator import AgentGenerator diff --git a/src/vstack/cli/interface.py b/src/vstack/cli/interface.py index 1a1cdf0..0ee5784 100644 --- a/src/vstack/cli/interface.py +++ b/src/vstack/cli/interface.py @@ -5,11 +5,10 @@ import argparse from pathlib import Path -from vstack.cli.base import CommandContext +from vstack.cli.base import BaseCommand, CommandContext from vstack.cli.catalog import COMMAND_CATALOG 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 @@ -100,7 +99,7 @@ def _read_exclude( 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")) + parsed = FrontmatterParser().parse_yaml(config_path.read_text(encoding="utf-8")) raw_exclude = parsed.get("exclude", "") if not isinstance(raw_exclude, dict): return frozenset(), {} @@ -137,7 +136,7 @@ def _read_artifacts_root(install_dir: Path | None) -> str: 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")) + parsed = FrontmatterParser().parse_yaml(config_path.read_text(encoding="utf-8")) artifacts = parsed.get("artifacts", "") if not isinstance(artifacts, dict): return ARTIFACTS_DOCS_ROOT @@ -164,7 +163,7 @@ def _read_workflow_stages(install_dir: Path | None) -> list[dict]: config_path = install_dir.parent / ".vstack" / "config.yaml" if not config_path.exists(): return [] - parsed = FrontmatterParser.parse_yaml(config_path.read_text(encoding="utf-8")) + parsed = FrontmatterParser().parse_yaml(config_path.read_text(encoding="utf-8")) workflow = parsed.get("workflow", "") if not isinstance(workflow, dict): return [] @@ -232,6 +231,13 @@ def _parse_stage_handoffs(item: dict) -> list[dict[str, str]]: return [{"prompt": legacy, "agent": "", "label": ""}] return [] + def _build_command_registry(self, service: CommandService) -> dict[str, BaseCommand]: + """Instantiate all catalog commands against *service*.""" + return { + command_name: config.command_factory(service) + for command_name, config in COMMAND_CATALOG.items() + } + def run(self) -> int: """Run one CLI invocation and return a process-style status code.""" cli_parser = self._parser_cls() @@ -251,7 +257,7 @@ def run(self) -> int: artifacts_root=artifacts_root, workflow_stages=workflow_stages, ) - commands = build_command_registry(service) + commands = self._build_command_registry(service) effective_only = self._resolve_only_filter( args=args, resolve_only_for_scope=command_config.resolve_only_for_scope, diff --git a/src/vstack/cli/registry.py b/src/vstack/cli/registry.py deleted file mode 100644 index a2e8070..0000000 --- a/src/vstack/cli/registry.py +++ /dev/null @@ -1,15 +0,0 @@ -"""Command registry construction for CLI dispatch.""" - -from __future__ import annotations - -from vstack.cli.base import BaseCommand -from vstack.cli.catalog import COMMAND_CATALOG -from vstack.cli.service import CommandService - - -def build_command_registry(service: CommandService) -> dict[str, BaseCommand]: - """Build the map of top-level command names to command handlers.""" - return { - command_name: config.command_factory(service) - for command_name, config in COMMAND_CATALOG.items() - } diff --git a/src/vstack/cli/uninstall.py b/src/vstack/cli/uninstall.py index 55db8a3..6e3c72a 100644 --- a/src/vstack/cli/uninstall.py +++ b/src/vstack/cli/uninstall.py @@ -9,9 +9,8 @@ from vstack.cli.base import BaseCommand, CommandContext from vstack.cli.constants import ArtifactState -from vstack.cli.helpers import normalize_targeted_names from vstack.constants import VERSION -from vstack.manifest import Manifest, preserve_existing_entry, preserved_manifest_entries +from vstack.manifest import Manifest if TYPE_CHECKING: from vstack.cli.service import CommandService @@ -67,7 +66,7 @@ def _remove_or_preserve_entry( return preserved.append(message) - preserve_existing_entry( + Manifest.preserve_existing_entry( new_entries=new_entries, manifest_key=gen.config.manifest_key, existing_entry=entry, @@ -125,7 +124,7 @@ def execute( manifest_file = service.manifest_for(install_dir) manifest_data = manifest_file.read() 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_force_names = UninstallCommand._normalize_targeted_names(force_names) if manifest_data is None: if manifest_file.read_error: @@ -135,10 +134,7 @@ def execute( return 0 selected_manifest_keys = {gen.config.manifest_key for gen in gens} - new_entries = preserved_manifest_entries( - manifest_data, - selected_manifest_keys, - ) + new_entries = manifest_data.preserved_entries(selected_manifest_keys) for gen in gens: out_dir = install_dir / gen.config.output_subdir diff --git a/src/vstack/frontmatter/parser.py b/src/vstack/frontmatter/parser.py index 7ec3dac..e32bf90 100644 --- a/src/vstack/frontmatter/parser.py +++ b/src/vstack/frontmatter/parser.py @@ -51,8 +51,7 @@ def __bool__(self) -> bool: class FrontmatterParser: """Parse YAML frontmatter using :func:`yaml.safe_load`.""" - @staticmethod - def parse(content: str) -> FrontmatterContent: + def parse(self, content: str) -> FrontmatterContent: """Split YAML frontmatter from body content. Returns a :class:`FrontmatterContent` instance. When no frontmatter @@ -62,11 +61,10 @@ def parse(content: str) -> FrontmatterContent: match = _FRONTMATTER_RE.match(content) if not match: return FrontmatterContent(metadata={}, content=content) - meta = FrontmatterParser._parse_yaml_block(match.group(1)) + meta = self._parse_yaml_block(match.group(1)) return FrontmatterContent(metadata=meta, content=match.group(2)) - @staticmethod - def parse_yaml(raw: str) -> dict: + def parse_yaml(self, raw: str) -> dict: """Parse a raw YAML string without frontmatter delimiters. Args: @@ -75,12 +73,11 @@ def parse_yaml(raw: str) -> dict: Returns: A parsed metadata dictionary. """ - return FrontmatterParser._parse_yaml_block(raw) + return self._parse_yaml_block(raw) # ── Internal ────────────────────────────────────────────────────────────── - @staticmethod - def _parse_yaml_block(raw: str) -> dict: + def _parse_yaml_block(self, raw: str) -> dict: """Delegate YAML parsing to :func:`yaml.safe_load`. Pre-processes ``- *`` (VS Code wildcard list items) into quoted form diff --git a/src/vstack/frontmatter/serializer.py b/src/vstack/frontmatter/serializer.py index c683147..370eae5 100644 --- a/src/vstack/frontmatter/serializer.py +++ b/src/vstack/frontmatter/serializer.py @@ -21,13 +21,6 @@ _YAML_SPECIAL_LEADING = frozenset("*&!") -def _quote_list_item(item: str) -> str: - """Return *item* single-quoted when it starts with a YAML-special character.""" - if item and item[0] in _YAML_SPECIAL_LEADING: - return "'" + item.replace("'", "''") + "'" - return item - - class FrontmatterSerializer: """Frontmatter serializer — converts metadata dict to YAML. @@ -35,6 +28,12 @@ class FrontmatterSerializer: No mutable instance state is retained between calls. """ + def _quote_list_item(self, item: str) -> str: + """Return *item* single-quoted when it starts with a YAML-special character.""" + if item and item[0] in _YAML_SPECIAL_LEADING: + return "'" + item.replace("'", "''") + "'" + return item + def _serialize_scalar(self, spec: FieldSpec, value: object) -> str: """Serialize a single ``"str"`` value according to *spec* options.""" text = str(value) @@ -102,7 +101,7 @@ def _serialize_object_field_pair( if spec.type == "list": if isinstance(value, list) and value: lines = [f"{spec.name}:"] - lines.extend(f" - {_quote_list_item(str(item_v))}" for item_v in value) + lines.extend(f" - {self._quote_list_item(str(item_v))}" for item_v in value) return lines return [] if self._should_emit_multiline(value, preserve_multiline): @@ -172,7 +171,7 @@ def _append_field_by_type( if isinstance(value, list) and value: lines.append(f"{spec.name}:") for item in value: - lines.append(f" - {_quote_list_item(str(item))}") + lines.append(f" - {self._quote_list_item(str(item))}") return if spec.type == "object-list": if isinstance(value, list) and value: diff --git a/src/vstack/manifest/__init__.py b/src/vstack/manifest/__init__.py index c46b11c..d6f26c0 100644 --- a/src/vstack/manifest/__init__.py +++ b/src/vstack/manifest/__init__.py @@ -6,11 +6,8 @@ ArtifactEntry, Manifest, ManifestFile, - content_hash, - hash_with_algorithm, - preserve_existing_entry, - preserved_manifest_entries, ) +from vstack.utils import content_hash, hash_with_algorithm __all__ = [ "CURRENT_HASH_ALGORITHM", @@ -20,6 +17,4 @@ "ManifestFile", "content_hash", "hash_with_algorithm", - "preserve_existing_entry", - "preserved_manifest_entries", ] diff --git a/src/vstack/manifest/store.py b/src/vstack/manifest/store.py index bb53f3b..12dc914 100644 --- a/src/vstack/manifest/store.py +++ b/src/vstack/manifest/store.py @@ -2,7 +2,6 @@ from __future__ import annotations -import hashlib import json import os import re @@ -10,25 +9,13 @@ from pathlib import Path from vstack.constants import MANIFEST_FILENAME +from vstack.utils import content_hash, hash_with_algorithm CURRENT_MANIFEST_VERSION = 2 CURRENT_HASH_ALGORITHM = "sha256" _META_COMMENT_RE = re.compile(r"") - -def content_hash(content: str) -> str: - """Return a stable SHA-256 checksum for rendered artifact content.""" - return hashlib.sha256(content.encode("utf-8")).hexdigest() - - -def hash_with_algorithm(content: str, algorithm: str) -> str: - """Return a checksum for *content* using the requested algorithm.""" - normalized = algorithm.lower() - if normalized == "sha256": - return hashlib.sha256(content.encode("utf-8")).hexdigest() - if normalized == "md5": - return hashlib.md5(content.encode("utf-8"), usedforsecurity=False).hexdigest() - raise ValueError(f"Unsupported checksum algorithm: {algorithm}") +__all__ = ["content_hash", "hash_with_algorithm"] @dataclass @@ -240,6 +227,39 @@ def with_backfilled_checksums( skipped, ) + def preserved_entries( + self, + selected_manifest_keys: set[str], + ) -> dict[str, list[ArtifactEntry]]: + """Return artifact families not in *selected_manifest_keys*, preserving them intact. + + Used to carry forward unselected artifact types when only a subset + is targeted for the current install or uninstall operation. + + :param selected_manifest_keys: Manifest keys that the current operation manages. + :returns: A shallow copy of every artifact family that was not selected. + """ + return { + manifest_key: list(entries) + for manifest_key, entries in self.artifacts.items() + if manifest_key not in selected_manifest_keys + } + + @staticmethod + def preserve_existing_entry( + *, + new_entries: dict[str, list[ArtifactEntry]], + manifest_key: str, + existing_entry: ArtifactEntry, + ) -> None: + """Carry forward one unchanged manifest entry into *new_entries*. + + :param new_entries: The artifact dict being built for the updated manifest. + :param manifest_key: Manifest key (e.g. ``"agents"``) for the entry. + :param existing_entry: The entry to preserve as-is. + """ + new_entries.setdefault(manifest_key, []).append(existing_entry) + @classmethod def from_dict(cls, data: dict) -> Manifest: """Create a :class:`Manifest` from parsed JSON data.""" @@ -281,31 +301,6 @@ def from_dict(cls, data: dict) -> Manifest: ) -def preserved_manifest_entries( - existing_manifest: Manifest | None, - selected_manifest_keys: set[str], -) -> dict[str, list[ArtifactEntry]]: - """Preserve artifact families not selected for the current operation.""" - if existing_manifest is None: - return {} - - preserved: dict[str, list[ArtifactEntry]] = {} - for manifest_key, entries in existing_manifest.artifacts.items(): - if manifest_key not in selected_manifest_keys: - preserved[manifest_key] = list(entries) - return preserved - - -def preserve_existing_entry( - *, - new_entries: dict[str, list[ArtifactEntry]], - manifest_key: str, - existing_entry: ArtifactEntry, -) -> None: - """Carry forward one unchanged manifest entry for a manifest key.""" - new_entries.setdefault(manifest_key, []).append(existing_entry) - - class ManifestFile: """Read and write the ``vstack.json`` manifest inside an install root.""" diff --git a/src/vstack/utils.py b/src/vstack/utils.py new file mode 100644 index 0000000..e9805bf --- /dev/null +++ b/src/vstack/utils.py @@ -0,0 +1,20 @@ +"""Generic utility functions for vstack.""" + +from __future__ import annotations + +import hashlib + + +def content_hash(content: str) -> str: + """Return a stable SHA-256 checksum for rendered artifact content.""" + return hashlib.sha256(content.encode("utf-8")).hexdigest() + + +def hash_with_algorithm(content: str, algorithm: str) -> str: + """Return a checksum for *content* using the requested algorithm.""" + normalized = algorithm.lower() + if normalized == "sha256": + return hashlib.sha256(content.encode("utf-8")).hexdigest() + if normalized == "md5": + return hashlib.md5(content.encode("utf-8"), usedforsecurity=False).hexdigest() + raise ValueError(f"Unsupported checksum algorithm: {algorithm}") diff --git a/tests/vstack/agents/test_generation.py b/tests/vstack/agents/test_generation.py index cb93d29..4a5b70d 100644 --- a/tests/vstack/agents/test_generation.py +++ b/tests/vstack/agents/test_generation.py @@ -44,7 +44,7 @@ def test_architect_agent_includes_model_and_handoffs(self, tmp_path: Path) -> No assert out.exists() content = out.read_text(encoding="utf-8") - parsed = FrontmatterParser.parse(content) + parsed = FrontmatterParser().parse(content) assert parsed.metadata.get("name") == "architect" assert parsed.metadata.get("model") == [ diff --git a/tests/vstack/agents/test_role_wiring.py b/tests/vstack/agents/test_role_wiring.py index eae808e..d7eb6fc 100644 --- a/tests/vstack/agents/test_role_wiring.py +++ b/tests/vstack/agents/test_role_wiring.py @@ -73,7 +73,7 @@ def test_all_role_handoff_targets_are_known_roles() -> None: if not agent_file.exists(): continue text = agent_file.read_text(encoding="utf-8") - parsed = FrontmatterParser.parse(text) + parsed = FrontmatterParser().parse(text) handoffs = parsed.metadata.get("handoffs") or [] for handoff in handoffs: target = handoff.get("agent") diff --git a/tests/vstack/cli/test_helpers.py b/tests/vstack/cli/test_helpers.py index 749ccfe..524e2e9 100644 --- a/tests/vstack/cli/test_helpers.py +++ b/tests/vstack/cli/test_helpers.py @@ -2,7 +2,7 @@ from __future__ import annotations -from vstack.cli.helpers import normalize_targeted_names +from vstack.cli.base import BaseCommand class TestNormalizeTargetedNames: @@ -10,26 +10,32 @@ class TestNormalizeTargetedNames: def test_returns_empty_set_for_none(self) -> None: """None input produces an empty set.""" - assert normalize_targeted_names(None) == set() + assert BaseCommand._normalize_targeted_names(None) == set() def test_returns_empty_set_for_empty_list(self) -> None: """Empty list produces an empty set.""" - assert normalize_targeted_names([]) == set() + assert BaseCommand._normalize_targeted_names([]) == set() def test_strips_whitespace(self) -> None: """Names with surrounding whitespace are stripped.""" - assert normalize_targeted_names([" vision ", " debug "]) == {"vision", "debug"} + assert BaseCommand._normalize_targeted_names([" vision ", " debug "]) == { + "vision", + "debug", + } def test_deduplicates_names(self) -> None: """Duplicate names collapse into a single entry.""" - assert normalize_targeted_names(["vision", "vision", "debug"]) == {"vision", "debug"} + assert BaseCommand._normalize_targeted_names(["vision", "vision", "debug"]) == { + "vision", + "debug", + } def test_filters_blank_strings(self) -> None: """Blank strings (empty or whitespace-only) are excluded.""" - assert normalize_targeted_names([" ", "", "debug"]) == {"debug"} + assert BaseCommand._normalize_targeted_names([" ", "", "debug"]) == {"debug"} def test_returns_set_of_names(self) -> None: """Result is always a set.""" - result = normalize_targeted_names(["a", "b"]) + result = BaseCommand._normalize_targeted_names(["a", "b"]) assert isinstance(result, set) assert result == {"a", "b"} diff --git a/tests/vstack/cli/test_interface.py b/tests/vstack/cli/test_interface.py index 34d14cd..d5650bb 100644 --- a/tests/vstack/cli/test_interface.py +++ b/tests/vstack/cli/test_interface.py @@ -80,8 +80,9 @@ def test_run_dispatches_validate_without_resolving_target(self, monkeypatch, tmp command = _Command(exit_code=5) monkeypatch.setattr( - "vstack.cli.interface.build_command_registry", - lambda service: {"validate": command}, + CommandLineInterface, + "_build_command_registry", + lambda self, service: {"validate": command}, ) interface = CommandLineInterface( @@ -103,8 +104,9 @@ def test_run_dispatches_install_with_global_default_types(self, monkeypatch, tmp command = _Command(exit_code=9) monkeypatch.setattr( - "vstack.cli.interface.build_command_registry", - lambda service: {"install": command}, + CommandLineInterface, + "_build_command_registry", + lambda self, service: {"install": command}, ) interface = CommandLineInterface( @@ -237,8 +239,9 @@ def __init__( captured.append(artifacts_root) monkeypatch.setattr( - "vstack.cli.interface.build_command_registry", - lambda service: {"install": command}, + CommandLineInterface, + "_build_command_registry", + lambda self, service: {"install": command}, ) interface = CommandLineInterface( @@ -484,8 +487,9 @@ def test_run_removes_excluded_type_from_only(self, monkeypatch, tmp_path: Path) command = _Command(exit_code=0) monkeypatch.setattr( - "vstack.cli.interface.build_command_registry", - lambda service: {"install": command}, + CommandLineInterface, + "_build_command_registry", + lambda self, service: {"install": command}, ) interface = CommandLineInterface( parser_cls=cast(Any, lambda: parser), @@ -513,8 +517,9 @@ def test_run_passes_excluded_names_to_context(self, monkeypatch, tmp_path: Path) command = _Command(exit_code=0) monkeypatch.setattr( - "vstack.cli.interface.build_command_registry", - lambda service: {"install": command}, + CommandLineInterface, + "_build_command_registry", + lambda self, service: {"install": command}, ) interface = CommandLineInterface( parser_cls=cast(Any, lambda: parser), diff --git a/tests/vstack/cli/test_registry.py b/tests/vstack/cli/test_registry.py index 07fde25..f32feff 100644 --- a/tests/vstack/cli/test_registry.py +++ b/tests/vstack/cli/test_registry.py @@ -1,4 +1,4 @@ -"""Tests for build_command_registry catalog factory dispatch.""" +"""Tests for CommandLineInterface command registry construction.""" from __future__ import annotations @@ -8,7 +8,7 @@ import pytest from vstack.cli.base import BaseCommand, CommandContext -from vstack.cli.registry import build_command_registry +from vstack.cli.interface import CommandLineInterface from vstack.cli.service import CommandService @@ -21,9 +21,9 @@ def run(self, *, context: CommandContext) -> int: class TestBuildCommandRegistry: - """Test cases for build_command_registry.""" + """Test cases for CommandLineInterface._build_command_registry.""" - def test_uses_catalog_factories(self, monkeypatch: pytest.MonkeyPatch) -> None: + def test_uses_catalog_factories(self, monkeypatch: pytest.MonkeyPatch, tmp_path) -> None: """Registry construction delegates to command_factory for each catalog entry.""" created: list[str] = [] @@ -35,22 +35,24 @@ def _make(_service: object) -> _FakeCommand: return _make monkeypatch.setattr( - "vstack.cli.registry.COMMAND_CATALOG", + "vstack.cli.interface.COMMAND_CATALOG", { "alpha": SimpleNamespace(command_factory=_factory("alpha")), "beta": SimpleNamespace(command_factory=_factory("beta")), }, ) - registry = build_command_registry(service=cast(CommandService, object())) + cli = CommandLineInterface(templates_root=tmp_path) + registry = cli._build_command_registry(service=cast(CommandService, object())) assert set(registry.keys()) == {"alpha", "beta"} assert created == ["alpha", "beta"] - def test_returns_all_catalog_commands(self) -> None: + def test_returns_all_catalog_commands(self, tmp_path) -> None: """Default registry contains one entry per COMMAND_CATALOG key.""" from vstack.cli.catalog import COMMAND_CATALOG from vstack.constants import TEMPLATES_ROOT svc = CommandService(templates_root=TEMPLATES_ROOT) - registry = build_command_registry(service=svc) + cli = CommandLineInterface(templates_root=tmp_path) + registry = cli._build_command_registry(service=svc) assert set(registry.keys()) == set(COMMAND_CATALOG.keys()) diff --git a/tests/vstack/frontmatter/test_parser.py b/tests/vstack/frontmatter/test_parser.py index 8dd1d66..512da36 100644 --- a/tests/vstack/frontmatter/test_parser.py +++ b/tests/vstack/frontmatter/test_parser.py @@ -32,72 +32,74 @@ def test_bool(self) -> None: class TestFrontmatterParser: """Test cases for FrontmatterParser.""" + def setup_method(self) -> None: + """Create a shared parser instance for all tests.""" + self.parser = FrontmatterParser() + def test_parse_frontmatter_string_value(self) -> None: """Test that parse frontmatter string value.""" - result = FrontmatterParser.parse("---\nname: vision\nversion: 1.0.0\n---\nbody") + result = self.parser.parse("---\nname: vision\nversion: 1.0.0\n---\nbody") assert result.metadata["name"] == "vision" assert result.content == "body" def test_parse_frontmatter_inline_list(self) -> None: """Test that parse frontmatter inline list.""" - meta = FrontmatterParser.parse("---\naliases: [foo, bar]\n---\n").metadata + meta = self.parser.parse("---\naliases: [foo, bar]\n---\n").metadata assert meta["aliases"] == ["foo", "bar"] def test_parse_frontmatter_block_list(self) -> None: """Test that parse frontmatter block list.""" - meta = FrontmatterParser.parse("---\ntools:\n - read\n - edit\n---\n").metadata + meta = self.parser.parse("---\ntools:\n - read\n - edit\n---\n").metadata assert meta["tools"] == ["read", "edit"] def test_parse_frontmatter_block_scalar(self) -> None: """Test that parse frontmatter block scalar.""" - meta = FrontmatterParser.parse( - "---\ndescription: |\n line one\n line two\n---\n" - ).metadata + meta = self.parser.parse("---\ndescription: |\n line one\n line two\n---\n").metadata assert "line one" in meta["description"] def test_parse_no_frontmatter(self) -> None: """Test that parse no frontmatter.""" - result = FrontmatterParser.parse("body-only") + result = self.parser.parse("body-only") assert result.metadata == {} assert result.content == "body-only" def test_parse_yaml_empty_input_returns_empty_dict(self) -> None: """Empty input and non-mapping YAML values return an empty dict.""" - assert FrontmatterParser.parse_yaml("") == {} - assert FrontmatterParser.parse_yaml("just a scalar") == {} - assert FrontmatterParser.parse_yaml("- a\n- b\n") == {} + assert self.parser.parse_yaml("") == {} + assert self.parser.parse_yaml("just a scalar") == {} + assert self.parser.parse_yaml("- a\n- b\n") == {} def test_parse_yaml_raw_block(self) -> None: """Raw block value is parsed as a nested dict by PyYAML.""" - meta = FrontmatterParser.parse_yaml("mcp-servers:\n srv:\n command: cmd\n") + meta = self.parser.parse_yaml("mcp-servers:\n srv:\n command: cmd\n") assert isinstance(meta["mcp-servers"], dict) assert meta["mcp-servers"]["srv"]["command"] == "cmd" def test_parse_yaml_ignores_comments_and_handles_object_list_continuation(self) -> None: """Test that parse yaml ignores comments and handles object list continuation.""" raw = "# comment\nhandoffs:\n - label: A\n prompt: hi\n" - meta = FrontmatterParser.parse_yaml(raw) + meta = self.parser.parse_yaml(raw) assert isinstance(meta["handoffs"], list) assert meta["handoffs"][0]["prompt"] == "hi" def test_parse_yaml_raw_block_closed_by_next_key(self) -> None: """Nested mapping value and subsequent sibling key are both parsed correctly.""" raw = "mcp-servers:\n srv:\n type: local\nname: x\n" - meta = FrontmatterParser.parse_yaml(raw) + meta = self.parser.parse_yaml(raw) assert meta["mcp-servers"]["srv"]["type"] == "local" assert meta["name"] == "x" def test_parse_yaml_block_scalar_closed_by_next_key(self) -> None: """Test that parse yaml block scalar closed by next key.""" raw = "description: |\n line one\nname: tool\n" - meta = FrontmatterParser.parse_yaml(raw) + meta = self.parser.parse_yaml(raw) assert "line one" in meta["description"] assert meta["name"] == "tool" def test_parse_yaml_folded_scalar_closed_by_next_key(self) -> None: """Test that parse yaml folded scalar closed by next key.""" raw = "description: >\n line one\n line two\nname: tool\n" - meta = FrontmatterParser.parse_yaml(raw) + meta = self.parser.parse_yaml(raw) assert "line one" in meta["description"] assert "line two" in meta["description"] assert meta["name"] == "tool" @@ -112,7 +114,7 @@ def test_parse_yaml_object_list_block_scalar_value(self) -> None: " Line two\n" " send: false\n" ) - meta = FrontmatterParser.parse_yaml(raw) + meta = self.parser.parse_yaml(raw) assert isinstance(meta["handoffs"], list) assert "Line one" in meta["handoffs"][0]["prompt"] assert "Line two" in meta["handoffs"][0]["prompt"] @@ -121,13 +123,13 @@ def test_parse_yaml_object_list_block_scalar_value(self) -> None: def test_parse_yaml_wildcard_list_item(self) -> None: """Bare ``*`` list items (VS Code wildcard) are pre-processed so PyYAML parses them as strings.""" raw = "agents:\n - '*'\n - architect\n" - meta = FrontmatterParser.parse_yaml(raw) + meta = self.parser.parse_yaml(raw) assert meta["agents"] == ["*", "architect"] def test_parse_yaml_unquoted_wildcard_list_item(self) -> None: """Unquoted ``- *`` in existing generated files is pre-processed before PyYAML.""" raw = "agents:\n - *\n - architect\n" - meta = FrontmatterParser.parse_yaml(raw) + meta = self.parser.parse_yaml(raw) assert meta["agents"] == ["*", "architect"] def test_parse_yaml_object_list_nested_block_dict(self) -> None: @@ -140,7 +142,7 @@ def test_parse_yaml_object_list_nested_block_dict(self) -> None: " prompt: Architecture done.\n" " agent: designer\n" ) - meta = FrontmatterParser.parse_yaml(raw) + meta = self.parser.parse_yaml(raw) assert isinstance(meta["stages"], list) stage = meta["stages"][0] assert stage["role"] == "architect" @@ -162,7 +164,7 @@ def test_parse_yaml_object_list_nested_block_with_block_scalar(self) -> None: " Line two\n" " other: value\n" ) - meta = FrontmatterParser.parse_yaml(raw) + meta = self.parser.parse_yaml(raw) stage = meta["stages"][0] # PyYAML parses the nested mapping directly as a dict with the folded scalar resolved assert isinstance(stage["handoffs"], dict) @@ -174,6 +176,6 @@ def test_parse_yaml_object_list_nested_block_with_block_scalar(self) -> None: def test_parse_yaml_object_list_empty_nested_block_is_none(self) -> None: """An empty-value nested key in an object-list item is None (PyYAML null).""" raw = "stages:\n - role: release\n gate: required\n handoffs:\n" - meta = FrontmatterParser.parse_yaml(raw) + meta = self.parser.parse_yaml(raw) stage = meta["stages"][0] assert stage["handoffs"] is None diff --git a/tests/vstack/skills/test_templates.py b/tests/vstack/skills/test_templates.py index a7c6748..3f76b28 100644 --- a/tests/vstack/skills/test_templates.py +++ b/tests/vstack/skills/test_templates.py @@ -42,7 +42,7 @@ def test_every_template_has_valid_config_yaml(self) -> None: name = _skill_name(tmpl) cfg = _skill_config(tmpl) assert cfg.exists(), f"{name}: missing config.yaml" - meta = FrontmatterParser.parse_yaml(cfg.read_text(encoding="utf-8")) + meta = FrontmatterParser().parse_yaml(cfg.read_text(encoding="utf-8")) assert meta.get("name"), f"{name}: missing name" assert meta.get("version"), f"{name}: missing version" assert meta.get("description"), f"{name}: missing description" @@ -51,7 +51,7 @@ def test_config_name_matches_directory(self) -> None: """Test that config name matches directory.""" for tmpl in _skill_templates(): name = _skill_name(tmpl) - meta = FrontmatterParser.parse_yaml(_skill_config(tmpl).read_text(encoding="utf-8")) + meta = FrontmatterParser().parse_yaml(_skill_config(tmpl).read_text(encoding="utf-8")) assert str(meta.get("name")) == name From 38d584fc598e1f70e6021b9f85cda99460df6b7d Mon Sep 17 00:00:00 2001 From: Erik Schaareman Date: Sat, 9 May 2026 20:13:17 +0200 Subject: [PATCH 10/25] =?UTF-8?q?docs:=20correct=20runtime=20dependency=20?= =?UTF-8?q?badge=20and=20text=20=E2=80=94=20PyYAML=20is=20required?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README-pypi.md | 4 ++-- README.md | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/README-pypi.md b/README-pypi.md index b3ad40f..787ef28 100644 --- a/README-pypi.md +++ b/README-pypi.md @@ -6,7 +6,7 @@ [![Python version](https://img.shields.io/badge/python-3.11--3.14-0B8A6F)](https://github.com/eschaar/vstack/blob/main/pyproject.toml) [![Verify status](https://img.shields.io/github/actions/workflow/status/eschaar/vstack/verify.yml?label=verify&color=1D6FA5)](https://github.com/eschaar/vstack/actions/workflows/verify.yml) [![Security checks](https://img.shields.io/github/actions/workflow/status/eschaar/vstack/security.yml?label=security&color=B15E00)](https://github.com/eschaar/vstack/actions/workflows/security.yml) -[![Runtime: stdlib only](https://img.shields.io/badge/runtime-stdlib%20only-5B6C8F)](https://github.com/eschaar/vstack/blob/main/pyproject.toml) +[![Runtime: PyYAML](https://img.shields.io/badge/runtime-PyYAML-5B6C8F)](https://github.com/eschaar/vstack/blob/main/pyproject.toml) [![License: MIT](https://img.shields.io/github/license/eschaar/vstack?color=5F7A1F)](https://github.com/eschaar/vstack/blob/main/LICENSE) [![GitHub Discussions](https://img.shields.io/badge/discussions-ask%20%26%20share-blueviolet?logo=github)](https://github.com/eschaar/vstack/discussions) @@ -27,7 +27,7 @@ It provides a fixed role model for end-to-end software delivery: `product`, `arc - Fixed role model: `product`, `architect`, `designer`, `engineer`, `tester`, `release` - Template-driven install model from `src/vstack/_templates/` - Backend-first verification, security, and release discipline -- Standard-library-only runtime dependencies +- One runtime dependency: PyYAML ## Building blocks diff --git a/README.md b/README.md index af9f74d..32e3182 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ [![Python version](https://img.shields.io/badge/python-3.11--3.14-0B8A6F "Supported Python versions")](pyproject.toml) [![Verify status](https://img.shields.io/github/actions/workflow/status/eschaar/vstack/verify.yml?label=verify&color=1D6FA5 "Build and test status")](https://github.com/eschaar/vstack/actions/workflows/verify.yml) [![Security checks](https://img.shields.io/github/actions/workflow/status/eschaar/vstack/security.yml?label=security&color=B15E00 "Security workflow status")](https://github.com/eschaar/vstack/actions/workflows/security.yml) -[![Runtime: stdlib only](https://img.shields.io/badge/runtime-stdlib%20only-5B6C8F "No runtime dependencies")](pyproject.toml) +[![Runtime: PyYAML](https://img.shields.io/badge/runtime-PyYAML-5B6C8F "One runtime dependency: PyYAML")](pyproject.toml) [![License: MIT](https://img.shields.io/github/license/eschaar/vstack?color=5F7A1F "Project license")](LICENSE) [![GitHub Discussions](https://img.shields.io/badge/discussions-ask%20%26%20share-blueviolet?logo=github "GitHub Discussions")](https://github.com/eschaar/vstack/discussions) @@ -35,7 +35,7 @@ ______________________________________________________________________ - Fixed role model with explicit ownership boundaries - Template-driven install model from `src/vstack/_templates/` - Backend-first verification, security, and release discipline -- No runtime dependencies beyond the Python standard library +- One runtime dependency: [PyYAML](https://pypi.org/project/PyYAML/) - Works at project scope or globally in the VS Code user profile ______________________________________________________________________ From e82069ccd00117f972c4f7de159bda9903d97c11 Mon Sep 17 00:00:00 2001 From: Erik Schaareman Date: Sat, 9 May 2026 20:20:28 +0200 Subject: [PATCH 11/25] chore: expand PyPI keywords and classifiers for discoverability --- pyproject.toml | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index f2a948d..9feb5f0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -19,8 +19,19 @@ keywords = [ "api", "workflow-automation", "release-engineering", + "agentic-workflow", + "agentic-ai", + "ai-agent", + "llm", + "llm-agents", + "prompt-engineering", + "code-generation", + "scaffolding", + "cli", + "agents", ] classifiers = [ + "Development Status :: 4 - Beta", "License :: OSI Approved :: MIT License", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.11", @@ -30,7 +41,9 @@ classifiers = [ "Environment :: Console", "Intended Audience :: Developers", "Topic :: Software Development :: Build Tools", + "Topic :: Software Development :: Code Generators", "Topic :: Software Development :: Libraries :: Python Modules", + "Topic :: Utilities", ] [project.urls] From f4cf9ad090b2aced39fb34e3eb8075561b074d64 Mon Sep 17 00:00:00 2001 From: Erik Schaareman Date: Sat, 9 May 2026 21:10:58 +0200 Subject: [PATCH 12/25] chore(docs): apply mdformat to adr-026 and migrations readme --- .../adr/026-docs-artifact-migration-policy.md | 171 ++++++ pyproject.toml | 1 + src/vstack/_migrations/README.md | 27 + src/vstack/_migrations/v2_to_v3.yaml | 39 ++ src/vstack/cli/catalog.py | 10 + src/vstack/cli/migrate.py | 232 ++++++++ src/vstack/cli/parser.py | 31 + src/vstack/constants.py | 1 + tests/vstack/cli/test_catalog.py | 1 + tests/vstack/cli/test_migrate.py | 559 ++++++++++++++++++ tests/vstack/cli/test_parser.py | 1 + 11 files changed, 1073 insertions(+) create mode 100644 docs/architecture/adr/026-docs-artifact-migration-policy.md create mode 100644 src/vstack/_migrations/README.md create mode 100644 src/vstack/_migrations/v2_to_v3.yaml create mode 100644 src/vstack/cli/migrate.py create mode 100644 tests/vstack/cli/test_migrate.py diff --git a/docs/architecture/adr/026-docs-artifact-migration-policy.md b/docs/architecture/adr/026-docs-artifact-migration-policy.md new file mode 100644 index 0000000..11ede00 --- /dev/null +++ b/docs/architecture/adr/026-docs-artifact-migration-policy.md @@ -0,0 +1,171 @@ +# ADR-026: Docs Artifact Path Stability and Migration Policy + +> Maintained by: **architect** role + +**date:** 2026-05-09\ +**status:** accepted\ +**depends on:** ADR-014 (manifest schema versioning), ADR-019 (vstack project directory), ADR-021 (config-driven artifact paths) + +## context + +vstack manages two distinct classes of output artifacts: + +1. **`.github/` artifacts** — skills, agents, instructions, and prompts generated + from templates. These paths are constrained by VS Code and GitHub Copilot conventions + (e.g. `.github/skills//SKILL.md`). They are tracked in the manifest + (`.vstack/vstack.json`) with checksums and can be upgraded, reinstalled, or removed + via CLI commands. Path changes here are infrequent and externally motivated. + +1. **`docs/` artifacts** — living documentation produced and maintained by agent roles + (architect, designer, product, tester, release). Examples: `docs/architecture/overview.md`, + `docs/product/requirements.md`, `docs/reports/security-report.md`. These are **not** + tracked in the manifest; they are owned and continuously updated by agents as project + outputs, not by vstack as generated files. + +ADR-021 established that agent `config.yaml` files are the machine-readable source of truth +for which paths each agent writes to (`artifacts.dir`, `artifacts.output`). Skill template +prose duplicates these paths as LLM guidance (e.g. "write to `docs/architecture/overview.md`") +— this is intentional, acceptable duplication. The prose is informational; the config is +authoritative. + +### The migration problem + +When `docs/` paths change across vstack versions (e.g. a subdirectory is renamed or a file +moves), no automated mechanism exists to relocate the files. Because they are untracked, +`vstack status` does not report them as stale. Because they are agent-owned content, not +generated content, a simple reinstall does not help. The user is left with orphaned files +at old paths and agents writing to new paths, resulting in divergent documentation. + +Two related questions arise: + +- **When may docs paths change?** What stability commitment do consumers of vstack receive? +- **When they do change, what is the upgrade path?** How does a user migrate their + existing files? + +## decision + +### 1. Path stability commitment + +Docs artifact paths — the `artifacts.dir` value and the glob patterns in `artifacts.output` +for each agent — are **stable within a minor version** and **may break only on a major version +boundary** (semver major bump, e.g. v2→v3, v3→v4). + +This commitment applies to: + +- Agent `config.yaml` `artifacts.dir` values +- Agent `config.yaml` `artifacts.output` paths +- Skill template prose references to the same paths (kept in sync as a consequence) + +It does **not** apply to: + +- `.github/` artifact paths, which follow VS Code / Copilot conventions (covered by ADR-002) +- Project-specific paths the user configures via `artifacts.root` or workflow overrides + +### 2. Agent config.yaml is the source of truth; skill prose is informational + +Skill templates reference docs paths in their markdown bodies (e.g. "Primary deliverable: +`docs/architecture/overview.md`"). This duplication is accepted as a usability aid for LLM +agents. It is not a second source of truth. When a path changes: + +- Update `artifacts.dir` / `artifacts.output` in the relevant agent `config.yaml` — this is + the authoritative change. +- Update all skill template prose references to the same paths for consistency. +- Never change skill prose without also updating the corresponding agent config. + +### 3. Manifest upgrade handles `.github/` relocations; `vstack migrate` handles `docs/` + +Two migration mechanisms exist, each scoped to its artifact class: + +| Artifact class | Location tracked | Migration command | +| ---------------------------------------------------------- | --------------------- | ---------------------------- | +| `.github/` (skills, agents, instructions, prompts) | `.vstack/vstack.json` | `vstack manifest upgrade` | +| `docs/` (architecture, design, product, reports, releases) | Not tracked | `vstack migrate` (see below) | + +`vstack manifest upgrade` already handles schema migrations and file relocation for `.github/` +artifacts (ADR-017). Docs artifact migration is a separate concern with a separate command. + +### 4. `vstack migrate` — deferred implementation, defined convention now + +The `vstack migrate` command is **not implemented** in this decision. The convention for +how it will work is defined here so that migration records can be authored now and executed +when the command ships. + +Migration records live in `src/vstack/_migrations/` within the package source. Each file +covers one major version transition: + +``` +src/vstack/_migrations/ +└── v3_to_v4.yaml # applied when upgrading from any 3.x to 4.x +``` + +A migration record lists path moves by artifact class: + +```yaml +from_version: "3.x" +to_version: "4.0" +moves: + - old: docs/reports/security-report.md + new: docs/security/security-report.md + type: docs + notes: > + tester role artifact dir changed from reports/ to security/ to align + with expanded scope. Files at the old path are safe to remove after + migration. +``` + +When `vstack migrate` ships, it will: + +1. Read the applicable migration record for the installed-to-installed version range. +1. For each `type: docs` move: check if the old path exists; if so, move it to the + new path (creating parent directories as needed), and report what was moved. +1. Print a summary and exit non-zero if any move failed. +1. Support `--dry-run` to preview moves without writing. + +Until the command ships, the migration record files serve as the canonical reference for +what a user must do manually when upgrading across major versions. `CHANGELOG.md` entries +for major releases must include a "Migration" section that lists the same moves in prose. + +### 5. Skill prose references are resolved at LLM runtime, not install time + +No variable substitution is applied to skill template prose at install time (e.g. +`{{artifacts_root}}/architecture/overview.md`). The agent config `artifacts.dir` is +the machine-readable source; prose is a human-readable aid for the LLM. If a user +sets a custom `artifacts.root` in `.vstack/config.yaml`, the agent's generated +`.agent.md` body will contain the resolved root because the agent template uses the +config value at generation time. Skill markdown bodies remain as authored. + +## alternatives considered + +### A — Track `docs/` files in the manifest + +Add docs artifacts to `.vstack/vstack.json` so `vstack status` and `vstack manifest upgrade` +can detect and migrate them. Rejected because docs files are agent-owned content that changes +continuously; tracking them like generated files would produce constant checksum drift and +requires agents to update the manifest on every write — which is not feasible in the +current execution model. + +### B — Variable substitution in skill template prose + +Replace hardcoded paths in skill markdown (e.g. `docs/architecture/overview.md`) with +install-time variables (`{{artifacts_root}}/architecture/overview.md`). Rejected because: +the agent config is already the machine-readable authority; prose is LLM guidance only; +adding a substitution pass increases template complexity for marginal gain. The LLM reads +the agent's own `.agent.md` (which does resolve `artifacts_root`) before executing a skill — +the agent already has the correct context. + +### C — Stability promise only, no migration tooling + +Commit to path stability without defining a `vstack migrate` command. Rejected because +stability promises are still best-effort — external reasons (tooling integration, naming +mistakes, file reorganisations) can require a path change within a major cycle. A defined +migration convention ensures that when a break does occur, the upgrade path is documented +and eventually automatable. + +## impact on the Option B pipeline + +The future `planner` orchestrator (ADR-004) resolves agent artifact paths from `config.yaml` +`artifacts.dir` and `artifacts.output` to hand off context between stages. This ADR's +stability commitment means the orchestrator can hard-code path resolution logic against +those config fields for the lifetime of a major version. The `vstack migrate` convention +ensures that when the orchestrator's expected paths change across majors, a machine-readable +migration record is available to update the planner's context without manual intervention. diff --git a/pyproject.toml b/pyproject.toml index 9feb5f0..f43aa58 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -67,6 +67,7 @@ vstack = "vstack:main" packages = [{include = "vstack", from = "src"}] include = [ { path = "src/vstack/_templates", format = ["sdist", "wheel"] }, + { path = "src/vstack/_migrations", format = ["sdist", "wheel"] }, ] # Keep the placeholder version last because the plugin may remove/re-add it in # PEP 621 mode, which otherwise causes noisy key reordering. diff --git a/src/vstack/_migrations/README.md b/src/vstack/_migrations/README.md new file mode 100644 index 0000000..7f4db95 --- /dev/null +++ b/src/vstack/_migrations/README.md @@ -0,0 +1,27 @@ +# vstack migration records + +This directory contains machine-readable migration records for `docs/` artifact path +changes across major vstack version boundaries. + +Each file covers one major version transition and is named `v{M}_to_v{N}.yaml` where +`M` is the source major version and `N` is the target major version. + +These files are read by `vstack migrate` (not yet implemented — see ADR-026) to relocate +agent-owned docs files when their paths change between major versions. + +Until `vstack migrate` ships, use the moves listed here as the canonical reference for +manual migration steps. `CHANGELOG.md` for each major release must include a "Migration" +section that restates these moves in prose. + +## Schema + +```yaml +from_version: "3.x" # semver range for the source version +to_version: "4.0" # first minor of the target major +moves: + - old: docs/path/old.md # path relative to project root + new: docs/path/new.md # path relative to project root + type: docs # always "docs" for this directory + notes: > # optional: human-readable explanation + Why this path changed and what to do with the old file after migration. +``` diff --git a/src/vstack/_migrations/v2_to_v3.yaml b/src/vstack/_migrations/v2_to_v3.yaml new file mode 100644 index 0000000..7c408b8 --- /dev/null +++ b/src/vstack/_migrations/v2_to_v3.yaml @@ -0,0 +1,39 @@ +from_version: "2.x" +to_version: "3.0" +moves: + - old: docs/architecture/architecture.md + new: docs/architecture/overview.md + type: docs + notes: > + architect role primary output renamed from architecture.md to overview.md + in v3 to align with the generic overview.md convention used by all roles. + Content is the same — only the filename changed. + + - old: docs/design/design.md + new: docs/design/overview.md + type: docs + notes: > + designer role primary output renamed from design.md to overview.md in v3 + to align with the generic overview.md convention used by all roles. + Content is the same — only the filename changed. + + - old: docs/test-report.md + new: docs/reports/test-report.md + type: docs + notes: > + tester role reports moved from the docs/ root into docs/reports/ in v3. + This groups all tester outputs under a single subdirectory. + + - old: docs/security-report.md + new: docs/reports/security-report.md + type: docs + notes: > + tester role reports moved from the docs/ root into docs/reports/ in v3. + This groups all tester outputs under a single subdirectory. + + - old: docs/performance-baseline.md + new: docs/reports/performance-baseline.md + type: docs + notes: > + tester role reports moved from the docs/ root into docs/reports/ in v3. + This groups all tester outputs under a single subdirectory. diff --git a/src/vstack/cli/catalog.py b/src/vstack/cli/catalog.py index d09cfd4..cd2f84d 100644 --- a/src/vstack/cli/catalog.py +++ b/src/vstack/cli/catalog.py @@ -10,6 +10,7 @@ from vstack.cli.init import InitCommand from vstack.cli.install import InstallCommand from vstack.cli.manifest import ManifestCommand +from vstack.cli.migrate import MigrateCommand from vstack.cli.status import StatusCommand from vstack.cli.uninstall import UninstallCommand from vstack.cli.validate import ValidateCommand @@ -53,6 +54,7 @@ class ManifestSubcommandConfig: "install", "init", "uninstall", + "migrate", ) @@ -125,6 +127,14 @@ class ManifestSubcommandConfig: scope_help="Uninstall from /.github/", only_help="Uninstall only these artifact types, e.g. --only skill agent", ), + "migrate": TopLevelCommandConfig( + command_factory=MigrateCommand, + help_text="Migrate docs artifact paths between major vstack versions", + requires_install_dir=False, + resolve_only_for_scope=False, + include_scope_group=False, + include_only_option=False, + ), } diff --git a/src/vstack/cli/migrate.py b/src/vstack/cli/migrate.py new file mode 100644 index 0000000..390b29b --- /dev/null +++ b/src/vstack/cli/migrate.py @@ -0,0 +1,232 @@ +"""Migrate command — apply docs artifact path migrations between major versions.""" + +from __future__ import annotations + +import shutil +import sys +from pathlib import Path +from typing import TYPE_CHECKING + +import yaml + +from vstack.cli.base import BaseCommand, CommandContext +from vstack.cli.constants import Colors +from vstack.constants import ARTIFACTS_DOCS_ROOT, MIGRATIONS_ROOT, VERSION, VSTACK_DIR_NAME +from vstack.frontmatter import FrontmatterParser +from vstack.manifest.store import ManifestFile + +if TYPE_CHECKING: + from vstack.cli.service import CommandService + + +class MigrateCommand(BaseCommand): + """Apply docs artifact path migrations between major vstack versions. + + Reads migration records from the package ``_migrations/`` directory and + moves files at old docs paths to their new locations. Only files that + exist at the old path are moved; absent files are silently skipped. + """ + + def __init__(self, service: CommandService) -> None: + self._service = service + + @staticmethod + def _resolve_project_root(args: object) -> Path: + """Resolve the project root from ``--target`` or the current directory.""" + target = getattr(args, "target", None) + if target: + return Path(target).expanduser().resolve() + return Path.cwd() + + @staticmethod + def _detect_installed_major(project_root: Path) -> int | None: + """Read ``vstack_version`` from the manifest and return its major number. + + Returns ``None`` when the manifest is absent or the version cannot be + parsed as a semver-like string. + """ + manifest_path = project_root / VSTACK_DIR_NAME / "vstack.json" + if not manifest_path.exists(): + return None + manifest_file = ManifestFile(project_root / VSTACK_DIR_NAME) + try: + manifest = manifest_file.read() + except Exception: # noqa: BLE001 + return None + if manifest is None: + return None + version = manifest.vstack_version + if not version: + return None + try: + return int(version.split(".")[0]) + except (ValueError, IndexError): + return None + + @staticmethod + def _load_migration_record(from_major: int, to_major: int) -> list[dict] | None: + """Load moves for a single *from_major* → *to_major* step. + + Returns the ``moves`` list from the YAML record, or ``None`` when no + record file exists for this step. + """ + record_path = MIGRATIONS_ROOT / f"v{from_major}_to_v{to_major}.yaml" + if not record_path.exists(): + return None + raw = yaml.safe_load(record_path.read_text(encoding="utf-8")) + if not isinstance(raw, dict): + return None + moves = raw.get("moves", []) + if not isinstance(moves, list): + return None + return [m for m in moves if isinstance(m, dict)] + + @staticmethod + def _resolve_new_path(new_rel: str, artifacts_root: str) -> str: + """Substitute the default docs root in *new_rel* with *artifacts_root*. + + Migration records store new paths using the default ``docs/`` root. + When the project uses a custom root, the first path component is + replaced so the file lands under the configured directory. + """ + if artifacts_root == ARTIFACTS_DOCS_ROOT: + return new_rel + parts = Path(new_rel).parts + if parts and parts[0] == ARTIFACTS_DOCS_ROOT: + return str(Path(artifacts_root).joinpath(*parts[1:])) + return new_rel + + @staticmethod + def _read_artifacts_root(project_root: Path) -> str: + """Read ``artifacts.root`` from ``.vstack/config.yaml``. + + Returns :data:`~vstack.constants.ARTIFACTS_DOCS_ROOT` when the config + is absent or the key is not set. + """ + config_path = project_root / VSTACK_DIR_NAME / "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", "") + 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 + + @staticmethod + def _apply_moves( + *, + moves: list[dict], + project_root: Path, + artifacts_root: str, + dry_run: bool, + colors: type[Colors], + ) -> tuple[int, int]: + """Apply *moves* relative to *project_root*. + + Returns a ``(moved, skipped)`` count pair. + """ + moved = 0 + skipped = 0 + prefix = f"{colors.DIM}[dry-run]{colors.RESET} " if dry_run else "" + + for move in moves: + old_rel = move.get("old", "") + new_rel_raw = move.get("new", "") + if not old_rel or not new_rel_raw: + continue + + new_rel = MigrateCommand._resolve_new_path(new_rel_raw, artifacts_root) + old_path = project_root / old_rel + new_path = project_root / new_rel + + if not old_path.exists(): + print(f" {prefix}{colors.DIM}↷ {old_rel} — absent, skipping{colors.RESET}") + skipped += 1 + continue + + if new_path.exists(): + print( + f" {prefix}{colors.YELLOW}↷{colors.RESET} {old_rel}" + f" {colors.DIM}→ {new_rel}" + f" — destination exists, skipping{colors.RESET}" + ) + skipped += 1 + continue + + print( + f" {prefix}{colors.GREEN}→{colors.RESET} " + f"{colors.BOLD}{old_rel}{colors.RESET}" + f" {colors.DIM}→{colors.RESET} {new_rel}" + ) + if not dry_run: + new_path.parent.mkdir(parents=True, exist_ok=True) + shutil.move(str(old_path), str(new_path)) + moved += 1 + + return moved, skipped + + def run(self, *, context: CommandContext) -> int: + """Execute the migrate command and return a process-style status code.""" + args = context.args + colors = Colors + dry_run = getattr(args, "dry_run", False) + project_root = self._resolve_project_root(args) + from_major: int | None = getattr(args, "from_major", None) + to_major: int | None = getattr(args, "to_major", None) + + if from_major is None: + from_major = self._detect_installed_major(project_root) + if from_major is None: + print( + "ERROR: could not detect installed version from manifest. " + "Specify --from explicitly.", + file=sys.stderr, + ) + return 1 + + if to_major is None: + try: + to_major = int(VERSION.split(".")[0]) + except (ValueError, IndexError): + to_major = from_major + 1 + + if from_major >= to_major: + print(f"Nothing to migrate: already at v{to_major}.") + return 0 + + artifacts_root = self._read_artifacts_root(project_root) + + print(f"\n {colors.BOLD}Migrating docs paths v{from_major} → v{to_major}{colors.RESET}") + if dry_run: + print(f" {colors.DIM}(dry-run — no files will be moved){colors.RESET}") + + total_moved = 0 + total_skipped = 0 + + for step_from in range(from_major, to_major): + step_to = step_from + 1 + moves = self._load_migration_record(step_from, step_to) + if moves is None: + print( + f"\n {colors.DIM}v{step_from} → v{step_to}:" + f" no migration record, skipping{colors.RESET}" + ) + continue + print(f"\n {colors.DIM}v{step_from} → v{step_to}{colors.RESET}") + moved, skipped = self._apply_moves( + moves=moves, + project_root=project_root, + artifacts_root=artifacts_root, + dry_run=dry_run, + colors=colors, + ) + total_moved += moved + total_skipped += skipped + + print( + f"\n {colors.BOLD}Done.{colors.RESET} Moved: {total_moved} Skipped: {total_skipped}" + ) + return 0 diff --git a/src/vstack/cli/parser.py b/src/vstack/cli/parser.py index d48eedf..454b9cb 100644 --- a/src/vstack/cli/parser.py +++ b/src/vstack/cli/parser.py @@ -271,6 +271,36 @@ def _add_uninstall_command(self, sub: SubparserFactory) -> None: help="Force uninstall one named artifact without removing every modified file", ) + def _add_migrate_command(self, sub: SubparserFactory) -> None: + """Register the ``migrate`` subcommand.""" + migrate_config = COMMAND_CATALOG["migrate"] + parser = sub.add_parser("migrate", help=migrate_config.help_text) + parser.add_argument( + "--target", + metavar="", + help="Project root directory (default: current working directory)", + ) + parser.add_argument( + "--from", + dest="from_major", + type=int, + metavar="", + help="Source major version (e.g. 2). Detected from manifest when absent.", + ) + parser.add_argument( + "--to", + dest="to_major", + type=int, + metavar="", + help="Target major version (e.g. 3). Defaults to current vstack major.", + ) + parser.add_argument( + "--dry-run", + dest="dry_run", + action="store_true", + help="Show what would be moved without writing files", + ) + def vscode_user_dir(self) -> Path | None: """Return the first detected VS Code user data directory. @@ -339,6 +369,7 @@ def build(self) -> argparse.ArgumentParser: "install": self._add_install_command, "init": self._add_init_command, "uninstall": self._add_uninstall_command, + "migrate": self._add_migrate_command, } for command_name in TOP_LEVEL_COMMAND_ORDER: command_adders[command_name](sub) diff --git a/src/vstack/constants.py b/src/vstack/constants.py index 75da934..08cb53a 100644 --- a/src/vstack/constants.py +++ b/src/vstack/constants.py @@ -13,6 +13,7 @@ _PACKAGE_ROOT = files("vstack") TEMPLATES_ROOT = Path(str(_PACKAGE_ROOT / "_templates")) +MIGRATIONS_ROOT = Path(str(_PACKAGE_ROOT / "_migrations")) # Default root directory for all role artifacts. Individual agent configs specify # only the subdirectory via ``artifacts.dir``; this root is applied at render time. diff --git a/tests/vstack/cli/test_catalog.py b/tests/vstack/cli/test_catalog.py index 20b8bf7..fb6a064 100644 --- a/tests/vstack/cli/test_catalog.py +++ b/tests/vstack/cli/test_catalog.py @@ -28,6 +28,7 @@ def test_catalog_contains_expected_commands(self) -> None: "install", "init", "uninstall", + "migrate", } def test_command_order_matches_catalog_keys(self) -> None: diff --git a/tests/vstack/cli/test_migrate.py b/tests/vstack/cli/test_migrate.py new file mode 100644 index 0000000..59b0e46 --- /dev/null +++ b/tests/vstack/cli/test_migrate.py @@ -0,0 +1,559 @@ +"""Tests for MigrateCommand.""" + +from __future__ import annotations + +from argparse import Namespace +from pathlib import Path +from typing import Any, cast +from unittest.mock import MagicMock, patch + +import pytest +import yaml + +from vstack.cli.base import CommandContext +from vstack.cli.constants import Colors +from vstack.cli.migrate import MigrateCommand +from vstack.cli.service import CommandService +from vstack.constants import ARTIFACTS_DOCS_ROOT + + +def _service() -> CommandService: + return cast(CommandService, MagicMock(spec=CommandService)) + + +def _context(args: Namespace) -> CommandContext: + return CommandContext(args=args, install_dir=None, only=None) + + +class TestResolveProjectRoot: + """_resolve_project_root returns cwd or explicit --target.""" + + def test_returns_cwd_when_no_target( + self, monkeypatch: pytest.MonkeyPatch, tmp_path: Path + ) -> None: + """Uses cwd when --target is absent.""" + monkeypatch.chdir(tmp_path) + args = Namespace(target=None) + assert MigrateCommand._resolve_project_root(args) == tmp_path + + def test_returns_target_when_set(self, tmp_path: Path) -> None: + """Uses the explicit --target path.""" + args = Namespace(target=str(tmp_path)) + assert MigrateCommand._resolve_project_root(args) == tmp_path + + def test_expands_user_in_target(self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None: + """Expands ~ in --target.""" + monkeypatch.setenv("HOME", str(tmp_path)) + args = Namespace(target="~") + assert MigrateCommand._resolve_project_root(args) == tmp_path + + +class TestDetectInstalledMajor: + """_detect_installed_major reads vstack_version from the manifest.""" + + def test_returns_major_from_manifest(self, tmp_path: Path) -> None: + """Parses major version from a valid semver string in the manifest.""" + vstack_dir = tmp_path / ".vstack" + vstack_dir.mkdir() + manifest = { + "manifest_version": 2, + "hash_algorithm": "sha256", + "vstack_version": "2.2.0", + "installed_at": "2026-01-01T00:00:00+00:00", + "artifacts": {}, + } + (vstack_dir / "vstack.json").write_text( + __import__("json").dumps(manifest), encoding="utf-8" + ) + assert MigrateCommand._detect_installed_major(tmp_path) == 2 + + def test_returns_none_when_manifest_absent(self, tmp_path: Path) -> None: + """Returns None when no manifest file exists.""" + assert MigrateCommand._detect_installed_major(tmp_path) is None + + def test_returns_none_when_version_unparseable(self, tmp_path: Path) -> None: + """Returns None when vstack_version cannot be split into a major int.""" + vstack_dir = tmp_path / ".vstack" + vstack_dir.mkdir() + manifest = { + "manifest_version": 2, + "hash_algorithm": "sha256", + "vstack_version": "not-a-version", + "installed_at": "2026-01-01T00:00:00+00:00", + "artifacts": {}, + } + (vstack_dir / "vstack.json").write_text( + __import__("json").dumps(manifest), encoding="utf-8" + ) + assert MigrateCommand._detect_installed_major(tmp_path) is None + + def test_returns_none_when_manifest_read_raises(self, tmp_path: Path) -> None: + """Returns None when ManifestFile.read() raises an unexpected exception.""" + vstack_dir = tmp_path / ".vstack" + vstack_dir.mkdir() + (vstack_dir / "vstack.json").write_text("{}", encoding="utf-8") + with patch("vstack.cli.migrate.ManifestFile") as mock_cls: + mock_cls.return_value.read.side_effect = RuntimeError("boom") + assert MigrateCommand._detect_installed_major(tmp_path) is None + + def test_returns_none_when_manifest_read_returns_none(self, tmp_path: Path) -> None: + """Returns None when ManifestFile.read() returns None (legacy schema).""" + vstack_dir = tmp_path / ".vstack" + vstack_dir.mkdir() + # manifest_version=1 triggers needs_upgrade() → read() returns None + manifest = { + "manifest_version": 1, + "hash_algorithm": "sha256", + "vstack_version": "1.3.6", + "installed_at": "2026-01-01T00:00:00+00:00", + "artifacts": {}, + } + (vstack_dir / "vstack.json").write_text( + __import__("json").dumps(manifest), encoding="utf-8" + ) + assert MigrateCommand._detect_installed_major(tmp_path) is None + + def test_returns_none_when_version_empty(self, tmp_path: Path) -> None: + """Returns None when vstack_version is an empty string.""" + vstack_dir = tmp_path / ".vstack" + vstack_dir.mkdir() + manifest = { + "manifest_version": 2, + "hash_algorithm": "sha256", + "vstack_version": "", + "installed_at": "2026-01-01T00:00:00+00:00", + "artifacts": {}, + } + (vstack_dir / "vstack.json").write_text( + __import__("json").dumps(manifest), encoding="utf-8" + ) + assert MigrateCommand._detect_installed_major(tmp_path) is None + + +class TestLoadMigrationRecord: + """_load_migration_record reads YAML files from the migrations directory.""" + + def test_returns_none_when_file_absent(self, tmp_path: Path) -> None: + """Returns None when no migration record exists for the given versions.""" + with patch("vstack.cli.migrate.MIGRATIONS_ROOT", tmp_path): + assert MigrateCommand._load_migration_record(9, 10) is None + + def test_returns_moves_from_yaml(self, tmp_path: Path) -> None: + """Returns the moves list from a valid YAML record.""" + record = { + "from_version": "1.x", + "to_version": "2.0", + "moves": [ + {"old": "docs/a.md", "new": "docs/b.md", "type": "docs"}, + ], + } + (tmp_path / "v1_to_v2.yaml").write_text(yaml.dump(record), encoding="utf-8") + with patch("vstack.cli.migrate.MIGRATIONS_ROOT", tmp_path): + result = MigrateCommand._load_migration_record(1, 2) + assert result == [{"old": "docs/a.md", "new": "docs/b.md", "type": "docs"}] + + def test_returns_empty_list_when_moves_absent(self, tmp_path: Path) -> None: + """Returns an empty list when the YAML record has no moves key.""" + record = {"from_version": "1.x", "to_version": "2.0"} + (tmp_path / "v1_to_v2.yaml").write_text(yaml.dump(record), encoding="utf-8") + with patch("vstack.cli.migrate.MIGRATIONS_ROOT", tmp_path): + result = MigrateCommand._load_migration_record(1, 2) + assert result == [] + + def test_filters_non_dict_moves(self, tmp_path: Path) -> None: + """Filters out non-dict entries from the moves list.""" + record = { + "from_version": "1.x", + "to_version": "2.0", + "moves": [ + {"old": "docs/a.md", "new": "docs/b.md", "type": "docs"}, + "not-a-dict", + 42, + ], + } + (tmp_path / "v1_to_v2.yaml").write_text(yaml.dump(record), encoding="utf-8") + with patch("vstack.cli.migrate.MIGRATIONS_ROOT", tmp_path): + result = MigrateCommand._load_migration_record(1, 2) + assert result == [{"old": "docs/a.md", "new": "docs/b.md", "type": "docs"}] + + def test_returns_none_when_moves_not_a_list(self, tmp_path: Path) -> None: + """Returns None when moves is not a list.""" + record = {"from_version": "1.x", "to_version": "2.0", "moves": "bad"} + (tmp_path / "v1_to_v2.yaml").write_text(yaml.dump(record), encoding="utf-8") + with patch("vstack.cli.migrate.MIGRATIONS_ROOT", tmp_path): + assert MigrateCommand._load_migration_record(1, 2) is None + + def test_returns_none_when_yaml_not_dict(self, tmp_path: Path) -> None: + """Returns None when the YAML root is not a mapping.""" + (tmp_path / "v1_to_v2.yaml").write_text("- item1\n- item2\n", encoding="utf-8") + with patch("vstack.cli.migrate.MIGRATIONS_ROOT", tmp_path): + assert MigrateCommand._load_migration_record(1, 2) is None + + +class TestResolveNewPath: + """_resolve_new_path substitutes the docs root when configured.""" + + def test_returns_path_unchanged_when_root_matches_default(self) -> None: + """Does not modify the path when the root equals the default.""" + result = MigrateCommand._resolve_new_path( + "docs/architecture/overview.md", ARTIFACTS_DOCS_ROOT + ) + assert result == "docs/architecture/overview.md" + + def test_substitutes_custom_root(self) -> None: + """Replaces the default docs/ prefix with the configured root.""" + result = MigrateCommand._resolve_new_path("docs/architecture/overview.md", "content") + assert result == "content/architecture/overview.md" + + def test_leaves_path_unchanged_when_prefix_does_not_match(self) -> None: + """Does not modify paths whose first component is not the default root.""" + result = MigrateCommand._resolve_new_path("other/architecture/overview.md", "content") + assert result == "other/architecture/overview.md" + + +class TestReadArtifactsRoot: + """_read_artifacts_root reads artifacts.root from config.yaml.""" + + def test_returns_default_when_config_absent(self, tmp_path: Path) -> None: + """Returns the default docs root when no config.yaml exists.""" + assert MigrateCommand._read_artifacts_root(tmp_path) == ARTIFACTS_DOCS_ROOT + + def test_returns_configured_root(self, tmp_path: Path) -> None: + """Returns the custom root from config.yaml.""" + vstack_dir = tmp_path / ".vstack" + vstack_dir.mkdir() + (vstack_dir / "config.yaml").write_text("artifacts:\n root: content\n", encoding="utf-8") + assert MigrateCommand._read_artifacts_root(tmp_path) == "content" + + def test_returns_default_when_artifacts_not_dict(self, tmp_path: Path) -> None: + """Returns default when artifacts key is not a mapping.""" + vstack_dir = tmp_path / ".vstack" + vstack_dir.mkdir() + (vstack_dir / "config.yaml").write_text("artifacts: all\n", encoding="utf-8") + assert MigrateCommand._read_artifacts_root(tmp_path) == ARTIFACTS_DOCS_ROOT + + def test_returns_default_when_root_blank(self, tmp_path: Path) -> None: + """Returns default when artifacts.root is blank.""" + vstack_dir = tmp_path / ".vstack" + vstack_dir.mkdir() + (vstack_dir / "config.yaml").write_text("artifacts:\n root: ''\n", encoding="utf-8") + assert MigrateCommand._read_artifacts_root(tmp_path) == ARTIFACTS_DOCS_ROOT + + +class _FakeColors(Colors): + DIM = "" + RESET = "" + GREEN = "" + YELLOW = "" + BOLD = "" + RED = "" + CYAN = "" + BLUE = "" + + +class TestApplyMoves: + """_apply_moves relocates files and reports results.""" + + def test_moves_file_to_new_path(self, tmp_path: Path) -> None: + """Moves an existing file from old to new path.""" + old = tmp_path / "docs" / "a.md" + old.parent.mkdir(parents=True) + old.write_text("content", encoding="utf-8") + + moves = [{"old": "docs/a.md", "new": "docs/sub/b.md"}] + moved, skipped = MigrateCommand._apply_moves( + moves=moves, + project_root=tmp_path, + artifacts_root="docs", + dry_run=False, + colors=_FakeColors, + ) + assert moved == 1 + assert skipped == 0 + assert not old.exists() + assert (tmp_path / "docs" / "sub" / "b.md").read_text(encoding="utf-8") == "content" + + def test_skips_absent_old_path(self, tmp_path: Path) -> None: + """Counts absent old paths as skipped without error.""" + moves = [{"old": "docs/missing.md", "new": "docs/new.md"}] + moved, skipped = MigrateCommand._apply_moves( + moves=moves, + project_root=tmp_path, + artifacts_root="docs", + dry_run=False, + colors=_FakeColors, + ) + assert moved == 0 + assert skipped == 1 + + def test_skips_when_destination_exists(self, tmp_path: Path) -> None: + """Counts existing destinations as skipped without overwriting.""" + old = tmp_path / "docs" / "a.md" + old.parent.mkdir(parents=True) + old.write_text("old content", encoding="utf-8") + new = tmp_path / "docs" / "b.md" + new.write_text("existing content", encoding="utf-8") + + moves = [{"old": "docs/a.md", "new": "docs/b.md"}] + moved, skipped = MigrateCommand._apply_moves( + moves=moves, + project_root=tmp_path, + artifacts_root="docs", + dry_run=False, + colors=_FakeColors, + ) + assert moved == 0 + assert skipped == 1 + assert new.read_text(encoding="utf-8") == "existing content" + + def test_dry_run_does_not_move_files(self, tmp_path: Path) -> None: + """Dry-run reports a move without actually moving the file.""" + old = tmp_path / "docs" / "a.md" + old.parent.mkdir(parents=True) + old.write_text("content", encoding="utf-8") + + moves = [{"old": "docs/a.md", "new": "docs/b.md"}] + moved, skipped = MigrateCommand._apply_moves( + moves=moves, + project_root=tmp_path, + artifacts_root="docs", + dry_run=True, + colors=_FakeColors, + ) + assert moved == 1 + assert skipped == 0 + assert old.exists() + assert not (tmp_path / "docs" / "b.md").exists() + + def test_skips_moves_with_empty_paths(self, tmp_path: Path) -> None: + """Ignores move entries that are missing old or new keys.""" + moves: list[dict[str, Any]] = [ + {"old": "", "new": "docs/b.md"}, + {"old": "docs/a.md", "new": ""}, + ] + moved, skipped = MigrateCommand._apply_moves( + moves=moves, + project_root=tmp_path, + artifacts_root="docs", + dry_run=False, + colors=_FakeColors, + ) + assert moved == 0 + assert skipped == 0 + + def test_creates_parent_directories(self, tmp_path: Path) -> None: + """Creates missing parent directories for the new path.""" + old = tmp_path / "docs" / "a.md" + old.parent.mkdir(parents=True) + old.write_text("content", encoding="utf-8") + + moves = [{"old": "docs/a.md", "new": "docs/deep/nested/b.md"}] + MigrateCommand._apply_moves( + moves=moves, + project_root=tmp_path, + artifacts_root="docs", + dry_run=False, + colors=_FakeColors, + ) + assert (tmp_path / "docs" / "deep" / "nested" / "b.md").exists() + + def test_substitutes_custom_artifacts_root_in_new_path(self, tmp_path: Path) -> None: + """Applies the custom artifacts root when resolving new paths.""" + old = tmp_path / "docs" / "a.md" + old.parent.mkdir(parents=True) + old.write_text("content", encoding="utf-8") + + moves = [{"old": "docs/a.md", "new": "docs/b.md"}] + MigrateCommand._apply_moves( + moves=moves, + project_root=tmp_path, + artifacts_root="content", + dry_run=False, + colors=_FakeColors, + ) + assert (tmp_path / "content" / "b.md").exists() + + +class TestMigrateCommandRun: + """MigrateCommand.run end-to-end scenarios.""" + + def test_detects_to_major_from_package_version(self, tmp_path: Path) -> None: + """Detects to_major from the current package VERSION when --to is absent.""" + record_dir = tmp_path / "_migrations" + record_dir.mkdir() + + cmd = MigrateCommand(_service()) + context = _context( + Namespace(dry_run=True, target=str(tmp_path), from_major=1, to_major=None) + ) + with patch("vstack.cli.migrate.MIGRATIONS_ROOT", record_dir): + # Should complete without error regardless of what VERSION resolves to + result = cmd.run(context=context) + assert result == 0 + + def test_to_major_falls_back_when_version_unparseable(self, tmp_path: Path) -> None: + """Falls back to from_major+1 when VERSION cannot be parsed.""" + record_dir = tmp_path / "_migrations" + record_dir.mkdir() + + cmd = MigrateCommand(_service()) + context = _context( + Namespace(dry_run=True, target=str(tmp_path), from_major=2, to_major=None) + ) + with ( + patch("vstack.cli.migrate.MIGRATIONS_ROOT", record_dir), + patch("vstack.cli.migrate.VERSION", "not-semver"), + ): + result = cmd.run(context=context) + assert result == 0 + + def test_returns_0_when_nothing_to_migrate( + self, tmp_path: Path, capsys: pytest.CaptureFixture + ) -> None: + """Returns 0 and prints a message when from >= to.""" + cmd = MigrateCommand(_service()) + context = _context(Namespace(dry_run=False, target=str(tmp_path), from_major=3, to_major=3)) + assert cmd.run(context=context) == 0 + assert "Nothing to migrate" in capsys.readouterr().out + + def test_returns_1_when_major_not_detectable(self, tmp_path: Path) -> None: + """Returns 1 and writes to stderr when from_major cannot be detected.""" + cmd = MigrateCommand(_service()) + context = _context( + Namespace(dry_run=False, target=str(tmp_path), from_major=None, to_major=3) + ) + assert cmd.run(context=context) == 1 + + def test_applies_moves_for_range(self, tmp_path: Path) -> None: + """Applies migration records across the full from→to range.""" + # Create old-style docs + (tmp_path / "docs").mkdir() + (tmp_path / "docs" / "architecture.md").write_text("arch", encoding="utf-8") + + # Fake migration record: v2→v3 + record_dir = tmp_path / "_migrations" + record_dir.mkdir() + record = { + "from_version": "2.x", + "to_version": "3.0", + "moves": [ + {"old": "docs/architecture.md", "new": "docs/overview.md", "type": "docs"}, + ], + } + (record_dir / "v2_to_v3.yaml").write_text(yaml.dump(record), encoding="utf-8") + + cmd = MigrateCommand(_service()) + context = _context(Namespace(dry_run=False, target=str(tmp_path), from_major=2, to_major=3)) + with patch("vstack.cli.migrate.MIGRATIONS_ROOT", record_dir): + result = cmd.run(context=context) + + assert result == 0 + assert not (tmp_path / "docs" / "architecture.md").exists() + assert (tmp_path / "docs" / "overview.md").read_text(encoding="utf-8") == "arch" + + def test_skips_steps_with_no_record( + self, tmp_path: Path, capsys: pytest.CaptureFixture + ) -> None: + """Skips version steps that have no migration record file.""" + record_dir = tmp_path / "_migrations" + record_dir.mkdir() # empty — no records + + cmd = MigrateCommand(_service()) + context = _context(Namespace(dry_run=False, target=str(tmp_path), from_major=1, to_major=3)) + with patch("vstack.cli.migrate.MIGRATIONS_ROOT", record_dir): + result = cmd.run(context=context) + + assert result == 0 + out = capsys.readouterr().out + assert "no migration record" in out + + def test_dry_run_does_not_move_files(self, tmp_path: Path) -> None: + """Dry-run leaves files in place while reporting counts.""" + (tmp_path / "docs").mkdir() + (tmp_path / "docs" / "architecture.md").write_text("arch", encoding="utf-8") + + record_dir = tmp_path / "_migrations" + record_dir.mkdir() + record = { + "from_version": "2.x", + "to_version": "3.0", + "moves": [ + {"old": "docs/architecture.md", "new": "docs/overview.md", "type": "docs"}, + ], + } + (record_dir / "v2_to_v3.yaml").write_text(yaml.dump(record), encoding="utf-8") + + cmd = MigrateCommand(_service()) + context = _context(Namespace(dry_run=True, target=str(tmp_path), from_major=2, to_major=3)) + with patch("vstack.cli.migrate.MIGRATIONS_ROOT", record_dir): + result = cmd.run(context=context) + + assert result == 0 + assert (tmp_path / "docs" / "architecture.md").exists() + assert not (tmp_path / "docs" / "overview.md").exists() + + def test_detects_from_major_from_manifest(self, tmp_path: Path) -> None: + """Detects from_major from the manifest when --from is absent.""" + vstack_dir = tmp_path / ".vstack" + vstack_dir.mkdir() + manifest = { + "manifest_version": 2, + "hash_algorithm": "sha256", + "vstack_version": "2.1.0", + "installed_at": "2026-01-01T00:00:00+00:00", + "artifacts": {}, + } + (vstack_dir / "vstack.json").write_text( + __import__("json").dumps(manifest), encoding="utf-8" + ) + + record_dir = tmp_path / "_migrations" + record_dir.mkdir() + + cmd = MigrateCommand(_service()) + context = _context( + Namespace(dry_run=True, target=str(tmp_path), from_major=None, to_major=3) + ) + with patch("vstack.cli.migrate.MIGRATIONS_ROOT", record_dir): + result = cmd.run(context=context) + + assert result == 0 + + def test_chains_multiple_migration_steps(self, tmp_path: Path) -> None: + """Applies v1→v2 and v2→v3 moves in sequence for a v1→v3 migration.""" + (tmp_path / "docs").mkdir() + (tmp_path / "docs" / "old_v1.md").write_text("v1", encoding="utf-8") + (tmp_path / "docs" / "old_v2.md").write_text("v2", encoding="utf-8") + + record_dir = tmp_path / "_migrations" + record_dir.mkdir() + + (record_dir / "v1_to_v2.yaml").write_text( + yaml.dump( + { + "from_version": "1.x", + "to_version": "2.0", + "moves": [{"old": "docs/old_v1.md", "new": "docs/new_v1.md", "type": "docs"}], + } + ), + encoding="utf-8", + ) + (record_dir / "v2_to_v3.yaml").write_text( + yaml.dump( + { + "from_version": "2.x", + "to_version": "3.0", + "moves": [{"old": "docs/old_v2.md", "new": "docs/new_v2.md", "type": "docs"}], + } + ), + encoding="utf-8", + ) + + cmd = MigrateCommand(_service()) + context = _context(Namespace(dry_run=False, target=str(tmp_path), from_major=1, to_major=3)) + with patch("vstack.cli.migrate.MIGRATIONS_ROOT", record_dir): + result = cmd.run(context=context) + + assert result == 0 + assert (tmp_path / "docs" / "new_v1.md").exists() + assert (tmp_path / "docs" / "new_v2.md").exists() diff --git a/tests/vstack/cli/test_parser.py b/tests/vstack/cli/test_parser.py index c24123b..f877cb2 100644 --- a/tests/vstack/cli/test_parser.py +++ b/tests/vstack/cli/test_parser.py @@ -90,6 +90,7 @@ def test_build_parser_has_expected_commands(self) -> None: "install", "init", "uninstall", + "migrate", } def test_verify_accepts_only_filter(self) -> None: From f35ad4b7c84709de2bec922e3ca510e8d00c3c01 Mon Sep 17 00:00:00 2001 From: Erik Schaareman Date: Sat, 9 May 2026 21:38:44 +0200 Subject: [PATCH 13/25] docs: add install and upgrade guide with quickstart and quick upgrade paths --- README-pypi.md | 87 +++++++++++++++++-- README.md | 223 ++++++++++++++++++++++++++++++++++++++++++++----- 2 files changed, 281 insertions(+), 29 deletions(-) diff --git a/README-pypi.md b/README-pypi.md index 787ef28..eb6c8ac 100644 --- a/README-pypi.md +++ b/README-pypi.md @@ -53,16 +53,23 @@ VS Code command palette (`Chat: Run Prompt File`) or the Copilot Chat attach but | `migration-safety` | Review DB migration safety, rollback, and zero-downtime | | `release-readiness` | Evaluate release readiness from reports and open blockers | -## Quick start +## Quickstart — fresh install Install with `pipx`, then install vstack artifacts into your repository: ```bash +# Install the CLI once, globally pipx install vstack -vstack install --target /path/to/your/project -vstack validate + +# Move to your repository root and run install — no --target needed +cd /path/to/your/project +vstack install # seeds .vstack/config.yaml and generates .github/ in the current directory +vstack validate # confirm no errors ``` +When you omit `--target`, vstack uses the current working directory. The equivalent +explicit form is `vstack install --target /path/to/your/project`. + Run a first task in Copilot Agent Mode: ```text @@ -74,6 +81,57 @@ Expected result: - `vstack validate` reports no unresolved template tokens - Agent command returns a concrete verification summary for your repository +## Quick upgrade + +### Patch or minor version (e.g. v3.1 → v3.2, same major) + +Docs paths never change within a major version. Only `.github/` artifacts are updated. + +```bash +pipx upgrade vstack + +cd /path/to/your/project +vstack init # idempotent — safe to run in CI +``` + +### Major version (e.g. v2 → v3) + +Docs paths may change on a major version bump. Run `vstack migrate` before `vstack init`. + +```bash +pipx upgrade vstack + +cd /path/to/your/project +vstack migrate # moves docs files to their new paths (auto-detects installed version) +vstack init # regenerates .github/ artifacts + +# Only if you see "Legacy manifest schema detected" in the output above: +vstack manifest upgrade +vstack init +``` + +Preview the docs moves without touching any files: + +```bash +vstack migrate --dry-run +``` + +For upgrades spanning multiple major versions (e.g. v1 → v3), `vstack migrate` chains +all intermediate steps automatically. Use `--from` and `--to` to specify the range +explicitly if auto-detection from the manifest fails: + +```bash +vstack migrate --from 1 --to 3 +vstack init +``` + +### Force reinstall (overwrite local edits) + +```bash +vstack install --force # overwrite all managed artifacts +vstack install --force-name agent/engineer # overwrite one specific artifact +``` + ## Why this helps - Consistent role boundaries for planning, implementation, validation, and release @@ -85,10 +143,17 @@ Expected result: ```bash vstack --version vstack validate + +# Run from your repository root (--target defaults to the current directory) +vstack install +vstack init +vstack migrate +vstack manifest verify +vstack manifest status +vstack manifest upgrade + +# Or specify a path explicitly vstack install --target /path/to/your/project -vstack manifest verify --target /path/to/your/project -vstack manifest status --target /path/to/your/project -vstack manifest upgrade --target /path/to/your/project ``` ## Common usage patterns @@ -96,6 +161,11 @@ vstack manifest upgrade --target /path/to/your/project Repository-scoped install (recommended for teams): ```bash +# Move to your repository root and install there +cd /path/to/your/project +vstack install + +# Or specify a path explicitly from any directory vstack install --target /path/to/your/project ``` @@ -122,7 +192,8 @@ exclude: If you already have agents, skills, or other files in `.github/`, run a dry-run first to see what would be preserved before committing: ```bash -vstack install --dry-run --target /path/to/your/project +# Run from your repository root +vstack install --dry-run ``` The summary lists preserved files as `type/name` selectors (e.g. `agent/engineer`). Resolve each conflict with `--force-name type/name` to overwrite, `--adopt-name type/name` to take ownership without overwriting, or `--force` to overwrite everything. @@ -130,7 +201,7 @@ The summary lists preserved files as `type/name` selectors (e.g. `agent/engineer ## Fast troubleshooting - Command not found after install: ensure your `pipx` binary path is in `PATH` -- Validation error: rerun `vstack install --target ...` and then `vstack validate` +- Validation error: rerun `vstack install` from your repository root and then `vstack validate` - Agent results look generic: explicitly invoke a role (for example `@tester`) before a skill ## Full documentation diff --git a/README.md b/README.md index 32e3182..5a43a8c 100644 --- a/README.md +++ b/README.md @@ -55,38 +55,75 @@ For experienced users: - Role summary - Example usage - All vstack CLI commands +- Install and upgrade guide - Workflow - Development - CI and Release Automation ### ⚡ Quick paths -#### New user path (2 minutes) +#### Quickstart — fresh install (2 minutes) ```bash +# 1. Install the CLI once, globally pipx install vstack -vstack install --target /path/to/your/project + +# 2. Move to your repository root — all commands default to the current directory +cd /path/to/your/project +vstack install # seeds .vstack/config.yaml and generates .github/ here + +# 3. Confirm everything is in order vstack validate ``` -Then in Copilot Agent Mode: +When you omit `--target`, vstack uses the current working directory. +The explicit form `vstack install --target /path/to/your/project` is equivalent +and useful when running from a different directory. + +Then open Copilot Agent Mode and run: ```text @tester /verify Check this repository and summarize findings ``` -#### Power user path (30 seconds) +#### Quick upgrade — patch or minor version (same major) + +Docs paths never change within a major version. Only `.github/` artifacts are updated. ```bash -vstack install --target /path/to/your/project && vstack verify --target /path/to/your/project +# 1. Upgrade the CLI +pipx upgrade vstack + +# 2. From your repository root — regenerate .github/ artifacts +cd /path/to/your/project +vstack init ``` -Then jump straight into your role workflow: +#### Quick upgrade — major version (e.g. v2 → v3) -```text -@architect Review contracts in src/api/ -@engineer /code-review -@tester /security +Docs paths may change on a major version bump. Run `vstack migrate` before `vstack init`. + +```bash +# 1. Upgrade the CLI +pipx upgrade vstack + +# 2. From your repository root +cd /path/to/your/project + +# 3. Move any docs files that changed path (reads installed version from .vstack/vstack.json) +vstack migrate + +# 4. Regenerate .github/ artifacts +vstack init + +# 5. If the manifest schema is outdated (you will see an error message telling you to do this) +vstack manifest upgrade +``` + +Preview the docs moves without touching any files: + +```bash +vstack migrate --dry-run ``` ```mermaid @@ -101,7 +138,7 @@ ______________________________________________________________________ ## 🚀 Quickstart -> New here? Start with `pipx install ...`, then run `vstack install --target ...`, then try `@tester /verify` in Copilot Agent Mode. +> New here? Run `pipx install vstack`, move to your repository root, run `vstack install`, then try `@tester /verify` in Copilot Agent Mode. ### ⚡ Install with pipx (recommended) @@ -116,7 +153,11 @@ pipx install vstack Afterwards, the `vstack` command is available everywhere: ```bash -# Recommended: install vstack artifacts per project/repository +# Recommended: move to your repository root and install there +cd /path/to/your/project +vstack install # generates .github/ in the current directory + +# Or specify a path explicitly when running from a different directory vstack install --target /path/to/your/project # Optional: install profile-wide defaults for all VS Code projects @@ -156,8 +197,8 @@ Use repository-scoped installation so every contributor and CI run uses the same 1. Require `commit.yml`, `check.yml`, `verify.yml`, and `security.yml` checks before merge. ```bash -# From your repository root -vstack install --target /path/to/your/project +cd /path/to/your/project +vstack install git add .github git commit -m "chore: install vstack artifacts" ``` @@ -344,7 +385,7 @@ A: In a specific repository, run `vstack install --target /path/to/your/project` A: Python 3.11–3.14 (see badges above). **Q: How do I reset the install?** -A: For one repository, run `vstack uninstall --target /path/to/your/project` and then reinstall with `vstack install --target /path/to/your/project`. Use `--global` only for profile-wide defaults. +A: Move to your repository root and run `vstack uninstall`, then `vstack install`. You can also use `vstack uninstall --target /path/to/your/project` from any directory. Use `--global` only for profile-wide defaults. **Q: Where can I ask questions or give feedback?** A: [Start a discussion or ask a question here.](https://github.com/eschaar/vstack/discussions) @@ -356,7 +397,11 @@ ______________________________________________________________________ To remove vstack artifacts from your project or profile, use the CLI: ```bash -# Uninstall vstack artifacts from your current project +# Move to your repository root and uninstall +cd /path/to/your/project +vstack uninstall + +# Or specify a path explicitly from any directory vstack uninstall --target /path/to/your/project # Uninstall vstack artifacts from your global VS Code profile @@ -416,27 +461,30 @@ ______________________________________________________________________ | `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 | +| `vstack migrate --target DIR` | Move docs files from old paths to new paths after a major vstack upgrade | +| `vstack migrate --from M --to N` | Migrate docs paths across major versions M through N (chains intermediate steps) | +| `vstack migrate --dry-run` | Preview docs path moves without touching any files | 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: ```bash -# Preview what install would do — no files are written -vstack install --dry-run --target /path/to/your/project +# Preview what install would do — no files are written (run from your repository root) +vstack install --dry-run ``` The summary shows every preserved file as a `type/name` selector (e.g. `agent/engineer`, `skill/verify`). You can then resolve each conflict selectively: ```bash # Overwrite a specific preserved artifact -vstack install --target . --force-name agent/engineer +vstack install --force-name agent/engineer # Take ownership of an existing file without overwriting it -vstack install --target . --adopt-name agent/engineer +vstack install --adopt-name agent/engineer # Overwrite everything -vstack install --target . --force +vstack install --force ``` When multiple artifact types share the same name (e.g. an `agent` and a `skill` both named `engineer`), use the `type/name` form to target one precisely. @@ -489,6 +537,139 @@ All fields are optional. An absent or commented-out block restores the default b ______________________________________________________________________ +## ⬆️ Install and upgrade guide + +vstack manages two separate layers. Knowing which layer each command touches prevents mistakes: + +| Layer | What it contains | Updated by | +| --------------------- | -------------------------------------------------------------------------------- | -------------------------------- | +| `.github/` | Agent, skill, instruction, and prompt files that Copilot reads | `vstack install` / `vstack init` | +| `docs/` | Docs files that agents read and write (paths may change on a major version bump) | `vstack migrate` | +| `.vstack/vstack.json` | Manifest — tracks which `.github/` files are managed and stores their checksums | `vstack manifest upgrade` | + +### Scenario 1 — Fresh install (no previous vstack) + +```bash +# Install the CLI once, globally +pipx install vstack + +# Move to your repository root +cd /path/to/your/project +vstack install # seeds .vstack/config.yaml and generates .github/ in the current directory +vstack validate # confirm no errors +``` + +All commands default to the current working directory when `--target` is omitted. +Run them from the repository root. The explicit form `vstack install --target /path/to/your/project` +is equivalent and useful when running from a different directory. + +### Scenario 2 — Fresh install over an existing version (force) + +Replace all managed artifacts, even if you have made local edits: + +```bash +# Preview what would happen first +vstack install --dry-run + +# Overwrite everything +vstack install --force +``` + +Or target a single artifact without touching the rest: + +```bash +vstack install --force-name agent/engineer +vstack install --force-name skill/verify +``` + +To take ownership of an existing unmanaged file without overwriting it: + +```bash +vstack install --adopt-name agent/architect +``` + +### Scenario 3 — Patch or minor upgrade (e.g. v3.1 → v3.2, same major) + +Docs paths never change within a major version. Only `.github/` artifacts need updating. + +```bash +pipx upgrade vstack + +cd /path/to/your/project +vstack init # idempotent regeneration — safe to run in CI +``` + +`vstack init` is safe to re-run at any time. It reads `.vstack/config.yaml` and regenerates `.github/` artifacts without touching anything else. + +### Scenario 4 — Major upgrade, single step (e.g. v2 → v3) + +Docs paths may change on a major version bump. Run `vstack migrate` before `vstack init`. + +```bash +pipx upgrade vstack + +cd /path/to/your/project + +# Preview what migrate would move (no files are touched) +vstack migrate --dry-run + +# Apply the docs path moves (auto-detects your installed version from .vstack/vstack.json) +vstack migrate + +# Regenerate .github/ artifacts +vstack init + +# Only needed if you see: "Legacy manifest schema detected" in the output above +vstack manifest upgrade +vstack init +``` + +### Scenario 5 — Major upgrade, multiple steps (e.g. v1 → v3) + +`vstack migrate` chains all intermediate steps automatically. You do not need to run it once per version. + +```bash +pipx upgrade vstack + +cd /path/to/your/project + +# Auto-detects v1 from manifest, chains v1→v2→v3 automatically +vstack migrate + +# Or specify the range explicitly if auto-detection fails +vstack migrate --from 1 --to 3 + +vstack init +``` + +If no migration record exists for an intermediate step (for example v1 → v2), that step is silently skipped. + +### Scenario 6 — Manifest schema is outdated + +If a command fails with: + +```text +Legacy manifest schema detected in vstack.json. Run: vstack manifest upgrade +``` + +Run: + +```bash +vstack manifest upgrade +vstack init +``` + +### Common mistakes to avoid + +| Mistake | Symptom | Fix | +| ---------------------------------------------------------------- | --------------------------------------------------------- | --------------------------------------------------------------------- | +| Running `vstack init` before `vstack migrate` on a major upgrade | Agents reference docs paths that no longer exist | Run `vstack migrate` first, then `vstack init` | +| Running `vstack migrate` without a manifest | `ERROR: could not detect installed version from manifest` | Run `vstack migrate --from ` to specify explicitly | +| Local edits not overwritten | `vstack init` skips modified files silently | Use `vstack install --force-name type/name` to overwrite one artifact | +| Manifest schema error after upgrade | `vstack init` fails with a schema error | Run `vstack manifest upgrade` first | + +______________________________________________________________________ + ## 🤝 How to contribute Contributions are welcome! Please see [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines, code style, and how to get started. From 9a4d51cabcc7f0c040d4f4a11673f4075d310da6 Mon Sep 17 00:00:00 2001 From: Erik Schaareman Date: Sat, 9 May 2026 21:39:02 +0200 Subject: [PATCH 14/25] docs(roadmap): mark workflow contract source-of-truth as shipped --- docs/product/roadmap.md | 86 ++++++++++++++++++++--------------------- 1 file changed, 43 insertions(+), 43 deletions(-) diff --git a/docs/product/roadmap.md b/docs/product/roadmap.md index 495f13e..4ecc617 100644 --- a/docs/product/roadmap.md +++ b/docs/product/roadmap.md @@ -7,36 +7,36 @@ ______________________________________________________________________ ## feature status table -| 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/.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. | in progress | Central contract file in `.vstack/config.yaml`; `gate`, `hitl`, `handoffs` schema (ADR-023) | -| optional orchestrated role pipeline | t.b.d. | in progress | `planner` coordinator agent using VS Code native subagents (ADR-024); supersedes ADR-004 | -| 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 | +| 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/.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. | shipped | `workflow:` block in `.vstack/config.yaml`; `gate`, `hitl`, `handoffs` schema; `vstack migrate` command (ADR-023, ADR-026) | +| optional orchestrated role pipeline | t.b.d. | in progress | `planner` coordinator agent using VS Code native subagents (ADR-024); supersedes ADR-004 | +| 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 | ______________________________________________________________________ @@ -240,23 +240,23 @@ Overlay model (template source priority): Ref: [GitHub — Customize Copilot for your project](https://docs.github.com/en/copilot/how-tos/copilot-on-github/customize-copilot/customize-copilot-overview) -### workflow contract source-of-truth [candidate — t.b.d.] +### workflow contract source-of-truth [shipped — t.b.d.] -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. +Shipped in this release: -What remains: +- `workflow:` block seeded in `.vstack/config.yaml` by `vstack install`, with `stage`, `gate`, + `hitl`, and `handoffs.prompt` for all six roles (ADR-023) +- `handoffs:` section in generated `.agent.md` files is now driven by the workflow config rather + than hardcoded in each agent's `config.yaml` +- `baseline:` flag on output artifacts; agents explicitly maintain baseline docs vs. per-session deliverables +- `vstack migrate` command applies docs artifact path moves between major versions, + with `--dry-run`, `--from`, and `--to` flags; migration records in `src/vstack/_migrations/` (ADR-026) +- `docs/design/workflow.md` remains the human-readable explanation layer -- 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. +Not yet implemented (deferred to orchestrated pipeline milestone): -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. +- Generator-level cross-role validation of input/output chains +- Central read-only contract export for external orchestrator consumption ### optional orchestrated role pipeline [candidate — t.b.d.] From 4ce284cabb4f76e60724ee1ab57115bf8efb0791 Mon Sep 17 00:00:00 2001 From: Erik Schaareman Date: Sat, 9 May 2026 21:43:09 +0200 Subject: [PATCH 15/25] chore: regenerate poetry.lock after adding pyyaml runtime dependency --- poetry.lock | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/poetry.lock b/poetry.lock index 6640268..bb286f2 100644 --- a/poetry.lock +++ b/poetry.lock @@ -577,7 +577,7 @@ version = "6.0.3" description = "YAML parser and emitter for Python" optional = false python-versions = ">=3.8" -groups = ["dev"] +groups = ["main", "dev"] files = [ {file = "PyYAML-6.0.3-cp38-cp38-macosx_10_13_x86_64.whl", hash = "sha256:c2514fceb77bc5e7a2f7adfaa1feb2fb311607c9cb518dbc378688ec73d8292f"}, {file = "PyYAML-6.0.3-cp38-cp38-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9c57bb8c96f6d1808c030b1687b9b5fb476abaa47f0db9c0101f5e9f394e97f4"}, @@ -721,6 +721,18 @@ virtualenv = ">=21.1" [package.extras] completion = ["argcomplete (>=3.6.3)"] +[[package]] +name = "types-pyyaml" +version = "6.0.12.20260508" +description = "Typing stubs for PyYAML" +optional = false +python-versions = ">=3.10" +groups = ["dev"] +files = [ + {file = "types_pyyaml-6.0.12.20260508-py3-none-any.whl", hash = "sha256:edc094ed3a918b0c6232f71a5b67fdf38e76e17517b7d87bfbb9fc27d442fb51"}, + {file = "types_pyyaml-6.0.12.20260508.tar.gz", hash = "sha256:5ae42149c3ebf7aaaf6c65ee49af590c80f0ba52e9e3f75a75c5564b33556fa6"}, +] + [[package]] name = "typing-extensions" version = "4.15.0" @@ -754,4 +766,4 @@ python-discovery = ">=1.2.2" [metadata] lock-version = "2.1" python-versions = ">=3.11,<3.15" -content-hash = "70281e6bd9bca0658ec7da4e5647a65a071fe717d39cb61c1bf2d51226421222" +content-hash = "b3b6750041a168225c31625cd64b77b78229506f5b66edc8bebbe468bc7ba586" From e7aaf903ffb350500ed1330d6cfda7c70d725adc Mon Sep 17 00:00:00 2001 From: Erik Schaareman Date: Sun, 10 May 2026 00:14:34 +0200 Subject: [PATCH 16/25] fix(frontmatter): restore @staticmethod on FrontmatterParser; fix dict/list serialization Restores the class-level static-method call form FrontmatterParser.parse(...) and FrontmatterParser.parse_yaml(...) that was broken by the OOP-cleanup commit. Updates every call site in artifacts/generator.py, cli/interface.py, and cli/service.py. Fixes FrontmatterSerializer._append_raw_field to use yaml.dump for dict and list values instead of str(), which was emitting invalid Python-repr YAML (e.g. {'owner': 'vstack', ...}) into generated agent frontmatter. Adds tests for dict-value and list-value raw-field serialization. --- src/vstack/artifacts/generator.py | 7 +++-- src/vstack/cli/interface.py | 6 ++--- src/vstack/cli/service.py | 4 ++- src/vstack/frontmatter/parser.py | 13 +++++---- src/vstack/frontmatter/serializer.py | 22 +++++++++++++++- tests/vstack/frontmatter/test_serializer.py | 29 +++++++++++++++++++++ 6 files changed, 67 insertions(+), 14 deletions(-) diff --git a/src/vstack/artifacts/generator.py b/src/vstack/artifacts/generator.py index cba4537..8cbcf80 100644 --- a/src/vstack/artifacts/generator.py +++ b/src/vstack/artifacts/generator.py @@ -46,7 +46,6 @@ def __init__(self, type_config: ArtifactTypeConfig, templates_root: Path) -> Non else None ) self._partials: dict[str, str] | None = None - self._parser = FrontmatterParser() # ── Placeholder resolution ──────────────────────────────────────────────── @@ -141,7 +140,7 @@ def load_artifact_config(self, tmpl_dir: Path) -> dict: if not config_file.exists(): return {} raw = config_file.read_text(encoding="utf-8") - return self._parser.parse_yaml(raw) + return FrontmatterParser.parse_yaml(raw) # ── Rendering ───────────────────────────────────────────────────────────── @@ -173,7 +172,7 @@ def render(self, tmpl_dir: Path) -> RenderedArtifact: resolved = self.resolve_placeholders(content, partials) # Split existing frontmatter from body - parsed = self._parser.parse(resolved) + parsed = FrontmatterParser.parse(resolved) existing_fm = parsed.metadata body = parsed.content @@ -313,7 +312,7 @@ def fail(msg: str) -> None: for name, tmpl_dir in tmpl_by_name.items(): content = (tmpl_dir / self.config.template_filename).read_text(encoding="utf-8") artifact_config = self.load_artifact_config(tmpl_dir) - parsed = self._parser.parse(content) + parsed = FrontmatterParser.parse(content) existing_fm = parsed.metadata meta = {**artifact_config, **existing_fm} if existing_fm else artifact_config diff --git a/src/vstack/cli/interface.py b/src/vstack/cli/interface.py index 0ee5784..704ec57 100644 --- a/src/vstack/cli/interface.py +++ b/src/vstack/cli/interface.py @@ -99,7 +99,7 @@ def _read_exclude( 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")) + parsed = FrontmatterParser.parse_yaml(config_path.read_text(encoding="utf-8")) raw_exclude = parsed.get("exclude", "") if not isinstance(raw_exclude, dict): return frozenset(), {} @@ -136,7 +136,7 @@ def _read_artifacts_root(install_dir: Path | None) -> str: 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")) + parsed = FrontmatterParser.parse_yaml(config_path.read_text(encoding="utf-8")) artifacts = parsed.get("artifacts", "") if not isinstance(artifacts, dict): return ARTIFACTS_DOCS_ROOT @@ -163,7 +163,7 @@ def _read_workflow_stages(install_dir: Path | None) -> list[dict]: config_path = install_dir.parent / ".vstack" / "config.yaml" if not config_path.exists(): return [] - parsed = FrontmatterParser().parse_yaml(config_path.read_text(encoding="utf-8")) + parsed = FrontmatterParser.parse_yaml(config_path.read_text(encoding="utf-8")) workflow = parsed.get("workflow", "") if not isinstance(workflow, dict): return [] diff --git a/src/vstack/cli/service.py b/src/vstack/cli/service.py index 8ba7218..6a2147e 100644 --- a/src/vstack/cli/service.py +++ b/src/vstack/cli/service.py @@ -56,7 +56,9 @@ def __init__( override via ``artifacts.root`` in ``.vstack/config.yaml``. workflow_stages: Ordered list of pipeline stage dicts read from the ``workflow.stages`` block in ``.vstack/config.yaml``. - Each dict has ``role``, ``gate``, and ``handoff_prompt`` keys. + Each dict has ``role`` and ``gate`` string keys, a ``handoffs`` + key containing a list of dicts with ``prompt``, ``agent``, and + ``label`` string keys, and an optional ``hitl`` string key. When ``None`` or empty the generator falls back to v3 behaviour. """ self.root = templates_root diff --git a/src/vstack/frontmatter/parser.py b/src/vstack/frontmatter/parser.py index e32bf90..7ec3dac 100644 --- a/src/vstack/frontmatter/parser.py +++ b/src/vstack/frontmatter/parser.py @@ -51,7 +51,8 @@ def __bool__(self) -> bool: class FrontmatterParser: """Parse YAML frontmatter using :func:`yaml.safe_load`.""" - def parse(self, content: str) -> FrontmatterContent: + @staticmethod + def parse(content: str) -> FrontmatterContent: """Split YAML frontmatter from body content. Returns a :class:`FrontmatterContent` instance. When no frontmatter @@ -61,10 +62,11 @@ def parse(self, content: str) -> FrontmatterContent: match = _FRONTMATTER_RE.match(content) if not match: return FrontmatterContent(metadata={}, content=content) - meta = self._parse_yaml_block(match.group(1)) + meta = FrontmatterParser._parse_yaml_block(match.group(1)) return FrontmatterContent(metadata=meta, content=match.group(2)) - def parse_yaml(self, raw: str) -> dict: + @staticmethod + def parse_yaml(raw: str) -> dict: """Parse a raw YAML string without frontmatter delimiters. Args: @@ -73,11 +75,12 @@ def parse_yaml(self, raw: str) -> dict: Returns: A parsed metadata dictionary. """ - return self._parse_yaml_block(raw) + return FrontmatterParser._parse_yaml_block(raw) # ── Internal ────────────────────────────────────────────────────────────── - def _parse_yaml_block(self, raw: str) -> dict: + @staticmethod + def _parse_yaml_block(raw: str) -> dict: """Delegate YAML parsing to :func:`yaml.safe_load`. Pre-processes ``- *`` (VS Code wildcard list items) into quoted form diff --git a/src/vstack/frontmatter/serializer.py b/src/vstack/frontmatter/serializer.py index 370eae5..b2eb05c 100644 --- a/src/vstack/frontmatter/serializer.py +++ b/src/vstack/frontmatter/serializer.py @@ -13,6 +13,8 @@ import re import textwrap +import yaml + from vstack.frontmatter.schema import FieldSpec, FrontmatterSchema # Characters that open a YAML alias (*), anchor (&), or tag (!) when they appear @@ -149,7 +151,25 @@ def _append_object_list_items( lines.append(f"{prefix}{obj_line}") def _append_raw_field(self, lines: list[str], name: str, value: object) -> None: - """Append a raw YAML field value without additional serialization.""" + """Append a raw YAML field, properly indenting dict/list values. + + When *value* is a ``dict`` or ``list`` (as returned by PyYAML when + parsing nested structures), it is serialised with :func:`yaml.dump` and + each output line is indented by two spaces under the parent key. Plain + string values are treated as pre-formatted YAML and emitted as-is, + preserving the original behaviour for hand-authored raw blocks. + """ + if isinstance(value, (dict, list)): + yaml_str = yaml.dump( + value, + default_flow_style=False, + allow_unicode=True, + sort_keys=False, + ).rstrip("\n") + lines.append(f"{name}:") + for raw_line in yaml_str.splitlines(): + lines.append(f" {raw_line}") + return raw_str = str(value).strip() if value is not None else "" if raw_str: lines.append(f"{name}:") diff --git a/tests/vstack/frontmatter/test_serializer.py b/tests/vstack/frontmatter/test_serializer.py index b1ec4f0..a28a9eb 100644 --- a/tests/vstack/frontmatter/test_serializer.py +++ b/tests/vstack/frontmatter/test_serializer.py @@ -29,6 +29,35 @@ def test_serialize_raw_field_empty_skipped(self) -> None: output = FrontmatterSerializer().serialize({"name": "agent", "mcp-servers": ""}, schema) assert "mcp-servers" not in output + def test_serialize_raw_field_dict_value(self) -> None: + """Dict value from PyYAML parsing is emitted as properly indented YAML.""" + schema = FrontmatterSchema( + [FieldSpec("name", quoted=False), FieldSpec("metadata", type="raw")] + ) + output = FrontmatterSerializer().serialize( + {"name": "agent", "metadata": {"owner": "team-a", "tier": "backend"}}, + schema, + ) + assert "metadata:" in output + assert " owner: team-a" in output + assert " tier: backend" in output + # Must not contain a Python dict repr + assert "{'owner'" not in output + + def test_serialize_raw_field_list_value(self) -> None: + """List value from PyYAML parsing is emitted as properly indented YAML.""" + schema = FrontmatterSchema( + [FieldSpec("name", quoted=False), FieldSpec("hooks", type="raw")] + ) + output = FrontmatterSerializer().serialize( + {"name": "agent", "hooks": [{"event": "onSave", "command": "lint"}]}, + schema, + ) + assert "hooks:" in output + assert " - command: lint" in output or " - event: onSave" in output + # Must not contain a Python list repr + assert "[{" not in output + def test_serialize_frontmatter_required_fields(self) -> None: """Test that serialize includes required frontmatter fields.""" output = FrontmatterSerializer().serialize( From e6146912a1e6cf45684beb2dba87ecd2bd3e4a71 Mon Sep 17 00:00:00 2001 From: Erik Schaareman Date: Sun, 10 May 2026 00:14:45 +0200 Subject: [PATCH 17/25] fix(agents): restore _resolve_handoffs fallback; fix None guard in migrate Implements the documented fallback in AgentGenerator._resolve_handoffs: when no workflow stages are configured but a handoff_prompt is present, returns a single generic handoff entry without an agent: key, preserving v3 behaviour. Previously the method returned [] in all no-workflow cases, discarding the agent's own prompt. Removes self._parser instance from AgentGenerator.__init__; all call sites now use FrontmatterParser class-level static methods directly. Adds a None guard in cli/migrate.py _detect_installed_major so it does not raise AttributeError when ManifestFile.read() returns None. Updates agent generator tests to cover the fallback case and the static call form. --- src/vstack/agents/generator.py | 20 ++++++++++++++++---- src/vstack/cli/migrate.py | 2 +- tests/vstack/agents/test_generation.py | 2 +- tests/vstack/agents/test_generator.py | 23 +++++++++++++---------- tests/vstack/agents/test_role_wiring.py | 2 +- tests/vstack/skills/test_templates.py | 4 ++-- 6 files changed, 34 insertions(+), 19 deletions(-) diff --git a/src/vstack/agents/generator.py b/src/vstack/agents/generator.py index b0b3887..f226682 100644 --- a/src/vstack/agents/generator.py +++ b/src/vstack/agents/generator.py @@ -68,8 +68,10 @@ def __init__( ``"docs"``). workflow_stages: Ordered list of pipeline stage dicts read from ``workflow.stages`` in ``.vstack/config.yaml``. Each dict has - ``role``, ``gate``, and ``handoff_prompt`` keys. When ``None`` - or empty the generator falls back to v3 behaviour: a generic + ``role`` and ``gate`` string keys, a ``handoffs`` key containing + a list of dicts with ``prompt``, ``agent``, and ``label`` string + keys, and an optional ``hitl`` string key. When ``None`` or + empty the generator falls back to v3 behaviour: a generic handoff label without an explicit ``agent:`` target. """ super().__init__( @@ -189,8 +191,18 @@ def _resolve_handoffs(self, agent_role: str, handoff_prompt: str) -> list[dict[s or an empty list when this is the last stage or no prompts exist. """ if not self.workflow_stages: - # No workflow configured — cannot emit valid ``agent:`` targets. - return [] + # No workflow configured — emit a generic handoff without an + # explicit ``agent:`` target when a prompt is available, so that + # projects without a workflow: block still get a usable handoff + # entry rather than silently dropping the configured prompt. + if not handoff_prompt.strip(): + return [] + return [ + { + "label": "Continue to next stage", + "prompt": handoff_prompt.strip(), + } + ] roles = [s["role"] for s in self.workflow_stages] try: diff --git a/src/vstack/cli/migrate.py b/src/vstack/cli/migrate.py index 390b29b..b35525a 100644 --- a/src/vstack/cli/migrate.py +++ b/src/vstack/cli/migrate.py @@ -106,7 +106,7 @@ def _read_artifacts_root(project_root: Path) -> str: config_path = project_root / VSTACK_DIR_NAME / "config.yaml" if not config_path.exists(): return ARTIFACTS_DOCS_ROOT - parsed = FrontmatterParser().parse_yaml(config_path.read_text(encoding="utf-8")) + parsed = FrontmatterParser.parse_yaml(config_path.read_text(encoding="utf-8")) artifacts = parsed.get("artifacts", "") if not isinstance(artifacts, dict): return ARTIFACTS_DOCS_ROOT diff --git a/tests/vstack/agents/test_generation.py b/tests/vstack/agents/test_generation.py index 4a5b70d..cb93d29 100644 --- a/tests/vstack/agents/test_generation.py +++ b/tests/vstack/agents/test_generation.py @@ -44,7 +44,7 @@ def test_architect_agent_includes_model_and_handoffs(self, tmp_path: Path) -> No assert out.exists() content = out.read_text(encoding="utf-8") - parsed = FrontmatterParser().parse(content) + parsed = FrontmatterParser.parse(content) assert parsed.metadata.get("name") == "architect" assert parsed.metadata.get("model") == [ diff --git a/tests/vstack/agents/test_generator.py b/tests/vstack/agents/test_generator.py index a5d974f..e91f163 100644 --- a/tests/vstack/agents/test_generator.py +++ b/tests/vstack/agents/test_generator.py @@ -334,13 +334,13 @@ def test_returns_empty_when_no_workflow_and_no_prompt(self) -> None: """Returns empty list when no workflow is configured and no prompt given.""" assert AgentGenerator()._resolve_handoffs("architect", "") == [] - def test_no_handoff_without_workflow(self) -> None: - """Returns empty list when no workflow is configured, even with a prompt. - - A handoff without an explicit ``agent:`` target is invalid per the - VS Code agent schema, so none is emitted when no workflow is configured. - """ - assert AgentGenerator()._resolve_handoffs("architect", "Do some work.") == [] + def test_fallback_handoff_without_workflow(self) -> None: + """Returns a generic handoff (no agent:) when no workflow is configured but a prompt exists.""" + result = AgentGenerator()._resolve_handoffs("architect", "Do some work.") + assert len(result) == 1 + assert result[0]["prompt"] == "Do some work." + assert result[0]["label"] == "Continue to next stage" + assert "agent" not in result[0] def test_with_workflow_finds_next_role(self) -> None: """Returns handoff with correct next agent when workflow is configured.""" @@ -476,9 +476,12 @@ def test_returns_empty_string_when_no_prompt(self) -> None: """Returns empty string when no workflow and no prompt.""" assert AgentGenerator()._build_handoffs("architect", "") == "" - def test_returns_empty_string_without_workflow(self) -> None: - """Returns empty string when no workflow is configured, even with a prompt.""" - assert AgentGenerator()._build_handoffs("architect", "Work done.") == "" + def test_returns_handoff_without_agent_when_no_workflow(self) -> None: + """Returns a handoff block without agent: key when no workflow is configured but prompt is set.""" + result = AgentGenerator()._build_handoffs("architect", "Work done.") + assert "handoffs:" in result + assert "Work done." in result + assert "agent:" not in result def test_returns_handoff_string_with_agent(self) -> None: """Returns handoffs block with agent key when workflow is configured.""" diff --git a/tests/vstack/agents/test_role_wiring.py b/tests/vstack/agents/test_role_wiring.py index d7eb6fc..eae808e 100644 --- a/tests/vstack/agents/test_role_wiring.py +++ b/tests/vstack/agents/test_role_wiring.py @@ -73,7 +73,7 @@ def test_all_role_handoff_targets_are_known_roles() -> None: if not agent_file.exists(): continue text = agent_file.read_text(encoding="utf-8") - parsed = FrontmatterParser().parse(text) + parsed = FrontmatterParser.parse(text) handoffs = parsed.metadata.get("handoffs") or [] for handoff in handoffs: target = handoff.get("agent") diff --git a/tests/vstack/skills/test_templates.py b/tests/vstack/skills/test_templates.py index 3f76b28..a7c6748 100644 --- a/tests/vstack/skills/test_templates.py +++ b/tests/vstack/skills/test_templates.py @@ -42,7 +42,7 @@ def test_every_template_has_valid_config_yaml(self) -> None: name = _skill_name(tmpl) cfg = _skill_config(tmpl) assert cfg.exists(), f"{name}: missing config.yaml" - meta = FrontmatterParser().parse_yaml(cfg.read_text(encoding="utf-8")) + meta = FrontmatterParser.parse_yaml(cfg.read_text(encoding="utf-8")) assert meta.get("name"), f"{name}: missing name" assert meta.get("version"), f"{name}: missing version" assert meta.get("description"), f"{name}: missing description" @@ -51,7 +51,7 @@ def test_config_name_matches_directory(self) -> None: """Test that config name matches directory.""" for tmpl in _skill_templates(): name = _skill_name(tmpl) - meta = FrontmatterParser().parse_yaml(_skill_config(tmpl).read_text(encoding="utf-8")) + meta = FrontmatterParser.parse_yaml(_skill_config(tmpl).read_text(encoding="utf-8")) assert str(meta.get("name")) == name From 1bd9c6a79bcf6a253d7d76c5dc6b2c272c40e69e Mon Sep 17 00:00:00 2001 From: Erik Schaareman Date: Sun, 10 May 2026 00:14:55 +0200 Subject: [PATCH 18/25] fix(manifest): re-export removed helpers as deprecated shims preserve_existing_entry() and preserved_manifest_entries() were removed from vstack.manifest.__all__ as part of the OOP-cleanup refactor, but both are part of the public API and removing them is a backward-incompatible change for the current minor bump. Re-adds both as module-level wrapper functions in manifest/__init__.py that delegate to Manifest.preserve_existing_entry() and Manifest.preserved_entries() respectively and emit DeprecationWarning(stacklevel=2) so callers see the warning at their own call site. Both are listed in __all__ with a comment noting they will be dropped in the next minor release. Adds TestDeprecatedModuleLevelHelpers with three tests covering delegation, warning emission, and the None-manifest edge case. --- src/vstack/manifest/__init__.py | 51 +++++++++++++++++++++++++++++ tests/vstack/manifest/test_store.py | 38 +++++++++++++++++++++ 2 files changed, 89 insertions(+) diff --git a/src/vstack/manifest/__init__.py b/src/vstack/manifest/__init__.py index d6f26c0..2056599 100644 --- a/src/vstack/manifest/__init__.py +++ b/src/vstack/manifest/__init__.py @@ -1,5 +1,7 @@ """Manifest domain models and persistence helpers.""" +import warnings + from vstack.manifest.store import ( CURRENT_HASH_ALGORITHM, CURRENT_MANIFEST_VERSION, @@ -17,4 +19,53 @@ "ManifestFile", "content_hash", "hash_with_algorithm", + # Deprecated shims — removed from store.py in this release. Will be + # dropped in the next minor release. Use Manifest.preserved_entries() + # and Manifest.preserve_existing_entry() instead. + "preserve_existing_entry", + "preserved_manifest_entries", ] + + +def preserved_manifest_entries( + existing_manifest: "Manifest | None", + selected_manifest_keys: "set[str]", +) -> "dict[str, list[ArtifactEntry]]": + """Return artifact families not in *selected_manifest_keys*. + + .. deprecated:: + Use :meth:`Manifest.preserved_entries` instead. This module-level + wrapper will be removed in the next minor release. + """ + warnings.warn( + "preserved_manifest_entries() is deprecated; use Manifest.preserved_entries() instead.", + DeprecationWarning, + stacklevel=2, + ) + if existing_manifest is None: + return {} + return existing_manifest.preserved_entries(selected_manifest_keys) + + +def preserve_existing_entry( + *, + new_entries: "dict[str, list[ArtifactEntry]]", + manifest_key: str, + existing_entry: "ArtifactEntry", +) -> None: + """Carry forward one unchanged manifest entry into *new_entries*. + + .. deprecated:: + Use :meth:`Manifest.preserve_existing_entry` instead. This + module-level wrapper will be removed in the next minor release. + """ + warnings.warn( + "preserve_existing_entry() is deprecated; use Manifest.preserve_existing_entry() instead.", + DeprecationWarning, + stacklevel=2, + ) + Manifest.preserve_existing_entry( + new_entries=new_entries, + manifest_key=manifest_key, + existing_entry=existing_entry, + ) diff --git a/tests/vstack/manifest/test_store.py b/tests/vstack/manifest/test_store.py index 10ecb2c..0449d2f 100644 --- a/tests/vstack/manifest/test_store.py +++ b/tests/vstack/manifest/test_store.py @@ -16,6 +16,8 @@ Manifest, ManifestFile, hash_with_algorithm, + preserve_existing_entry, + preserved_manifest_entries, ) @@ -597,3 +599,39 @@ def test_read_none_when_manifest_entry_missing_required_file_key(self, tmp_path) encoding="utf-8", ) assert mf.read() is None + + +class TestDeprecatedModuleLevelHelpers: + """Deprecated module-level helpers re-export Manifest methods and warn.""" + + def test_preserved_manifest_entries_warns_and_delegates(self) -> None: + """preserved_manifest_entries() emits DeprecationWarning and returns correct result.""" + entry = ArtifactEntry(name="vision", file="docs/vision.md", checksum="abc") + manifest = Manifest( + vstack_version="0.1.0", + installed_at="2026-01-01T00:00:00Z", + artifacts={"skills": [entry], "agents": []}, + ) + with pytest.warns( + DeprecationWarning, match="preserved_manifest_entries\\(\\) is deprecated" + ): + result = preserved_manifest_entries(manifest, {"agents"}) + assert result == {"skills": [entry]} + + def test_preserved_manifest_entries_none_returns_empty(self) -> None: + """preserved_manifest_entries() returns {} when existing_manifest is None.""" + with pytest.warns(DeprecationWarning): + result = preserved_manifest_entries(None, {"agents"}) + assert result == {} + + def test_preserve_existing_entry_warns_and_delegates(self) -> None: + """preserve_existing_entry() emits DeprecationWarning and appends the entry.""" + entry = ArtifactEntry(name="vision", file="docs/vision.md", checksum="abc") + new_entries: dict = {} + with pytest.warns(DeprecationWarning, match="preserve_existing_entry\\(\\) is deprecated"): + preserve_existing_entry( + new_entries=new_entries, + manifest_key="skills", + existing_entry=entry, + ) + assert new_entries == {"skills": [entry]} From c66cd949a18488076ada4314b2764dd6338cf80c Mon Sep 17 00:00:00 2001 From: Erik Schaareman Date: Sun, 10 May 2026 00:15:10 +0200 Subject: [PATCH 19/25] docs(adr): align ADR-023 schema; mark ADR-026 shipped; update migrations README MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ADR-023: corrects section 2 — agents use defaults.handoffs.prompt (nested block), not a flat handoff_prompt: key. Adds a real config.yaml example. Marks section 4 (project-level artifact overrides) as deferred / not implemented in this release rather than describing an unimplemented feature as if it were shipped. ADR-026: updates status to accepted and documents the shipped vstack migrate command with its CLI flags (--target, --from, --to, --dry-run). src/vstack/_migrations/README.md: removes 'not yet implemented' language; describes the actual migrate command behaviour and flags. --- .../architecture/adr/023-workflow-contract.md | 45 ++++++++++--------- .../adr/026-docs-artifact-migration-policy.md | 39 +++++++++------- src/vstack/_migrations/README.md | 27 ++++++++--- 3 files changed, 69 insertions(+), 42 deletions(-) diff --git a/docs/architecture/adr/023-workflow-contract.md b/docs/architecture/adr/023-workflow-contract.md index af06974..eaec07f 100644 --- a/docs/architecture/adr/023-workflow-contract.md +++ b/docs/architecture/adr/023-workflow-contract.md @@ -114,10 +114,24 @@ by the agent and confirmed by the human — never inferred automatically. ### 2. Handoffs generated from workflow config -Agent `config.yaml` files drop the `handoffs:` block. In its place, each agent carries a single -`handoff_prompt:` string — the text to send to the next stage. The generator reads the workflow -config to determine the next stage's role name and combines it with the agent's `handoff_prompt` -to produce the full handoff block in the generated `.agent.md`. +Agent `config.yaml` files drop the top-level `handoffs:` block. In its place, each agent carries +a `defaults.handoffs.prompt` string — the text to send to the next stage. The generator reads the +workflow config to determine the next stage's role name and combines it with the agent's +`defaults.handoffs.prompt` to produce the full handoff block in the generated `.agent.md`. + +```yaml +# agent config.yaml +defaults: + handoffs: + prompt: > + Product outputs are approved. Assess the current state and produce or + update the architecture as needed. +``` + +When the workflow config has a `handoffs:` list for a stage, those entries drive the generated +handoffs directly; the agent's `defaults.handoffs.prompt` overrides the prompt of the first +workflow-level handoff that has no explicit `agent:` override, allowing per-template +customisation without editing the central config. When no workflow config is present (absent or empty `workflow:` block), the generator falls back to a generic label ("Continue to next stage") and omits the `agent:` field, preserving v3 behavior @@ -132,24 +146,13 @@ artifacts section. Agents are explicitly instructed to keep these files current. Artifacts without the flag are treated as deliverables — produced per session, not maintained indefinitely. -### 4. Project-level artifact overrides (overlay model) - -An `agents:` block may be added to `.vstack/config.yaml` to override per-agent artifact -configuration. Only delta entries need to be specified; omitting an agent means the template -default applies. The generator merges project overrides on top of template defaults at `init` -time. - -```yaml -agents: - product: - artifacts: - output: - - path: vision.md - baseline: true -``` +### 4. Project-level artifact overrides — deferred -This allows project teams to promote deliverables to baseline status without forking agent -templates. +An `agents:` overlay block in `.vstack/config.yaml` (allowing teams to promote deliverables to +baseline status or adjust artifact paths without forking templates) is **not implemented** in +this decision. It is reserved as a follow-on change once the workflow contract proves stable +in practice. Until then, teams that need per-project artifact customisation should fork the +relevant agent template. ## alternatives considered diff --git a/docs/architecture/adr/026-docs-artifact-migration-policy.md b/docs/architecture/adr/026-docs-artifact-migration-policy.md index 11ede00..4d888ca 100644 --- a/docs/architecture/adr/026-docs-artifact-migration-policy.md +++ b/docs/architecture/adr/026-docs-artifact-migration-policy.md @@ -4,6 +4,7 @@ **date:** 2026-05-09\ **status:** accepted\ +**updated:** 2026-05-10 — `vstack migrate` shipped in this version\ **depends on:** ADR-014 (manifest schema versioning), ADR-019 (vstack project directory), ADR-021 (config-driven artifact paths) ## context @@ -84,21 +85,31 @@ Two migration mechanisms exist, each scoped to its artifact class: `vstack manifest upgrade` already handles schema migrations and file relocation for `.github/` artifacts (ADR-017). Docs artifact migration is a separate concern with a separate command. -### 4. `vstack migrate` — deferred implementation, defined convention now +### 4. `vstack migrate` — implemented -The `vstack migrate` command is **not implemented** in this decision. The convention for -how it will work is defined here so that migration records can be authored now and executed -when the command ships. +The `vstack migrate` command is shipped as of this decision. Run it from the project root +after upgrading vstack to a new major version: + +``` +vstack migrate [--target ] [--from ] [--to ] [--dry-run] +``` + +| Flag | Description | +| ---------------- | --------------------------------------------------------------- | +| `--target ` | Project root to migrate (default: current directory) | +| `--from ` | Source major version (default: read from `.vstack/vstack.json`) | +| `--to ` | Target major version (default: current vstack package major) | +| `--dry-run` | Print planned moves without touching the filesystem | Migration records live in `src/vstack/_migrations/` within the package source. Each file covers one major version transition: ``` src/vstack/_migrations/ -└── v3_to_v4.yaml # applied when upgrading from any 3.x to 4.x +└── v2_to_v3.yaml # applied when upgrading from any 2.x to 3.x ``` -A migration record lists path moves by artifact class: +A migration record lists path moves: ```yaml from_version: "3.x" @@ -113,17 +124,13 @@ moves: migration. ``` -When `vstack migrate` ships, it will: - -1. Read the applicable migration record for the installed-to-installed version range. -1. For each `type: docs` move: check if the old path exists; if so, move it to the - new path (creating parent directories as needed), and report what was moved. -1. Print a summary and exit non-zero if any move failed. -1. Support `--dry-run` to preview moves without writing. +For each `type: docs` move, the command checks whether the old path exists; if so, it moves +the file to the new path (creating parent directories as needed) and reports the result. +Chained moves across multiple major versions are applied in order (e.g. v1→v2→v3 when +upgrading from major 1 to major 3). The command exits non-zero if any move fails. -Until the command ships, the migration record files serve as the canonical reference for -what a user must do manually when upgrading across major versions. `CHANGELOG.md` entries -for major releases must include a "Migration" section that lists the same moves in prose. +`CHANGELOG.md` entries for major releases must include a "Migration" section that restates +the same moves in prose for users who prefer to migrate manually. ### 5. Skill prose references are resolved at LLM runtime, not install time diff --git a/src/vstack/_migrations/README.md b/src/vstack/_migrations/README.md index 7f4db95..d5b9cf2 100644 --- a/src/vstack/_migrations/README.md +++ b/src/vstack/_migrations/README.md @@ -6,12 +6,29 @@ changes across major vstack version boundaries. Each file covers one major version transition and is named `v{M}_to_v{N}.yaml` where `M` is the source major version and `N` is the target major version. -These files are read by `vstack migrate` (not yet implemented — see ADR-026) to relocate -agent-owned docs files when their paths change between major versions. +These files are read by `vstack migrate` to relocate agent-owned docs files when their +paths change between major versions. -Until `vstack migrate` ships, use the moves listed here as the canonical reference for -manual migration steps. `CHANGELOG.md` for each major release must include a "Migration" -section that restates these moves in prose. +## Usage + +``` +vstack migrate [--target ] [--from ] [--to ] [--dry-run] +``` + +Run from the project root (or pass `--target`) after upgrading vstack to a new major +version. The command chains all necessary migration steps between the installed major +and the current package major. + +| Flag | Description | +| ---------------- | --------------------------------------------------------------- | +| `--target ` | Project root to migrate (default: current directory) | +| `--from ` | Source major version (default: read from `.vstack/vstack.json`) | +| `--to ` | Target major version (default: current vstack package major) | +| `--dry-run` | Print moves without touching the filesystem | + +`vstack migrate` only moves files that exist at the old path; absent files are silently +skipped. It reads `artifacts.root` from `.vstack/config.yaml` and adjusts destination +paths when the project uses a custom docs root. ## Schema From 695c06f1aa59153d4a3d956df457d4ddbd5916cf Mon Sep 17 00:00:00 2001 From: Erik Schaareman Date: Sun, 10 May 2026 00:15:19 +0200 Subject: [PATCH 20/25] chore(install): regenerate skills artifacts and manifest Regenerates all .github/skills/*/SKILL.md files and .vstack/vstack.json after the frontmatter serializer fix (dict/list metadata fields now emit valid block YAML instead of Python repr strings). --- .github/skills/adr/SKILL.md | 3 +- .github/skills/analyse/SKILL.md | 3 +- .github/skills/architecture/SKILL.md | 3 +- .github/skills/aws-cli/SKILL.md | 3 +- .github/skills/cicd/SKILL.md | 3 +- .github/skills/cloudformation/SKILL.md | 3 +- .github/skills/code-review/SKILL.md | 3 +- .github/skills/codeql/SKILL.md | 3 +- .github/skills/concise/SKILL.md | 3 +- .github/skills/consult/SKILL.md | 3 +- .github/skills/container/SKILL.md | 3 +- .github/skills/conventional-commit/SKILL.md | 3 +- .github/skills/debug/SKILL.md | 3 +- .github/skills/dependabot/SKILL.md | 3 +- .github/skills/dependency/SKILL.md | 3 +- .github/skills/design/SKILL.md | 3 +- .github/skills/docs/SKILL.md | 3 +- .github/skills/explore/SKILL.md | 3 +- .github/skills/gdpr/SKILL.md | 3 +- .github/skills/gh-issues/SKILL.md | 3 +- .github/skills/gh-release/SKILL.md | 3 +- .github/skills/guardrails/SKILL.md | 3 +- .github/skills/helm/SKILL.md | 3 +- .github/skills/incident/SKILL.md | 3 +- .github/skills/inspect/SKILL.md | 3 +- .github/skills/k8s/SKILL.md | 3 +- .github/skills/migrate/SKILL.md | 3 +- .github/skills/onboard/SKILL.md | 3 +- .github/skills/openapi/SKILL.md | 3 +- .github/skills/performance/SKILL.md | 3 +- .github/skills/postmortem/SKILL.md | 3 +- .github/skills/pr/SKILL.md | 3 +- .github/skills/rancher/SKILL.md | 3 +- .github/skills/rca/SKILL.md | 3 +- .github/skills/refactor/SKILL.md | 3 +- .github/skills/release-notes/SKILL.md | 3 +- .github/skills/requirements/SKILL.md | 3 +- .github/skills/secret-scan/SKILL.md | 3 +- .github/skills/security/SKILL.md | 3 +- .github/skills/terraform/SKILL.md | 3 +- .github/skills/terragrunt/SKILL.md | 3 +- .github/skills/threat-model/SKILL.md | 3 +- .github/skills/verify/SKILL.md | 3 +- .github/skills/vision/SKILL.md | 3 +- .vstack/vstack.json | 90 ++++++++++----------- 45 files changed, 133 insertions(+), 89 deletions(-) diff --git a/.github/skills/adr/SKILL.md b/.github/skills/adr/SKILL.md index 8a08e20..e0e9a3a 100644 --- a/.github/skills/adr/SKILL.md +++ b/.github/skills/adr/SKILL.md @@ -4,7 +4,8 @@ description: 'Architecture Decision Record writing. Documents a significant arch license: 'MIT' compatibility: 'Requires a skills-compatible agent with repository file access and terminal command execution when needed.' metadata: -{'owner': 'vstack', 'maturity': 'stable'} + owner: vstack + maturity: stable argument-hint: '[decision to record]' user-invocable: true disable-model-invocation: false diff --git a/.github/skills/analyse/SKILL.md b/.github/skills/analyse/SKILL.md index e7c422c..aca2109 100644 --- a/.github/skills/analyse/SKILL.md +++ b/.github/skills/analyse/SKILL.md @@ -4,7 +4,8 @@ description: 'Cross-cutting technical analysis. Investigates impact, tradeoffs, license: 'MIT' compatibility: 'Requires a skills-compatible agent with repository file access and terminal command execution when needed.' metadata: -{'owner': 'vstack', 'maturity': 'stable'} + owner: vstack + maturity: stable argument-hint: '[topic, change, or question to analyse]' user-invocable: true disable-model-invocation: false diff --git a/.github/skills/architecture/SKILL.md b/.github/skills/architecture/SKILL.md index 10dab85..2e4243e 100644 --- a/.github/skills/architecture/SKILL.md +++ b/.github/skills/architecture/SKILL.md @@ -4,7 +4,8 @@ description: 'Engineering-lead plan review. Lock in the execution plan — servi license: 'MIT' compatibility: 'Requires a skills-compatible agent with repository file access and terminal command execution when needed.' metadata: -{'owner': 'vstack', 'maturity': 'stable'} + owner: vstack + maturity: stable argument-hint: '[plan or system to review]' user-invocable: true disable-model-invocation: false diff --git a/.github/skills/aws-cli/SKILL.md b/.github/skills/aws-cli/SKILL.md index 8791d6f..25586cb 100644 --- a/.github/skills/aws-cli/SKILL.md +++ b/.github/skills/aws-cli/SKILL.md @@ -4,7 +4,8 @@ description: 'AWS CLI command reference and workflow patterns for backend engine license: 'MIT' compatibility: 'Requires AWS CLI v2 installed and configured (aws configure or environment variables). IAM permissions vary by operation — principle of least privilege applies.' metadata: -{'owner': 'vstack', 'maturity': 'stable'} + owner: vstack + maturity: stable argument-hint: '[service: iam | ec2 | s3 | rds | ecs | lambda | cloudwatch | ssm | secrets]' user-invocable: true disable-model-invocation: false diff --git a/.github/skills/cicd/SKILL.md b/.github/skills/cicd/SKILL.md index c9e38a5..70be947 100644 --- a/.github/skills/cicd/SKILL.md +++ b/.github/skills/cicd/SKILL.md @@ -4,7 +4,8 @@ description: 'Write GitHub Actions CI/CD workflow configuration. Covers build, t license: 'MIT' compatibility: 'Requires a skills-compatible agent with repository file access and terminal command execution when needed.' metadata: -{'owner': 'vstack', 'maturity': 'stable'} + owner: vstack + maturity: stable argument-hint: '[service or workflow to configure]' user-invocable: true disable-model-invocation: false diff --git a/.github/skills/cloudformation/SKILL.md b/.github/skills/cloudformation/SKILL.md index 3ce8f8e..a015aa9 100644 --- a/.github/skills/cloudformation/SKILL.md +++ b/.github/skills/cloudformation/SKILL.md @@ -4,7 +4,8 @@ description: 'Write, review, and refactor AWS CloudFormation templates. Covers t license: 'MIT' compatibility: 'Requires a skills-compatible agent with repository file access. Requires AWS CLI with appropriate IAM permissions for deploy and drift operations.' metadata: -{'owner': 'vstack', 'maturity': 'stable'} + owner: vstack + maturity: stable argument-hint: '[resource type or stack name, e.g. VPC | RDS | ECS service | Lambda function]' user-invocable: true disable-model-invocation: false diff --git a/.github/skills/code-review/SKILL.md b/.github/skills/code-review/SKILL.md index 41f83c9..979c681 100644 --- a/.github/skills/code-review/SKILL.md +++ b/.github/skills/code-review/SKILL.md @@ -4,7 +4,8 @@ description: 'Pre-landing code review. Finds bugs that pass CI but break in prod license: 'MIT' compatibility: 'Requires a skills-compatible agent with repository file access and terminal command execution when needed.' metadata: -{'owner': 'vstack', 'maturity': 'stable'} + owner: vstack + maturity: stable argument-hint: '[files, PR, or change to review]' user-invocable: true disable-model-invocation: false diff --git a/.github/skills/codeql/SKILL.md b/.github/skills/codeql/SKILL.md index ac86f7f..ce8f67a 100644 --- a/.github/skills/codeql/SKILL.md +++ b/.github/skills/codeql/SKILL.md @@ -4,7 +4,8 @@ description: 'Set up and configure CodeQL code scanning via GitHub Actions or th license: 'MIT' compatibility: 'Requires a skills-compatible agent with repository file access and terminal command execution. GitHub Advanced Security or public repository required for alert upload.' metadata: -{'owner': 'vstack', 'maturity': 'stable'} + owner: vstack + maturity: stable argument-hint: '[languages and setup type: default or advanced]' user-invocable: true disable-model-invocation: false diff --git a/.github/skills/concise/SKILL.md b/.github/skills/concise/SKILL.md index 25c8a8e..8ff9138 100644 --- a/.github/skills/concise/SKILL.md +++ b/.github/skills/concise/SKILL.md @@ -4,7 +4,8 @@ description: 'Runtime response-style controller for concise communication. Switc license: 'MIT' compatibility: 'Requires a skills-compatible agent with session memory and repository context.' metadata: -{'owner': 'vstack', 'maturity': 'stable'} + owner: vstack + maturity: stable argument-hint: '[normal|compact|ultra|status|on|off]' user-invocable: true disable-model-invocation: false diff --git a/.github/skills/consult/SKILL.md b/.github/skills/consult/SKILL.md index bc2a4d4..6532b9f 100644 --- a/.github/skills/consult/SKILL.md +++ b/.github/skills/consult/SKILL.md @@ -4,7 +4,8 @@ description: 'DX triage and focused review. First classifies whether the request license: 'MIT' compatibility: 'Requires a skills-compatible agent with repository file access and terminal command execution when needed.' metadata: -{'owner': 'vstack', 'maturity': 'stable'} + owner: vstack + maturity: stable argument-hint: '[API, tool, or workflow to consult]' user-invocable: true disable-model-invocation: false diff --git a/.github/skills/container/SKILL.md b/.github/skills/container/SKILL.md index bdc6d49..8f0a472 100644 --- a/.github/skills/container/SKILL.md +++ b/.github/skills/container/SKILL.md @@ -4,7 +4,8 @@ description: 'Write and review Dockerfile, docker-compose, and container configu license: 'MIT' compatibility: 'Requires a skills-compatible agent with repository file access and terminal command execution when needed.' metadata: -{'owner': 'vstack', 'maturity': 'stable'} + owner: vstack + maturity: stable argument-hint: '[service to containerise]' user-invocable: true disable-model-invocation: false diff --git a/.github/skills/conventional-commit/SKILL.md b/.github/skills/conventional-commit/SKILL.md index 2943ab6..5f66f2d 100644 --- a/.github/skills/conventional-commit/SKILL.md +++ b/.github/skills/conventional-commit/SKILL.md @@ -4,7 +4,8 @@ description: 'Prepare high-quality Conventional Commit messages from current sta license: 'MIT' compatibility: 'Requires a skills-compatible agent with repository file access and terminal command execution.' metadata: -{'owner': 'vstack', 'maturity': 'stable'} + owner: vstack + maturity: stable argument-hint: '[changes to commit and desired release intent]' user-invocable: true disable-model-invocation: false diff --git a/.github/skills/debug/SKILL.md b/.github/skills/debug/SKILL.md index 462e380..d808802 100644 --- a/.github/skills/debug/SKILL.md +++ b/.github/skills/debug/SKILL.md @@ -4,7 +4,8 @@ description: 'Systematic root-cause debugging for backend services, APIs, and li license: 'MIT' compatibility: 'Requires a skills-compatible agent with repository file access and terminal command execution when needed.' metadata: -{'owner': 'vstack', 'maturity': 'stable'} + owner: vstack + maturity: stable argument-hint: '[issue or error to debug]' user-invocable: true disable-model-invocation: false diff --git a/.github/skills/dependabot/SKILL.md b/.github/skills/dependabot/SKILL.md index 4db96bd..5fd9934 100644 --- a/.github/skills/dependabot/SKILL.md +++ b/.github/skills/dependabot/SKILL.md @@ -4,7 +4,8 @@ description: 'Create or optimize a Dependabot configuration file (.github/depend license: 'MIT' compatibility: 'Requires a skills-compatible agent with repository file access. Dependabot requires GitHub repository access (public or private with GitHub Advanced Security for private).' metadata: -{'owner': 'vstack', 'maturity': 'stable'} + owner: vstack + maturity: stable argument-hint: '[repository type: library | service | monorepo, and ecosystems to cover]' user-invocable: true disable-model-invocation: false diff --git a/.github/skills/dependency/SKILL.md b/.github/skills/dependency/SKILL.md index 33a9478..1b5db8d 100644 --- a/.github/skills/dependency/SKILL.md +++ b/.github/skills/dependency/SKILL.md @@ -4,7 +4,8 @@ description: 'Dependency health audit. Covers vulnerability scanning, outdated p license: 'MIT' compatibility: 'Requires a skills-compatible agent with repository file access and terminal command execution when needed.' metadata: -{'owner': 'vstack', 'maturity': 'stable'} + owner: vstack + maturity: stable argument-hint: '[project or package manifest to audit]' user-invocable: true disable-model-invocation: false diff --git a/.github/skills/design/SKILL.md b/.github/skills/design/SKILL.md index d8a1942..3d8428c 100644 --- a/.github/skills/design/SKILL.md +++ b/.github/skills/design/SKILL.md @@ -4,7 +4,8 @@ description: 'Build a complete API design or service design from scratch. Produc license: 'MIT' compatibility: 'Requires a skills-compatible agent with repository file access and terminal command execution when needed.' metadata: -{'owner': 'vstack', 'maturity': 'stable'} + owner: vstack + maturity: stable argument-hint: '[API or service to design]' user-invocable: true disable-model-invocation: false diff --git a/.github/skills/docs/SKILL.md b/.github/skills/docs/SKILL.md index 7b1dbd7..98ef5c2 100644 --- a/.github/skills/docs/SKILL.md +++ b/.github/skills/docs/SKILL.md @@ -4,7 +4,8 @@ description: 'Post-release documentation alignment. Updates README, API docs, mi license: 'MIT' compatibility: 'Requires a skills-compatible agent with repository file access and terminal command execution when needed.' metadata: -{'owner': 'vstack', 'maturity': 'stable'} + owner: vstack + maturity: stable argument-hint: '[release or change to document]' user-invocable: true disable-model-invocation: false diff --git a/.github/skills/explore/SKILL.md b/.github/skills/explore/SKILL.md index a4f93f6..5ef71cf 100644 --- a/.github/skills/explore/SKILL.md +++ b/.github/skills/explore/SKILL.md @@ -4,7 +4,8 @@ description: 'Repository and system discovery. Maps the architecture, understand license: 'MIT' compatibility: 'Requires a skills-compatible agent with repository file access and terminal command execution when needed.' metadata: -{'owner': 'vstack', 'maturity': 'stable'} + owner: vstack + maturity: stable argument-hint: '[repository or system to explore]' user-invocable: true disable-model-invocation: false diff --git a/.github/skills/gdpr/SKILL.md b/.github/skills/gdpr/SKILL.md index 551408b..387f14a 100644 --- a/.github/skills/gdpr/SKILL.md +++ b/.github/skills/gdpr/SKILL.md @@ -4,7 +4,8 @@ description: 'GDPR-compliant engineering practices for APIs, data models, authen license: 'MIT' compatibility: 'Requires a skills-compatible agent with repository file access.' metadata: -{'owner': 'vstack', 'maturity': 'stable'} + owner: vstack + maturity: stable argument-hint: '[component or feature: data model | API | logging | retention | erasure | infra | PR review]' user-invocable: true disable-model-invocation: false diff --git a/.github/skills/gh-issues/SKILL.md b/.github/skills/gh-issues/SKILL.md index 1fb4d25..9a6bede 100644 --- a/.github/skills/gh-issues/SKILL.md +++ b/.github/skills/gh-issues/SKILL.md @@ -4,7 +4,8 @@ description: 'Create, update, and manage GitHub issues using the gh CLI. Covers license: 'MIT' compatibility: 'Requires a skills-compatible agent with terminal command execution and GitHub CLI authentication (`gh auth status`).' metadata: -{'owner': 'vstack', 'maturity': 'stable'} + owner: vstack + maturity: stable argument-hint: '[what to create or which issue number to update]' user-invocable: true disable-model-invocation: false diff --git a/.github/skills/gh-release/SKILL.md b/.github/skills/gh-release/SKILL.md index c48d4d6..11960e8 100644 --- a/.github/skills/gh-release/SKILL.md +++ b/.github/skills/gh-release/SKILL.md @@ -4,7 +4,8 @@ description: 'Create or update a GitHub Release using the gh CLI from prepared r license: 'MIT' compatibility: 'Requires a skills-compatible agent with repository file access, terminal command execution, and GitHub CLI authentication (`gh auth status`).' metadata: -{'owner': 'vstack', 'maturity': 'stable'} + owner: vstack + maturity: stable argument-hint: '[version/tag and release notes source]' user-invocable: true disable-model-invocation: false diff --git a/.github/skills/guardrails/SKILL.md b/.github/skills/guardrails/SKILL.md index 11e9a58..3a85808 100644 --- a/.github/skills/guardrails/SKILL.md +++ b/.github/skills/guardrails/SKILL.md @@ -4,7 +4,8 @@ description: 'Activate safety guardrails for the current session. Before any des license: 'MIT' compatibility: 'Requires a skills-compatible agent with repository file access and terminal command execution when needed.' metadata: -{'owner': 'vstack', 'maturity': 'stable'} + owner: vstack + maturity: stable argument-hint: '[task]' user-invocable: true disable-model-invocation: true diff --git a/.github/skills/helm/SKILL.md b/.github/skills/helm/SKILL.md index f26f8c6..acea595 100644 --- a/.github/skills/helm/SKILL.md +++ b/.github/skills/helm/SKILL.md @@ -4,7 +4,8 @@ description: 'Write, review, and operate Helm charts and release lifecycles. Cov license: 'MIT' compatibility: 'Requires a skills-compatible agent with repository file access. Requires Helm CLI and target cluster access for live release operations.' metadata: -{'owner': 'vstack', 'maturity': 'stable'} + owner: vstack + maturity: stable argument-hint: '[chart path, release name, namespace, and scope: chart review | install | upgrade | rollback]' user-invocable: true disable-model-invocation: false diff --git a/.github/skills/incident/SKILL.md b/.github/skills/incident/SKILL.md index 2d1b5b5..7b17180 100644 --- a/.github/skills/incident/SKILL.md +++ b/.github/skills/incident/SKILL.md @@ -4,7 +4,8 @@ description: 'Incident analysis and coordination. Guides timeline reconstruction license: 'MIT' compatibility: 'Requires a skills-compatible agent with repository file access and terminal command execution when needed.' metadata: -{'owner': 'vstack', 'maturity': 'stable'} + owner: vstack + maturity: stable argument-hint: '[incident or outage to analyse]' user-invocable: true disable-model-invocation: false diff --git a/.github/skills/inspect/SKILL.md b/.github/skills/inspect/SKILL.md index b725b68..c1dba2d 100644 --- a/.github/skills/inspect/SKILL.md +++ b/.github/skills/inspect/SKILL.md @@ -4,7 +4,8 @@ description: 'Read-only verification audit. Runs baseline plus optional extended license: 'MIT' compatibility: 'Requires a skills-compatible agent with repository file access and terminal command execution when needed.' metadata: -{'owner': 'vstack', 'maturity': 'stable'} + owner: vstack + maturity: stable argument-hint: '[component or service to inspect]' user-invocable: true disable-model-invocation: false diff --git a/.github/skills/k8s/SKILL.md b/.github/skills/k8s/SKILL.md index 836db23..7375f27 100644 --- a/.github/skills/k8s/SKILL.md +++ b/.github/skills/k8s/SKILL.md @@ -4,7 +4,8 @@ description: 'Write, review, and troubleshoot Kubernetes manifests and operation license: 'MIT' compatibility: 'Requires a skills-compatible agent with repository file access. Requires kubectl access to a target cluster for live operations.' metadata: -{'owner': 'vstack', 'maturity': 'stable'} + owner: vstack + maturity: stable argument-hint: '[cluster/context, namespace, and scope: manifest review | deploy | rollout debug | hardening]' user-invocable: true disable-model-invocation: false diff --git a/.github/skills/migrate/SKILL.md b/.github/skills/migrate/SKILL.md index baf6b37..21f26b6 100644 --- a/.github/skills/migrate/SKILL.md +++ b/.github/skills/migrate/SKILL.md @@ -4,7 +4,8 @@ description: 'Database migration review and authoring. Covers forwards/backwards license: 'MIT' compatibility: 'Requires a skills-compatible agent with repository file access and terminal command execution when needed.' metadata: -{'owner': 'vstack', 'maturity': 'stable'} + owner: vstack + maturity: stable argument-hint: '[migration file or schema change to review]' user-invocable: true disable-model-invocation: false diff --git a/.github/skills/onboard/SKILL.md b/.github/skills/onboard/SKILL.md index cadc773..929214c 100644 --- a/.github/skills/onboard/SKILL.md +++ b/.github/skills/onboard/SKILL.md @@ -4,7 +4,8 @@ description: 'Generate a contributor onboarding guide for a repository. Covers p license: 'MIT' compatibility: 'Requires a skills-compatible agent with repository file access and terminal command execution when needed.' metadata: -{'owner': 'vstack', 'maturity': 'stable'} + owner: vstack + maturity: stable argument-hint: '[repository or service to document]' user-invocable: true disable-model-invocation: false diff --git a/.github/skills/openapi/SKILL.md b/.github/skills/openapi/SKILL.md index 2fbbad7..976d3ee 100644 --- a/.github/skills/openapi/SKILL.md +++ b/.github/skills/openapi/SKILL.md @@ -4,7 +4,8 @@ description: 'Write and review OpenAPI 3.1 specifications. Covers resource namin license: 'MIT' compatibility: 'Requires a skills-compatible agent with repository file access and terminal command execution when needed.' metadata: -{'owner': 'vstack', 'maturity': 'stable'} + owner: vstack + maturity: stable argument-hint: '[API or spec file to write or review]' user-invocable: true disable-model-invocation: false diff --git a/.github/skills/performance/SKILL.md b/.github/skills/performance/SKILL.md index e696ddf..26c45d2 100644 --- a/.github/skills/performance/SKILL.md +++ b/.github/skills/performance/SKILL.md @@ -4,7 +4,8 @@ description: 'Performance profiling and regression detection. Establishes baseli license: 'MIT' compatibility: 'Requires a skills-compatible agent with repository file access and terminal command execution when needed.' metadata: -{'owner': 'vstack', 'maturity': 'stable'} + owner: vstack + maturity: stable argument-hint: '[endpoint or function to profile]' user-invocable: true disable-model-invocation: false diff --git a/.github/skills/postmortem/SKILL.md b/.github/skills/postmortem/SKILL.md index 39ddb48..07df9dc 100644 --- a/.github/skills/postmortem/SKILL.md +++ b/.github/skills/postmortem/SKILL.md @@ -4,7 +4,8 @@ description: 'Blameless post-mortem writing for incidents. Produces a stakeholde license: 'MIT' compatibility: 'Requires a skills-compatible agent with repository file access and terminal command execution when needed.' metadata: -{'owner': 'vstack', 'maturity': 'stable'} + owner: vstack + maturity: stable argument-hint: '[incident to write a post-mortem for]' user-invocable: true disable-model-invocation: false diff --git a/.github/skills/pr/SKILL.md b/.github/skills/pr/SKILL.md index a84beb4..23c4910 100644 --- a/.github/skills/pr/SKILL.md +++ b/.github/skills/pr/SKILL.md @@ -4,7 +4,8 @@ description: 'Commit, push, and open a pull request from the current branch to m license: 'MIT' compatibility: 'Requires a skills-compatible agent with repository file access and terminal command execution when needed.' metadata: -{'owner': 'vstack', 'maturity': 'stable'} + owner: vstack + maturity: stable argument-hint: '[task]' user-invocable: true disable-model-invocation: false diff --git a/.github/skills/rancher/SKILL.md b/.github/skills/rancher/SKILL.md index 661d49e..17f1727 100644 --- a/.github/skills/rancher/SKILL.md +++ b/.github/skills/rancher/SKILL.md @@ -4,7 +4,8 @@ description: 'Operate Kubernetes workloads and governance through Rancher. Cover license: 'MIT' compatibility: 'Requires a skills-compatible agent with repository file access. Requires Rancher UI/API access or Rancher CLI where applicable.' metadata: -{'owner': 'vstack', 'maturity': 'stable'} + owner: vstack + maturity: stable argument-hint: '[rancher server/context, cluster/project, and scope: deploy | governance | fleet | troubleshooting]' user-invocable: true disable-model-invocation: false diff --git a/.github/skills/rca/SKILL.md b/.github/skills/rca/SKILL.md index 3faf325..e5114e9 100644 --- a/.github/skills/rca/SKILL.md +++ b/.github/skills/rca/SKILL.md @@ -4,7 +4,8 @@ description: 'Root cause analysis for incidents and bugs. Guides a systematic te license: 'MIT' compatibility: 'Requires a skills-compatible agent with repository file access and terminal command execution when needed.' metadata: -{'owner': 'vstack', 'maturity': 'stable'} + owner: vstack + maturity: stable argument-hint: '[incident or issue to analyse]' user-invocable: true disable-model-invocation: false diff --git a/.github/skills/refactor/SKILL.md b/.github/skills/refactor/SKILL.md index 328d628..efb2840 100644 --- a/.github/skills/refactor/SKILL.md +++ b/.github/skills/refactor/SKILL.md @@ -4,7 +4,8 @@ description: 'Structured refactoring for backend services, APIs, and libraries. license: 'MIT' compatibility: 'Requires a skills-compatible agent with repository file access and terminal command execution when needed.' metadata: -{'owner': 'vstack', 'maturity': 'stable'} + owner: vstack + maturity: stable argument-hint: '[module, file, or area to refactor]' user-invocable: true disable-model-invocation: false diff --git a/.github/skills/release-notes/SKILL.md b/.github/skills/release-notes/SKILL.md index 5be74fb..19081b7 100644 --- a/.github/skills/release-notes/SKILL.md +++ b/.github/skills/release-notes/SKILL.md @@ -4,7 +4,8 @@ description: 'Prepare release artifacts: verify all docs are present, write rele license: 'MIT' compatibility: 'Requires a skills-compatible agent with repository file access and terminal command execution when needed.' metadata: -{'owner': 'vstack', 'maturity': 'stable'} + owner: vstack + maturity: stable argument-hint: '[version or changes to release]' user-invocable: true disable-model-invocation: false diff --git a/.github/skills/requirements/SKILL.md b/.github/skills/requirements/SKILL.md index b8add1f..efe5ee4 100644 --- a/.github/skills/requirements/SKILL.md +++ b/.github/skills/requirements/SKILL.md @@ -4,7 +4,8 @@ description: 'Collaborative requirements gathering and documentation. Clarifies license: 'MIT' compatibility: 'Requires a skills-compatible agent with repository file access and terminal command execution when needed.' metadata: -{'owner': 'vstack', 'maturity': 'stable'} + owner: vstack + maturity: stable argument-hint: '[feature or system to document]' user-invocable: true disable-model-invocation: false diff --git a/.github/skills/secret-scan/SKILL.md b/.github/skills/secret-scan/SKILL.md index 8850777..a378d37 100644 --- a/.github/skills/secret-scan/SKILL.md +++ b/.github/skills/secret-scan/SKILL.md @@ -4,7 +4,8 @@ description: 'Configure and manage GitHub secret scanning and push protection. C license: 'MIT' compatibility: 'Requires repository access and GitHub Advanced Security (private repos) or public repository. Alert management requires gh CLI authentication.' metadata: -{'owner': 'vstack', 'maturity': 'stable'} + owner: vstack + maturity: stable argument-hint: '[scope: enable | configure push-protection | custom-pattern | triage alerts | remediate]' user-invocable: true disable-model-invocation: false diff --git a/.github/skills/security/SKILL.md b/.github/skills/security/SKILL.md index 73296d3..a237075 100644 --- a/.github/skills/security/SKILL.md +++ b/.github/skills/security/SKILL.md @@ -4,7 +4,8 @@ description: 'OWASP Top 10 + STRIDE security audit for APIs, services, and libra license: 'MIT' compatibility: 'Requires a skills-compatible agent with repository file access and terminal command execution when needed.' metadata: -{'owner': 'vstack', 'maturity': 'stable'} + owner: vstack + maturity: stable argument-hint: '[component or service to audit]' user-invocable: true disable-model-invocation: false diff --git a/.github/skills/terraform/SKILL.md b/.github/skills/terraform/SKILL.md index b8fe8d3..2554e0c 100644 --- a/.github/skills/terraform/SKILL.md +++ b/.github/skills/terraform/SKILL.md @@ -4,7 +4,8 @@ description: 'Write, review, and refactor Terraform infrastructure-as-code. Cove license: 'MIT' compatibility: 'Requires a skills-compatible agent with repository file access. Requires Terraform CLI installed for plan/apply operations.' metadata: -{'owner': 'vstack', 'maturity': 'stable'} + owner: vstack + maturity: stable argument-hint: '[provider: aws | azure | gcp | generic, and scope: new resource | module | state migration | security review]' user-invocable: true disable-model-invocation: false diff --git a/.github/skills/terragrunt/SKILL.md b/.github/skills/terragrunt/SKILL.md index 120791f..baccafa 100644 --- a/.github/skills/terragrunt/SKILL.md +++ b/.github/skills/terragrunt/SKILL.md @@ -4,7 +4,8 @@ description: 'Write, review, and refactor Terragrunt configurations for DRY mult license: 'MIT' compatibility: 'Requires a skills-compatible agent with repository file access. Requires Terraform CLI and Terragrunt installed for plan/apply operations.' metadata: -{'owner': 'vstack', 'maturity': 'stable'} + owner: vstack + maturity: stable argument-hint: '[scope: new layout | dependency graph | state migration | run-all workflow | security review]' user-invocable: true disable-model-invocation: false diff --git a/.github/skills/threat-model/SKILL.md b/.github/skills/threat-model/SKILL.md index cdf43c9..fb8e7ce 100644 --- a/.github/skills/threat-model/SKILL.md +++ b/.github/skills/threat-model/SKILL.md @@ -4,7 +4,8 @@ description: 'Threat modeling for APIs, services, and systems using a practical license: 'MIT' compatibility: 'Requires a skills-compatible agent with repository file access and terminal command execution when needed.' metadata: -{'owner': 'vstack', 'maturity': 'stable'} + owner: vstack + maturity: stable argument-hint: '[system, component, or architecture to threat model]' user-invocable: true disable-model-invocation: false diff --git a/.github/skills/verify/SKILL.md b/.github/skills/verify/SKILL.md index 88f2212..55cd71c 100644 --- a/.github/skills/verify/SKILL.md +++ b/.github/skills/verify/SKILL.md @@ -4,7 +4,8 @@ description: 'Verification fix-loop skill. Routes by mode (quick/standard/exhaus license: 'MIT' compatibility: 'Requires a skills-compatible agent with repository file access and terminal command execution when needed.' metadata: -{'owner': 'vstack', 'maturity': 'stable'} + owner: vstack + maturity: stable argument-hint: '[component or feature to verify]' user-invocable: true disable-model-invocation: false diff --git a/.github/skills/vision/SKILL.md b/.github/skills/vision/SKILL.md index 8e2ae65..2e0d25f 100644 --- a/.github/skills/vision/SKILL.md +++ b/.github/skills/vision/SKILL.md @@ -4,7 +4,8 @@ description: 'CEO/founder-mode plan review. Rethink the problem from first princ license: 'MIT' compatibility: 'Requires a skills-compatible agent with repository file access and terminal command execution when needed.' metadata: -{'owner': 'vstack', 'maturity': 'stable'} + owner: vstack + maturity: stable argument-hint: '[plan or idea to review]' user-invocable: true disable-model-invocation: false diff --git a/.vstack/vstack.json b/.vstack/vstack.json index dd42799..b9c002e 100644 --- a/.vstack/vstack.json +++ b/.vstack/vstack.json @@ -2,315 +2,315 @@ "manifest_version": 2, "hash_algorithm": "sha256", "vstack_version": "0.0.0.post3.dev0+df3fe6e", - "installed_at": "2026-05-09T00:23:22.770927+00:00", + "installed_at": "2026-05-09T21:42:06.107627+00:00", "artifacts": { "skills": [ { "name": "adr", "file": "skills/adr/SKILL.md", "version": "20260421003", - "checksum": "5ec10193062263b26df22b03af901b099ccc376e6b97be154e6d3fbc0a264152", + "checksum": "0838ffc14c5b86ea8b3df93cf6ce76c9bcd27b6c55c4ba47a3d01cc000fd7ee0", "checksum_algorithm": "sha256" }, { "name": "analyse", "file": "skills/analyse/SKILL.md", "version": "20260421004", - "checksum": "64aaf1b0f43af9fc47795b68810373ebacbce947792b385455753b6cde3c6d64", + "checksum": "8ff12f1d1f12ac9c46a2cb36981b85aeea97a8bc0876bbef37300511ea0eb7b3", "checksum_algorithm": "sha256" }, { "name": "architecture", "file": "skills/architecture/SKILL.md", "version": "20260421005", - "checksum": "a9f2eb0147b829fa8006d3af771035aa3bc1e396297d87ba8a51fe6fac064a12", + "checksum": "4ed22957e392aa89cf4949d2a88e707bf913948c2805a257039f5376810af754", "checksum_algorithm": "sha256" }, { "name": "aws-cli", "file": "skills/aws-cli/SKILL.md", "version": "20260502033", - "checksum": "782db163e9675417f4207a98fb49c935149bf20ac5bb03a0bf3f582181e64460", + "checksum": "e5b2688de029ab0cc6d3e3862237c5bdb7f3ad4aab9baa4e1bceac433d699b79", "checksum_algorithm": "sha256" }, { "name": "cicd", "file": "skills/cicd/SKILL.md", "version": "20260421006", - "checksum": "7e9613dfff757c57cbc1caf98d1833a3e480a601a0291abc69843f6ce730ee70", + "checksum": "ffe0df7fe8c425844e9fc1a20976a9d6c116d828af96c1a568317728bfdefdcf", "checksum_algorithm": "sha256" }, { "name": "cloudformation", "file": "skills/cloudformation/SKILL.md", "version": "20260502032", - "checksum": "d330ede035e42d98bb062a8ffa8583935101bf2090ffcc8ab716f490f5b14e85", + "checksum": "d172ffc2b30c75986446a72b625baecae123d1cc3882b20ca803c60b46dcd75d", "checksum_algorithm": "sha256" }, { "name": "code-review", "file": "skills/code-review/SKILL.md", "version": "20260421007", - "checksum": "bc514f210d6af855d0a8ed48004d086b9edc48b65d3935d27049c38c04555586", + "checksum": "5bcdddc03ce0a54997037210b38e7b4ed22828cfba6a76153afaa94cf616527a", "checksum_algorithm": "sha256" }, { "name": "codeql", "file": "skills/codeql/SKILL.md", "version": "20260502026", - "checksum": "a00605dfa317d9aecdaa43141a105313739c5400d02807f3f6f38ec3b586363f", + "checksum": "1b1b5800be204cc0e5dc6b4a8fcb9a7b916cfba2f96d5f99dbb6d7e213b8e95c", "checksum_algorithm": "sha256" }, { "name": "concise", "file": "skills/concise/SKILL.md", "version": "20260421008", - "checksum": "ca07cbf74c7e48ef71d0f7d027b2439bb3add4e452790c6a424a4c185637a92c", + "checksum": "3a07860ba6c83a97c9ad5496e124be4e5277fc0b811fe25dcd5bfdb7d8252b98", "checksum_algorithm": "sha256" }, { "name": "consult", "file": "skills/consult/SKILL.md", "version": "20260421009", - "checksum": "6c091de408c12a22500691d5fc83a3859feb3605e21608422a9d2e1b8413e22b", + "checksum": "90e1b0d0ff757c8e5879832c5ac6c41ec4054e9bff5b12a14531d427c09bcbad", "checksum_algorithm": "sha256" }, { "name": "container", "file": "skills/container/SKILL.md", "version": "20260421010", - "checksum": "f010f66e2ac5c56b280f7daa47fcd4ea47660829765cb0bd57bb8c310fb08e86", + "checksum": "e135b18cb972d30b77de4492db0437853ff1f49bad60a9113a034e39154d30e1", "checksum_algorithm": "sha256" }, { "name": "conventional-commit", "file": "skills/conventional-commit/SKILL.md", "version": "20260502024", - "checksum": "78a41fa28e30b079d049e38651cf939c2a692f81bc33221d672a3f56d1ddd5cd", + "checksum": "76477b42b17c9f6eec92baa36dc172e7ee98b20a86399eabdd2b7d2a90923509", "checksum_algorithm": "sha256" }, { "name": "debug", "file": "skills/debug/SKILL.md", "version": "20260421011", - "checksum": "4c3de6cd912ed32a3b862aa561976cb2a8dc996db713ac7b6b173357967db94d", + "checksum": "8e46a2723004bc86f6aee50f492b73045acdab66c964787c58a988c98684750b", "checksum_algorithm": "sha256" }, { "name": "dependabot", "file": "skills/dependabot/SKILL.md", "version": "20260502027", - "checksum": "ea8b610b811bd86de09e26518971e3cb089854e18be21c280c91bd81901a2522", + "checksum": "3ce5836bf870f73805800f672d379aa10f5aff5a522017a77875d9339bb99984", "checksum_algorithm": "sha256" }, { "name": "dependency", "file": "skills/dependency/SKILL.md", "version": "20260421012", - "checksum": "e930b6807ec3cc270414a6d9d8b750202375ae70f88cd776a57a5be0d0a078bf", + "checksum": "46e75e8a28b1af5a60da7d9b3d1e46f0914b2f10d8733b37e6577e62b0074724", "checksum_algorithm": "sha256" }, { "name": "design", "file": "skills/design/SKILL.md", "version": "20260421013", - "checksum": "6be33bc6967e9e3477d8cf1a7cb197cb2732b3db17fa7e67cf859bf109cfa40a", + "checksum": "e21a674324244412f7b4a5bc010da4a7b9dfcf3522d54281bc9cf69f0b1126bd", "checksum_algorithm": "sha256" }, { "name": "docs", "file": "skills/docs/SKILL.md", "version": "20260421014", - "checksum": "bbf7626d35e90ed66be9637c5aa8cf1612a86e961956dc825ba43d1ac17ef202", + "checksum": "7d34421800bf04fe5a0782abfc5702a421419d31a5c66fd495d7dc8a6e2c64e3", "checksum_algorithm": "sha256" }, { "name": "explore", "file": "skills/explore/SKILL.md", "version": "20260421015", - "checksum": "60b3ebf52e96703b4cf625098a4b4b89720c5f265f863faf306148515109147f", + "checksum": "a257941d7b577f782b5d41703b1fbd25c5ab13f664e696d83cc3a9fd91a5565f", "checksum_algorithm": "sha256" }, { "name": "gdpr", "file": "skills/gdpr/SKILL.md", "version": "20260502029", - "checksum": "2d72a91b16b94957996da61a84dac58481d5b11d1f38a44f81440ddb5a4c39e0", + "checksum": "94207650498b56f246392449f57b4af4659bdfe511e7a6c2f23022ff2f68b08d", "checksum_algorithm": "sha256" }, { "name": "gh-issues", "file": "skills/gh-issues/SKILL.md", "version": "20260502025", - "checksum": "5fc71ec5dfad0012ef4ead9badef7ab45e2bed09950e831bbcadb1be1a3a4ef7", + "checksum": "7a2b9b7463a40436fe77c77d066dd455ddb4abef877726ba041b89cbaf030280", "checksum_algorithm": "sha256" }, { "name": "gh-release", "file": "skills/gh-release/SKILL.md", "version": "20260502023", - "checksum": "97a7e2bfb578613ac02b2e420a527604ff7e336918bdc49b0778e25b90442687", + "checksum": "beed7f98cb52222f688fed5a83d376acea1215c6654c570487412792bb577462", "checksum_algorithm": "sha256" }, { "name": "guardrails", "file": "skills/guardrails/SKILL.md", "version": "20260421016", - "checksum": "b116ea94fcea4264a1ebc28d42ad6b410271ddf7c85382411199a6f49eecf139", + "checksum": "8ec7213e1f8c85b4975ebb032d897e84e372c0279e3265da1098fc86cf98695f", "checksum_algorithm": "sha256" }, { "name": "helm", "file": "skills/helm/SKILL.md", "version": "20260502037", - "checksum": "2b0377609295dc99820d7c1ef53258e1b2f4e7f1e75ec648558b82c4a7339d6d", + "checksum": "13600308860723f683802431ac7a8c2c3834c40e521055854cdf3d3290570689", "checksum_algorithm": "sha256" }, { "name": "incident", "file": "skills/incident/SKILL.md", "version": "20260503002", - "checksum": "a7a9c217e9df69a72a900c53c3668f8490660b891c31dbc09524dc38cd584699", + "checksum": "d337c4f145c09af856f4a05f41b11f2e46f3822f77e067e1a6e39de5f2af1fe8", "checksum_algorithm": "sha256" }, { "name": "inspect", "file": "skills/inspect/SKILL.md", "version": "20260421018", - "checksum": "55f42917225d206f01086d2e1bbbe08397cdcb88c05f1a0b5737630cd3256f1c", + "checksum": "7997cb4477f9b1a8a6753082a4f3ea0c9b5a1a77520a3f20527f8c153c968a27", "checksum_algorithm": "sha256" }, { "name": "k8s", "file": "skills/k8s/SKILL.md", "version": "20260502036", - "checksum": "4f264e77f40a607c65b404d0afc856d4c5c770257995191d8dfb3277836ace39", + "checksum": "d178a4d86f6ca5f4144cd8212eaf09979bb48afa55cb482409609aa50c5f0f88", "checksum_algorithm": "sha256" }, { "name": "migrate", "file": "skills/migrate/SKILL.md", "version": "20260421019", - "checksum": "95af7429d6ed41c5757bf7a1d9e4b7a2765d71ee4ee0ff591105c55f48126a24", + "checksum": "c6c18c750319c0feb14526d637cb591f51ffde341b1385b4063aeb1edf12fd8b", "checksum_algorithm": "sha256" }, { "name": "onboard", "file": "skills/onboard/SKILL.md", "version": "20260421020", - "checksum": "506870d17780c4f2e7bb85245ab4056746e6759f5f28c00914c6f26feaefdf4f", + "checksum": "b96fb4067121eabbdf686641b55d32f253282e0b734fa4b2df6e4a9e85938cde", "checksum_algorithm": "sha256" }, { "name": "openapi", "file": "skills/openapi/SKILL.md", "version": "20260421021", - "checksum": "dc721152115a5f347cb780511a72b09420abfeb1b67236b5f6848dd2bfad3ada", + "checksum": "75d7cab59b1cde6dc91490867eb8537821059960957ba21ebd24d922e9fe7a91", "checksum_algorithm": "sha256" }, { "name": "performance", "file": "skills/performance/SKILL.md", "version": "20260421022", - "checksum": "24dd38ad95167be5fda2744abc717da23426457a476cae0c4d6e5afb222f9d89", + "checksum": "e8cf2eba2a3fdc162d03afeaba260bd0c2f0aac47121c26dc9d63614bff971ec", "checksum_algorithm": "sha256" }, { "name": "postmortem", "file": "skills/postmortem/SKILL.md", "version": "20260503001", - "checksum": "a1ff3b06292f5131b751ede987a8b380a5ccced6a6a7c6b0dbfebde549d25ee9", + "checksum": "ead97f2a5b28672f4ac483e467eec5b0d2a7e3b96eceaaa2017c918972c2d827", "checksum_algorithm": "sha256" }, { "name": "pr", "file": "skills/pr/SKILL.md", "version": "20260502013", - "checksum": "8b25caca722cf9781c8d2fab87548071f3dbfd63fab847a0081849f0df6c3c38", + "checksum": "ed4b13b69b325b21d9abf17ae9be34c2b2fa6ff1dd78cc6ee04af73d428438b8", "checksum_algorithm": "sha256" }, { "name": "rancher", "file": "skills/rancher/SKILL.md", "version": "20260502038", - "checksum": "d5f4517005e3829a3a0503fdc4e15814993b6d2bbf85a92512b6e46b25d65037", + "checksum": "c86759f257553554f2069cb6d0eda93bd98fd5c2fb251b1c8cc423302f33b01d", "checksum_algorithm": "sha256" }, { "name": "rca", "file": "skills/rca/SKILL.md", "version": "20260503001", - "checksum": "cc022ebca687f463ca88002db04dbd3af720ba2c9d9810fc441cd8fc5e29e947", + "checksum": "4d2347e1bab1d0717669b78160a5703ba4d1bb3486a9499924ca569e6f049f0e", "checksum_algorithm": "sha256" }, { "name": "refactor", "file": "skills/refactor/SKILL.md", "version": "20260421023", - "checksum": "c7741be01bfca5bf77727cb21b6b00c939fee8a61160fe0e66d21ceedc7124f2", + "checksum": "7559d9148ee0ed7434162f672fe47329a417fd332af92a3637c47030e6b8f271", "checksum_algorithm": "sha256" }, { "name": "release-notes", "file": "skills/release-notes/SKILL.md", "version": "20260502014", - "checksum": "2a76cf7833e7bfd30977162102b94f7e7253b174ea263a57aa5e0b04dc669582", + "checksum": "81fd326b93789aad264e1ad27a73d00efd76c4486b5a20935bafcb18ebd43758", "checksum_algorithm": "sha256" }, { "name": "requirements", "file": "skills/requirements/SKILL.md", "version": "20260421024", - "checksum": "66716f4e09c4ab820ced6d4bf3b495d13ec2701d1c1544556f0a804c4850ab7a", + "checksum": "312e1c24bc43828798e86b881204196ebcad4f90ff5cfa63d099b8836e4f1e49", "checksum_algorithm": "sha256" }, { "name": "secret-scan", "file": "skills/secret-scan/SKILL.md", "version": "20260502028", - "checksum": "5fcb8afd3547448ee41cba699453f4c572072a424cf2eaaf521019c79c80fd2f", + "checksum": "7a83d376b062bcf497ea89dd1e44a8f9fb945349d6b66513018ce1abe265e277", "checksum_algorithm": "sha256" }, { "name": "security", "file": "skills/security/SKILL.md", "version": "20260421025", - "checksum": "412dad2ff508306160c427e5c09cf80693d1d716d61452352dc218512581754e", + "checksum": "f58a629804bec6f6b5839eea1e7754458bf85cceb7403b9470f80513119a306d", "checksum_algorithm": "sha256" }, { "name": "terraform", "file": "skills/terraform/SKILL.md", "version": "20260502030", - "checksum": "46e7c27ca0421a0862f307566cedb864fb2c4d3067a30b4c9bfa65a1b06111fa", + "checksum": "b24b008c171aafbf81902b3f52661b4acc09e8980e42ebf579ac3566dc138de5", "checksum_algorithm": "sha256" }, { "name": "terragrunt", "file": "skills/terragrunt/SKILL.md", "version": "20260502031", - "checksum": "b518c7e63a1a83fc82578a549caa6eac622feffb4a728046fd47ed9761f2fe2d", + "checksum": "33a208ccc4dfff01b59110445e0fd851e5c9d5e094711456c1cd1efdc68d440d", "checksum_algorithm": "sha256" }, { "name": "threat-model", "file": "skills/threat-model/SKILL.md", "version": "20260502021", - "checksum": "43c853ab3baa99b25adec0570395a39d309222616c2844c017469302340d0b79", + "checksum": "11504d508ddfc6e9e2ded8dfac8fe6fb7af691c7ef27fdc539c5f8f870fc06bb", "checksum_algorithm": "sha256" }, { "name": "verify", "file": "skills/verify/SKILL.md", "version": "20260421026", - "checksum": "1fcec13deb1316dcc55050108f394ae8878e45c009304179a81da49bc512354c", + "checksum": "b8f80fda903c0d55c556374626e308dcc61787d59e2e642f9c9513d06b0ac2b9", "checksum_algorithm": "sha256" }, { "name": "vision", "file": "skills/vision/SKILL.md", "version": "20260421027", - "checksum": "ca6c5fc7b3b7800e858e80631a36c4839f7364e89246fe04c412b7c7fd4a4520", + "checksum": "a74271c10816e3cee75fc1489d0b2defb391035b22f8d3da0cc951ae1afb7ef2", "checksum_algorithm": "sha256" } ], From a45c1e187ee8301dbdc9b3f21cdf64db1956a119 Mon Sep 17 00:00:00 2001 From: Erik Schaareman Date: Sun, 10 May 2026 00:50:31 +0200 Subject: [PATCH 21/25] feat(agents): add missing execute/web tools to product, architect, designer, release MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit product, architect, and designer agents lacked execute, preventing them from running terminal commands (find, git log, OpenAPI validators, doc aggregation). release lacked web, needed for CI-status, GitHub release, and PyPI checks. All six agents now have: read search edit execute web vscode todo agent. The agent tool is already present everywhere — subagent invocations are already wired correctly via the agents: ["*"] field. --- src/vstack/_templates/agents/architect/config.yaml | 1 + src/vstack/_templates/agents/designer/config.yaml | 1 + src/vstack/_templates/agents/product/config.yaml | 1 + src/vstack/_templates/agents/release/config.yaml | 1 + 4 files changed, 4 insertions(+) diff --git a/src/vstack/_templates/agents/architect/config.yaml b/src/vstack/_templates/agents/architect/config.yaml index b8129c2..e36ad57 100644 --- a/src/vstack/_templates/agents/architect/config.yaml +++ b/src/vstack/_templates/agents/architect/config.yaml @@ -11,6 +11,7 @@ tools: - read - search - edit + - execute - web - vscode - todo diff --git a/src/vstack/_templates/agents/designer/config.yaml b/src/vstack/_templates/agents/designer/config.yaml index cf1e64e..3408cd4 100644 --- a/src/vstack/_templates/agents/designer/config.yaml +++ b/src/vstack/_templates/agents/designer/config.yaml @@ -10,6 +10,7 @@ tools: - read - search - edit + - execute - web - vscode - todo diff --git a/src/vstack/_templates/agents/product/config.yaml b/src/vstack/_templates/agents/product/config.yaml index 625a7c5..0b7b554 100644 --- a/src/vstack/_templates/agents/product/config.yaml +++ b/src/vstack/_templates/agents/product/config.yaml @@ -10,6 +10,7 @@ tools: - read - search - edit + - execute - web - vscode - todo diff --git a/src/vstack/_templates/agents/release/config.yaml b/src/vstack/_templates/agents/release/config.yaml index b3c71b0..23debee 100644 --- a/src/vstack/_templates/agents/release/config.yaml +++ b/src/vstack/_templates/agents/release/config.yaml @@ -11,6 +11,7 @@ tools: - search - edit - execute + - web - vscode - todo - agent From 2285ce9f11fcd1563f08d146157f955c0ed24e15 Mon Sep 17 00:00:00 2001 From: Erik Schaareman Date: Sun, 10 May 2026 00:50:42 +0200 Subject: [PATCH 22/25] feat(skills): add allowed-tools field to SKILL_SCHEMA and all 44 skill configs The agentskills.io spec defines allowed-tools as an experimental field that pre-approves a space-separated set of tool names for a skill, preventing scope creep when a skill is loaded into an agent that has broader permissions. SKILL_SCHEMA gains a new FieldSpec("allowed-tools") entry, slotted between the metadata and argument-hint fields. Skill tool tiers: - read-only / analysis (analyse, code-review, concise, consult, explore, inspect): execute read search - network-dependent (aws-cli, codeql, dependency, gh-issues, gh-release, pr, secret-scan): execute read search edit web - all others (31 skills): execute read search edit --- src/vstack/_templates/skills/adr/config.yaml | 1 + src/vstack/_templates/skills/analyse/config.yaml | 1 + src/vstack/_templates/skills/architecture/config.yaml | 1 + src/vstack/_templates/skills/aws-cli/config.yaml | 1 + src/vstack/_templates/skills/cicd/config.yaml | 1 + src/vstack/_templates/skills/cloudformation/config.yaml | 1 + src/vstack/_templates/skills/code-review/config.yaml | 1 + src/vstack/_templates/skills/codeql/config.yaml | 1 + src/vstack/_templates/skills/concise/config.yaml | 1 + src/vstack/_templates/skills/consult/config.yaml | 1 + src/vstack/_templates/skills/container/config.yaml | 1 + src/vstack/_templates/skills/conventional-commit/config.yaml | 1 + src/vstack/_templates/skills/debug/config.yaml | 1 + src/vstack/_templates/skills/dependabot/config.yaml | 1 + src/vstack/_templates/skills/dependency/config.yaml | 1 + src/vstack/_templates/skills/design/config.yaml | 1 + src/vstack/_templates/skills/docs/config.yaml | 1 + src/vstack/_templates/skills/explore/config.yaml | 1 + src/vstack/_templates/skills/gdpr/config.yaml | 1 + src/vstack/_templates/skills/gh-issues/config.yaml | 1 + src/vstack/_templates/skills/gh-release/config.yaml | 1 + src/vstack/_templates/skills/guardrails/config.yaml | 1 + src/vstack/_templates/skills/helm/config.yaml | 1 + src/vstack/_templates/skills/incident/config.yaml | 1 + src/vstack/_templates/skills/inspect/config.yaml | 1 + src/vstack/_templates/skills/k8s/config.yaml | 1 + src/vstack/_templates/skills/migrate/config.yaml | 1 + src/vstack/_templates/skills/onboard/config.yaml | 1 + src/vstack/_templates/skills/openapi/config.yaml | 1 + src/vstack/_templates/skills/performance/config.yaml | 1 + src/vstack/_templates/skills/postmortem/config.yaml | 1 + src/vstack/_templates/skills/pr/config.yaml | 1 + src/vstack/_templates/skills/rancher/config.yaml | 1 + src/vstack/_templates/skills/rca/config.yaml | 1 + src/vstack/_templates/skills/refactor/config.yaml | 1 + src/vstack/_templates/skills/release-notes/config.yaml | 1 + src/vstack/_templates/skills/requirements/config.yaml | 1 + src/vstack/_templates/skills/secret-scan/config.yaml | 1 + src/vstack/_templates/skills/security/config.yaml | 1 + src/vstack/_templates/skills/terraform/config.yaml | 1 + src/vstack/_templates/skills/terragrunt/config.yaml | 1 + src/vstack/_templates/skills/threat-model/config.yaml | 1 + src/vstack/_templates/skills/verify/config.yaml | 1 + src/vstack/_templates/skills/vision/config.yaml | 1 + src/vstack/skills/config.py | 2 ++ 45 files changed, 46 insertions(+) diff --git a/src/vstack/_templates/skills/adr/config.yaml b/src/vstack/_templates/skills/adr/config.yaml index 912219f..c8e1da0 100644 --- a/src/vstack/_templates/skills/adr/config.yaml +++ b/src/vstack/_templates/skills/adr/config.yaml @@ -6,6 +6,7 @@ description: | Use when asked to "write an ADR", "document this decision", "record why we chose X", or when a significant technical decision needs a permanent record. Runs after a decision is made or while evaluating options. +allowed-tools: 'execute read search edit' argument-hint: '[decision to record]' license: MIT diff --git a/src/vstack/_templates/skills/analyse/config.yaml b/src/vstack/_templates/skills/analyse/config.yaml index b7d5f98..039d4d1 100644 --- a/src/vstack/_templates/skills/analyse/config.yaml +++ b/src/vstack/_templates/skills/analyse/config.yaml @@ -5,6 +5,7 @@ description: | or feasibility without implementing changes. Use when asked to "analyse this", "investigate the impact", "what are the tradeoffs", "root cause analysis", "is this feasible?", or "compare these approaches". Produces an analysis report. +allowed-tools: 'execute read search' argument-hint: '[topic, change, or question to analyse]' license: MIT diff --git a/src/vstack/_templates/skills/architecture/config.yaml b/src/vstack/_templates/skills/architecture/config.yaml index 585a286..54b08d6 100644 --- a/src/vstack/_templates/skills/architecture/config.yaml +++ b/src/vstack/_templates/skills/architecture/config.yaml @@ -7,6 +7,7 @@ description: | issues with opinionated recommendations. Use when asked to "review the architecture", "engineering review", or "lock in the plan". Proactively suggest when the user has a plan and is about to start coding — catch architecture issues before implementation. +allowed-tools: 'execute read search edit' argument-hint: '[plan or system to review]' license: MIT diff --git a/src/vstack/_templates/skills/aws-cli/config.yaml b/src/vstack/_templates/skills/aws-cli/config.yaml index 4d00d35..5188899 100644 --- a/src/vstack/_templates/skills/aws-cli/config.yaml +++ b/src/vstack/_templates/skills/aws-cli/config.yaml @@ -6,6 +6,7 @@ description: | SSM Parameter Store, and cross-account operations. Use when asked to "query AWS", "list resources", "rotate secrets", "check CloudWatch logs", "scale ECS", "run an SSM command", or "script an AWS operation". +allowed-tools: 'execute read search edit web' argument-hint: '[service: iam | ec2 | s3 | rds | ecs | lambda | cloudwatch | ssm | secrets]' license: MIT diff --git a/src/vstack/_templates/skills/cicd/config.yaml b/src/vstack/_templates/skills/cicd/config.yaml index b7769bf..ab961a0 100644 --- a/src/vstack/_templates/skills/cicd/config.yaml +++ b/src/vstack/_templates/skills/cicd/config.yaml @@ -5,6 +5,7 @@ description: | security scan, container publish, and deployment trigger workflows. Use when asked to "write a CI pipeline", "set up GitHub Actions", "add a workflow", or "configure CD". +allowed-tools: 'execute read search edit' argument-hint: '[service or workflow to configure]' license: MIT diff --git a/src/vstack/_templates/skills/cloudformation/config.yaml b/src/vstack/_templates/skills/cloudformation/config.yaml index 533b2e4..5e07388 100644 --- a/src/vstack/_templates/skills/cloudformation/config.yaml +++ b/src/vstack/_templates/skills/cloudformation/config.yaml @@ -7,6 +7,7 @@ description: | configuration, and security hardening. Use when asked to "write a CloudFormation template", "review this CFN stack", "create a SAM template", "add a CloudFormation resource", or "migrate from CDK to CloudFormation". +allowed-tools: 'execute read search edit' argument-hint: '[resource type or stack name, e.g. VPC | RDS | ECS service | Lambda function]' license: MIT diff --git a/src/vstack/_templates/skills/code-review/config.yaml b/src/vstack/_templates/skills/code-review/config.yaml index 7cfc74f..c91e15b 100644 --- a/src/vstack/_templates/skills/code-review/config.yaml +++ b/src/vstack/_templates/skills/code-review/config.yaml @@ -5,6 +5,7 @@ description: | race conditions, missing error handling, API contract violations, observability gaps, security issues, and performance landmines. Use when asked to "review", "code review", "review this PR", or before merging. +allowed-tools: 'execute read search' argument-hint: '[files, PR, or change to review]' license: MIT diff --git a/src/vstack/_templates/skills/codeql/config.yaml b/src/vstack/_templates/skills/codeql/config.yaml index 8d6a759..efb5ea4 100644 --- a/src/vstack/_templates/skills/codeql/config.yaml +++ b/src/vstack/_templates/skills/codeql/config.yaml @@ -6,6 +6,7 @@ description: | configuration, SARIF output, and alert triage. Use when asked to "set up CodeQL", "configure code scanning", "add a codeql workflow", or "scan for vulnerabilities with CodeQL". +allowed-tools: 'execute read search edit web' argument-hint: '[languages and setup type: default or advanced]' license: MIT diff --git a/src/vstack/_templates/skills/concise/config.yaml b/src/vstack/_templates/skills/concise/config.yaml index e932117..c5d4f9f 100644 --- a/src/vstack/_templates/skills/concise/config.yaml +++ b/src/vstack/_templates/skills/concise/config.yaml @@ -5,6 +5,7 @@ description: | normal, compact, and ultra output density without regenerating agents. Use when asked for shorter responses, token efficiency, or to check active style mode. +allowed-tools: 'execute read search' argument-hint: '[normal|compact|ultra|status|on|off]' license: MIT diff --git a/src/vstack/_templates/skills/consult/config.yaml b/src/vstack/_templates/skills/consult/config.yaml index c604164..496b8fc 100644 --- a/src/vstack/_templates/skills/consult/config.yaml +++ b/src/vstack/_templates/skills/consult/config.yaml @@ -7,6 +7,7 @@ description: | specialized skill (analyse/debug/security/performance/design/verify/code-review). Use when asked to "review DX", "review API usability", "review CLI experience", or "review developer workflow friction". +allowed-tools: 'execute read search' argument-hint: '[API, tool, or workflow to consult]' license: MIT diff --git a/src/vstack/_templates/skills/container/config.yaml b/src/vstack/_templates/skills/container/config.yaml index 4511551..70b402f 100644 --- a/src/vstack/_templates/skills/container/config.yaml +++ b/src/vstack/_templates/skills/container/config.yaml @@ -6,6 +6,7 @@ description: | layer optimisation, and local development compose setup. Use when asked to "containerise", "write a Dockerfile", "add docker-compose", or "harden the container image". +allowed-tools: 'execute read search edit' argument-hint: '[service to containerise]' license: MIT diff --git a/src/vstack/_templates/skills/conventional-commit/config.yaml b/src/vstack/_templates/skills/conventional-commit/config.yaml index faf8d21..cc6377a 100644 --- a/src/vstack/_templates/skills/conventional-commit/config.yaml +++ b/src/vstack/_templates/skills/conventional-commit/config.yaml @@ -7,6 +7,7 @@ description: | non-compliant messages before commit. Use when asked to "write a commit message", "make a conventional commit", or "prepare commits before PR". +allowed-tools: 'execute read search edit' argument-hint: '[changes to commit and desired release intent]' license: MIT diff --git a/src/vstack/_templates/skills/debug/config.yaml b/src/vstack/_templates/skills/debug/config.yaml index b2e9920..e6cfed5 100644 --- a/src/vstack/_templates/skills/debug/config.yaml +++ b/src/vstack/_templates/skills/debug/config.yaml @@ -5,6 +5,7 @@ description: | No fixes without investigation. Follows the scientific method: observe → hypothesize → test → conclude → fix → prevent. Use when asked to "debug", "investigate", "find the root cause", or "why is this broken?". +allowed-tools: 'execute read search edit' argument-hint: '[issue or error to debug]' license: MIT diff --git a/src/vstack/_templates/skills/dependabot/config.yaml b/src/vstack/_templates/skills/dependabot/config.yaml index d6a7c82..1219d81 100644 --- a/src/vstack/_templates/skills/dependabot/config.yaml +++ b/src/vstack/_templates/skills/dependabot/config.yaml @@ -6,6 +6,7 @@ description: | update configuration, schedule optimization, and PR customization. Use when asked to "set up Dependabot", "configure dependency updates", "add dependabot.yml", or "reduce Dependabot PR noise". +allowed-tools: 'execute read search edit' argument-hint: '[repository type: library | service | monorepo, and ecosystems to cover]' license: MIT diff --git a/src/vstack/_templates/skills/dependency/config.yaml b/src/vstack/_templates/skills/dependency/config.yaml index d9e47a6..c9a5f94 100644 --- a/src/vstack/_templates/skills/dependency/config.yaml +++ b/src/vstack/_templates/skills/dependency/config.yaml @@ -7,6 +7,7 @@ description: | licence obligations, and long-term dependency health. Use when asked to "audit dependencies", "check for outdated packages", "licence compliance", "pin versions", or "dependency health check". +allowed-tools: 'execute read search edit web' argument-hint: '[project or package manifest to audit]' license: MIT diff --git a/src/vstack/_templates/skills/design/config.yaml b/src/vstack/_templates/skills/design/config.yaml index be7eca5..95858ce 100644 --- a/src/vstack/_templates/skills/design/config.yaml +++ b/src/vstack/_templates/skills/design/config.yaml @@ -5,6 +5,7 @@ description: | specs, error conventions, naming standards, pagination patterns, and versioning policies. Use when asked to "design the API", "create a design system for this service", "define the API standards", or "design this service interface". +allowed-tools: 'execute read search edit' argument-hint: '[API or service to design]' license: MIT diff --git a/src/vstack/_templates/skills/docs/config.yaml b/src/vstack/_templates/skills/docs/config.yaml index e0fbaf4..d3db53f 100644 --- a/src/vstack/_templates/skills/docs/config.yaml +++ b/src/vstack/_templates/skills/docs/config.yaml @@ -6,6 +6,7 @@ description: | generation or CHANGELOG updates. Use after release or deploy, or when asked to "update docs", "align documentation", or "refresh README/API docs". +allowed-tools: 'execute read search edit' argument-hint: '[release or change to document]' license: MIT diff --git a/src/vstack/_templates/skills/explore/config.yaml b/src/vstack/_templates/skills/explore/config.yaml index 9b5a79e..e2fb9c7 100644 --- a/src/vstack/_templates/skills/explore/config.yaml +++ b/src/vstack/_templates/skills/explore/config.yaml @@ -6,6 +6,7 @@ description: | summary. Use at the start of any engagement with an unfamiliar codebase, when asked to "understand this codebase", "map the architecture", "explore the repo", or "what does this service do?". +allowed-tools: 'execute read search' argument-hint: '[repository or system to explore]' license: MIT diff --git a/src/vstack/_templates/skills/gdpr/config.yaml b/src/vstack/_templates/skills/gdpr/config.yaml index b667cc1..b2c965b 100644 --- a/src/vstack/_templates/skills/gdpr/config.yaml +++ b/src/vstack/_templates/skills/gdpr/config.yaml @@ -8,6 +8,7 @@ description: | checklists. Use when asked to "GDPR review", "is this GDPR-compliant?", "privacy by design", "data retention policy", "right to erasure", or "DPIA". Proactively suggest before any feature that handles personal data. +allowed-tools: 'execute read search edit' argument-hint: '[component or feature: data model | API | logging | retention | erasure | infra | PR review]' license: MIT diff --git a/src/vstack/_templates/skills/gh-issues/config.yaml b/src/vstack/_templates/skills/gh-issues/config.yaml index 84a82d5..60c6457 100644 --- a/src/vstack/_templates/skills/gh-issues/config.yaml +++ b/src/vstack/_templates/skills/gh-issues/config.yaml @@ -5,6 +5,7 @@ description: | feature requests, tasks, labels, assignees, milestones, sub-issues, and issue workflows. Use when asked to "create an issue", "file a bug", "create a feature request", "update issue #N", "add a label", or "close an issue". +allowed-tools: 'execute read search edit web' argument-hint: '[what to create or which issue number to update]' license: MIT diff --git a/src/vstack/_templates/skills/gh-release/config.yaml b/src/vstack/_templates/skills/gh-release/config.yaml index 282ecce..ca53566 100644 --- a/src/vstack/_templates/skills/gh-release/config.yaml +++ b/src/vstack/_templates/skills/gh-release/config.yaml @@ -6,6 +6,7 @@ description: | selection, optional asset upload, and release metadata verification before publication. Use when asked to "create a GitHub release", "publish a release", or "draft release with gh". +allowed-tools: 'execute read search edit web' argument-hint: '[version/tag and release notes source]' license: MIT diff --git a/src/vstack/_templates/skills/guardrails/config.yaml b/src/vstack/_templates/skills/guardrails/config.yaml index b8213d3..ea66391 100644 --- a/src/vstack/_templates/skills/guardrails/config.yaml +++ b/src/vstack/_templates/skills/guardrails/config.yaml @@ -12,5 +12,6 @@ compatibility: Requires a skills-compatible agent with repository file access an metadata: owner: vstack maturity: stable +allowed-tools: 'execute read search edit' argument-hint: '[task]' user-invocable: true diff --git a/src/vstack/_templates/skills/helm/config.yaml b/src/vstack/_templates/skills/helm/config.yaml index 81eb68d..e31b569 100644 --- a/src/vstack/_templates/skills/helm/config.yaml +++ b/src/vstack/_templates/skills/helm/config.yaml @@ -5,6 +5,7 @@ description: | structure, values layering, lint/template validation, install/upgrade/rollback, dependency handling, and release troubleshooting. Use when asked to "create a Helm chart", "review Helm values", "upgrade Helm release", or "debug Helm deployment". +allowed-tools: 'execute read search edit' argument-hint: '[chart path, release name, namespace, and scope: chart review | install | upgrade | rollback]' license: MIT diff --git a/src/vstack/_templates/skills/incident/config.yaml b/src/vstack/_templates/skills/incident/config.yaml index bc3af18..62ce328 100644 --- a/src/vstack/_templates/skills/incident/config.yaml +++ b/src/vstack/_templates/skills/incident/config.yaml @@ -6,6 +6,7 @@ description: | 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. +allowed-tools: 'execute read search edit' argument-hint: '[incident or outage to analyse]' license: MIT diff --git a/src/vstack/_templates/skills/inspect/config.yaml b/src/vstack/_templates/skills/inspect/config.yaml index c2a38a2..997f75f 100644 --- a/src/vstack/_templates/skills/inspect/config.yaml +++ b/src/vstack/_templates/skills/inspect/config.yaml @@ -5,6 +5,7 @@ description: | produces severity-ranked findings, and makes no code or commit changes. Use when asked to "inspect", "assess", "check without fixing", or "what's wrong with this" before deciding whether to run verify fix loops. +allowed-tools: 'execute read search' argument-hint: '[component or service to inspect]' license: MIT diff --git a/src/vstack/_templates/skills/k8s/config.yaml b/src/vstack/_templates/skills/k8s/config.yaml index 328f662..0edb9f1 100644 --- a/src/vstack/_templates/skills/k8s/config.yaml +++ b/src/vstack/_templates/skills/k8s/config.yaml @@ -6,6 +6,7 @@ description: | RBAC, namespace isolation, and kubectl-based diagnostics. Use when asked to "deploy to Kubernetes", "review Kubernetes manifests", "debug Kubernetes rollout", "harden Kubernetes config", or "operate a workload on a cluster". +allowed-tools: 'execute read search edit' argument-hint: '[cluster/context, namespace, and scope: manifest review | deploy | rollout debug | hardening]' license: MIT diff --git a/src/vstack/_templates/skills/migrate/config.yaml b/src/vstack/_templates/skills/migrate/config.yaml index a821389..faa6b75 100644 --- a/src/vstack/_templates/skills/migrate/config.yaml +++ b/src/vstack/_templates/skills/migrate/config.yaml @@ -6,6 +6,7 @@ description: | Use when asked to "write a migration", "review this migration", "is this migration safe?", or "zero-downtime schema change". Proactively suggest before any DDL change ships to production. +allowed-tools: 'execute read search edit' argument-hint: '[migration file or schema change to review]' license: MIT diff --git a/src/vstack/_templates/skills/onboard/config.yaml b/src/vstack/_templates/skills/onboard/config.yaml index c8ed6b4..4afedd3 100644 --- a/src/vstack/_templates/skills/onboard/config.yaml +++ b/src/vstack/_templates/skills/onboard/config.yaml @@ -7,6 +7,7 @@ description: | guide", "create a contributor guide", "help new devs get started", or "document how to contribute". Produces or updates CONTRIBUTING.md and supplements README with a dev setup section. +allowed-tools: 'execute read search edit' argument-hint: '[repository or service to document]' license: MIT diff --git a/src/vstack/_templates/skills/openapi/config.yaml b/src/vstack/_templates/skills/openapi/config.yaml index 5e612d8..aeb0efb 100644 --- a/src/vstack/_templates/skills/openapi/config.yaml +++ b/src/vstack/_templates/skills/openapi/config.yaml @@ -6,6 +6,7 @@ description: | security schemes, and schema validation. Use when asked to "write an OpenAPI spec", "review this API spec", "add an endpoint to the spec", or "validate this OpenAPI file". +allowed-tools: 'execute read search edit' argument-hint: '[API or spec file to write or review]' license: MIT diff --git a/src/vstack/_templates/skills/performance/config.yaml b/src/vstack/_templates/skills/performance/config.yaml index 7aa35d0..de8e98c 100644 --- a/src/vstack/_templates/skills/performance/config.yaml +++ b/src/vstack/_templates/skills/performance/config.yaml @@ -4,6 +4,7 @@ description: | Performance profiling and regression detection. Establishes baselines, detects regressions, profiles bottlenecks, and recommends optimizations. Use when asked to "profile", "benchmark", "performance test", or "is this faster?". +allowed-tools: 'execute read search edit' argument-hint: '[endpoint or function to profile]' license: MIT diff --git a/src/vstack/_templates/skills/postmortem/config.yaml b/src/vstack/_templates/skills/postmortem/config.yaml index 5d89989..4f20b3d 100644 --- a/src/vstack/_templates/skills/postmortem/config.yaml +++ b/src/vstack/_templates/skills/postmortem/config.yaml @@ -5,6 +5,7 @@ description: | 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. +allowed-tools: 'execute read search edit' argument-hint: '[incident to write a post-mortem for]' license: MIT diff --git a/src/vstack/_templates/skills/pr/config.yaml b/src/vstack/_templates/skills/pr/config.yaml index e21790c..6726ab8 100644 --- a/src/vstack/_templates/skills/pr/config.yaml +++ b/src/vstack/_templates/skills/pr/config.yaml @@ -10,6 +10,7 @@ compatibility: Requires a skills-compatible agent with repository file access an metadata: owner: vstack maturity: stable +allowed-tools: 'execute read search edit web' argument-hint: '[task]' user-invocable: true disable-model-invocation: false diff --git a/src/vstack/_templates/skills/rancher/config.yaml b/src/vstack/_templates/skills/rancher/config.yaml index a4aa2a1..31f2318 100644 --- a/src/vstack/_templates/skills/rancher/config.yaml +++ b/src/vstack/_templates/skills/rancher/config.yaml @@ -5,6 +5,7 @@ description: | project context, role-based access, app deployment workflows, Fleet/GitOps basics, and multi-cluster operational checks. Use when asked to "deploy through Rancher", "review Rancher setup", "manage Rancher projects", or "troubleshoot Rancher-managed clusters". +allowed-tools: 'execute read search edit' argument-hint: '[rancher server/context, cluster/project, and scope: deploy | governance | fleet | troubleshooting]' license: MIT diff --git a/src/vstack/_templates/skills/rca/config.yaml b/src/vstack/_templates/skills/rca/config.yaml index 0111cd4..f3c2781 100644 --- a/src/vstack/_templates/skills/rca/config.yaml +++ b/src/vstack/_templates/skills/rca/config.yaml @@ -6,6 +6,7 @@ description: | 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. +allowed-tools: 'execute read search edit' argument-hint: '[incident or issue to analyse]' license: MIT diff --git a/src/vstack/_templates/skills/refactor/config.yaml b/src/vstack/_templates/skills/refactor/config.yaml index 04f438a..dcd2a3a 100644 --- a/src/vstack/_templates/skills/refactor/config.yaml +++ b/src/vstack/_templates/skills/refactor/config.yaml @@ -7,6 +7,7 @@ description: | "clean up this module", "reduce duplication", or "improve structure without changing behavior". Never changes behavior — if behavior must change, stop and use the engineering role. +allowed-tools: 'execute read search edit' argument-hint: '[module, file, or area to refactor]' license: MIT diff --git a/src/vstack/_templates/skills/release-notes/config.yaml b/src/vstack/_templates/skills/release-notes/config.yaml index 8244405..cc7cdb4 100644 --- a/src/vstack/_templates/skills/release-notes/config.yaml +++ b/src/vstack/_templates/skills/release-notes/config.yaml @@ -5,6 +5,7 @@ description: | own CHANGELOG.md updates, and produce docs/releases/{date}.md. Use when asked to "write release notes", "update the changelog", or "prepare release artifacts". +allowed-tools: 'execute read search edit' argument-hint: '[version or changes to release]' license: MIT diff --git a/src/vstack/_templates/skills/requirements/config.yaml b/src/vstack/_templates/skills/requirements/config.yaml index 591a0cc..b7f81f5 100644 --- a/src/vstack/_templates/skills/requirements/config.yaml +++ b/src/vstack/_templates/skills/requirements/config.yaml @@ -6,6 +6,7 @@ description: | Produces a requirements.md document. Use when asked to "gather requirements", "write the requirements", "define the spec", or "what are we building?". Runs before architecture and design work begins. +allowed-tools: 'execute read search edit' argument-hint: '[feature or system to document]' license: MIT diff --git a/src/vstack/_templates/skills/secret-scan/config.yaml b/src/vstack/_templates/skills/secret-scan/config.yaml index 6c70031..cffdb5a 100644 --- a/src/vstack/_templates/skills/secret-scan/config.yaml +++ b/src/vstack/_templates/skills/secret-scan/config.yaml @@ -6,6 +6,7 @@ description: | of exposed credentials. Use when asked to "set up secret scanning", "configure push protection", "define custom secret patterns", "triage a secret alert", or "fix a leaked credential". +allowed-tools: 'execute read search edit web' argument-hint: '[scope: enable | configure push-protection | custom-pattern | triage alerts | remediate]' license: MIT diff --git a/src/vstack/_templates/skills/security/config.yaml b/src/vstack/_templates/skills/security/config.yaml index 0e141b6..476cb87 100644 --- a/src/vstack/_templates/skills/security/config.yaml +++ b/src/vstack/_templates/skills/security/config.yaml @@ -6,6 +6,7 @@ description: | exposed secrets, dependency vulnerabilities, and broken access control. Use when asked to "security audit", "security review", or "check for vulnerabilities". Proactively suggest before any code ships to production. +allowed-tools: 'execute read search edit' argument-hint: '[component or service to audit]' license: MIT diff --git a/src/vstack/_templates/skills/terraform/config.yaml b/src/vstack/_templates/skills/terraform/config.yaml index 61aa9d9..00f013c 100644 --- a/src/vstack/_templates/skills/terraform/config.yaml +++ b/src/vstack/_templates/skills/terraform/config.yaml @@ -7,6 +7,7 @@ description: | security hardening. Use when asked to "write Terraform", "review this Terraform", "refactor IaC", "add a Terraform module", "plan state migration", or "harden Terraform configuration". +allowed-tools: 'execute read search edit' argument-hint: '[provider: aws | azure | gcp | generic, and scope: new resource | module | state migration | security review]' license: MIT diff --git a/src/vstack/_templates/skills/terragrunt/config.yaml b/src/vstack/_templates/skills/terragrunt/config.yaml index f0f0247..f1ed5b0 100644 --- a/src/vstack/_templates/skills/terragrunt/config.yaml +++ b/src/vstack/_templates/skills/terragrunt/config.yaml @@ -7,6 +7,7 @@ description: | run-all workflows. Use when asked to "write Terragrunt", "set up Terragrunt", "DRY Terraform across environments", "configure Terragrunt dependencies", or "migrate from plain Terraform to Terragrunt". +allowed-tools: 'execute read search edit' argument-hint: '[scope: new layout | dependency graph | state migration | run-all workflow | security review]' license: MIT diff --git a/src/vstack/_templates/skills/threat-model/config.yaml b/src/vstack/_templates/skills/threat-model/config.yaml index 6d96e7d..7d1709a 100644 --- a/src/vstack/_templates/skills/threat-model/config.yaml +++ b/src/vstack/_templates/skills/threat-model/config.yaml @@ -6,6 +6,7 @@ description: | contexts. Produces actionable threat scenarios, mitigations, and risk priorities. Use when asked to "threat model", "analyze attack paths", "STRIDE review", or "prioritize security design risks". +allowed-tools: 'execute read search edit' argument-hint: '[system, component, or architecture to threat model]' license: MIT diff --git a/src/vstack/_templates/skills/verify/config.yaml b/src/vstack/_templates/skills/verify/config.yaml index 523c4ce..685cebf 100644 --- a/src/vstack/_templates/skills/verify/config.yaml +++ b/src/vstack/_templates/skills/verify/config.yaml @@ -6,6 +6,7 @@ description: | Routes report-only requests to inspect and escalates deep security/performance concerns to specialized skills. Use when asked to "verify", "fix failing checks", "run QA with fixes", or "re-verify before shipping". +allowed-tools: 'execute read search edit' argument-hint: '[component or feature to verify]' license: MIT diff --git a/src/vstack/_templates/skills/vision/config.yaml b/src/vstack/_templates/skills/vision/config.yaml index 68fe9d8..be3646e 100644 --- a/src/vstack/_templates/skills/vision/config.yaml +++ b/src/vstack/_templates/skills/vision/config.yaml @@ -8,6 +8,7 @@ description: | Use when asked to "think bigger", "strategy review", "rethink this", or "is this ambitious enough". Proactively suggest when a plan feels under-scoped or when the user is questioning ambition. +allowed-tools: 'execute read search edit' argument-hint: '[plan or idea to review]' license: MIT diff --git a/src/vstack/skills/config.py b/src/vstack/skills/config.py index 10e1f8a..66e0d74 100644 --- a/src/vstack/skills/config.py +++ b/src/vstack/skills/config.py @@ -30,6 +30,8 @@ FieldSpec("compatibility", max_length=500, normalize_whitespace=True), # Agent Skills spec: arbitrary key/value mapping. We preserve this as raw YAML. FieldSpec("metadata", type="raw"), + # Agent Skills spec (experimental): space-separated pre-approved tool names. + FieldSpec("allowed-tools"), FieldSpec("argument-hint"), FieldSpec("user-invocable", type="bool"), FieldSpec("disable-model-invocation", type="bool"), From 3bbe9cfd5a5f2fcf63f0e992d57c8ec8fad19d77 Mon Sep 17 00:00:00 2001 From: Erik Schaareman Date: Sun, 10 May 2026 00:50:48 +0200 Subject: [PATCH 23/25] chore(install): regenerate artifacts after agent and skill tool config changes Artifact changes driven by: - execute added to product, architect, designer agents - web added to release agent - allowed-tools field added to all 44 skill configs Generated via: python3 -m vstack install --- .github/agents/architect.agent.md | 1 + .github/agents/designer.agent.md | 1 + .github/agents/product.agent.md | 1 + .github/agents/release.agent.md | 1 + .github/skills/adr/SKILL.md | 1 + .github/skills/analyse/SKILL.md | 1 + .github/skills/architecture/SKILL.md | 1 + .github/skills/aws-cli/SKILL.md | 1 + .github/skills/cicd/SKILL.md | 1 + .github/skills/cloudformation/SKILL.md | 1 + .github/skills/code-review/SKILL.md | 1 + .github/skills/codeql/SKILL.md | 1 + .github/skills/concise/SKILL.md | 1 + .github/skills/consult/SKILL.md | 1 + .github/skills/container/SKILL.md | 1 + .github/skills/conventional-commit/SKILL.md | 1 + .github/skills/debug/SKILL.md | 1 + .github/skills/dependabot/SKILL.md | 1 + .github/skills/dependency/SKILL.md | 1 + .github/skills/design/SKILL.md | 1 + .github/skills/docs/SKILL.md | 1 + .github/skills/explore/SKILL.md | 1 + .github/skills/gdpr/SKILL.md | 1 + .github/skills/gh-issues/SKILL.md | 1 + .github/skills/gh-release/SKILL.md | 1 + .github/skills/guardrails/SKILL.md | 1 + .github/skills/helm/SKILL.md | 1 + .github/skills/incident/SKILL.md | 1 + .github/skills/inspect/SKILL.md | 1 + .github/skills/k8s/SKILL.md | 1 + .github/skills/migrate/SKILL.md | 1 + .github/skills/onboard/SKILL.md | 1 + .github/skills/openapi/SKILL.md | 1 + .github/skills/performance/SKILL.md | 1 + .github/skills/postmortem/SKILL.md | 1 + .github/skills/pr/SKILL.md | 1 + .github/skills/rancher/SKILL.md | 1 + .github/skills/rca/SKILL.md | 1 + .github/skills/refactor/SKILL.md | 1 + .github/skills/release-notes/SKILL.md | 1 + .github/skills/requirements/SKILL.md | 1 + .github/skills/secret-scan/SKILL.md | 1 + .github/skills/security/SKILL.md | 1 + .github/skills/terraform/SKILL.md | 1 + .github/skills/terragrunt/SKILL.md | 1 + .github/skills/threat-model/SKILL.md | 1 + .github/skills/verify/SKILL.md | 1 + .github/skills/vision/SKILL.md | 1 + .vstack/vstack.json | 98 ++++++++++----------- 49 files changed, 97 insertions(+), 49 deletions(-) diff --git a/.github/agents/architect.agent.md b/.github/agents/architect.agent.md index e705c96..d16b7ac 100644 --- a/.github/agents/architect.agent.md +++ b/.github/agents/architect.agent.md @@ -10,6 +10,7 @@ tools: - read - search - edit + - execute - web - vscode - todo diff --git a/.github/agents/designer.agent.md b/.github/agents/designer.agent.md index adec29b..a5a1949 100644 --- a/.github/agents/designer.agent.md +++ b/.github/agents/designer.agent.md @@ -9,6 +9,7 @@ tools: - read - search - edit + - execute - web - vscode - todo diff --git a/.github/agents/product.agent.md b/.github/agents/product.agent.md index 9587f6c..17f92e1 100644 --- a/.github/agents/product.agent.md +++ b/.github/agents/product.agent.md @@ -9,6 +9,7 @@ tools: - read - search - edit + - execute - web - vscode - todo diff --git a/.github/agents/release.agent.md b/.github/agents/release.agent.md index 8911309..add10cb 100644 --- a/.github/agents/release.agent.md +++ b/.github/agents/release.agent.md @@ -11,6 +11,7 @@ tools: - search - edit - execute + - web - vscode - todo - agent diff --git a/.github/skills/adr/SKILL.md b/.github/skills/adr/SKILL.md index e0e9a3a..7bd56f5 100644 --- a/.github/skills/adr/SKILL.md +++ b/.github/skills/adr/SKILL.md @@ -6,6 +6,7 @@ compatibility: 'Requires a skills-compatible agent with repository file access a metadata: owner: vstack maturity: stable +allowed-tools: 'execute read search edit' argument-hint: '[decision to record]' user-invocable: true disable-model-invocation: false diff --git a/.github/skills/analyse/SKILL.md b/.github/skills/analyse/SKILL.md index aca2109..3856249 100644 --- a/.github/skills/analyse/SKILL.md +++ b/.github/skills/analyse/SKILL.md @@ -6,6 +6,7 @@ compatibility: 'Requires a skills-compatible agent with repository file access a metadata: owner: vstack maturity: stable +allowed-tools: 'execute read search' argument-hint: '[topic, change, or question to analyse]' user-invocable: true disable-model-invocation: false diff --git a/.github/skills/architecture/SKILL.md b/.github/skills/architecture/SKILL.md index 2e4243e..e55a8d3 100644 --- a/.github/skills/architecture/SKILL.md +++ b/.github/skills/architecture/SKILL.md @@ -6,6 +6,7 @@ compatibility: 'Requires a skills-compatible agent with repository file access a metadata: owner: vstack maturity: stable +allowed-tools: 'execute read search edit' argument-hint: '[plan or system to review]' user-invocable: true disable-model-invocation: false diff --git a/.github/skills/aws-cli/SKILL.md b/.github/skills/aws-cli/SKILL.md index 25586cb..c28e4af 100644 --- a/.github/skills/aws-cli/SKILL.md +++ b/.github/skills/aws-cli/SKILL.md @@ -6,6 +6,7 @@ compatibility: 'Requires AWS CLI v2 installed and configured (aws configure or e metadata: owner: vstack maturity: stable +allowed-tools: 'execute read search edit web' argument-hint: '[service: iam | ec2 | s3 | rds | ecs | lambda | cloudwatch | ssm | secrets]' user-invocable: true disable-model-invocation: false diff --git a/.github/skills/cicd/SKILL.md b/.github/skills/cicd/SKILL.md index 70be947..421c19a 100644 --- a/.github/skills/cicd/SKILL.md +++ b/.github/skills/cicd/SKILL.md @@ -6,6 +6,7 @@ compatibility: 'Requires a skills-compatible agent with repository file access a metadata: owner: vstack maturity: stable +allowed-tools: 'execute read search edit' argument-hint: '[service or workflow to configure]' user-invocable: true disable-model-invocation: false diff --git a/.github/skills/cloudformation/SKILL.md b/.github/skills/cloudformation/SKILL.md index a015aa9..2ad5f01 100644 --- a/.github/skills/cloudformation/SKILL.md +++ b/.github/skills/cloudformation/SKILL.md @@ -6,6 +6,7 @@ compatibility: 'Requires a skills-compatible agent with repository file access. metadata: owner: vstack maturity: stable +allowed-tools: 'execute read search edit' argument-hint: '[resource type or stack name, e.g. VPC | RDS | ECS service | Lambda function]' user-invocable: true disable-model-invocation: false diff --git a/.github/skills/code-review/SKILL.md b/.github/skills/code-review/SKILL.md index 979c681..4d3a47b 100644 --- a/.github/skills/code-review/SKILL.md +++ b/.github/skills/code-review/SKILL.md @@ -6,6 +6,7 @@ compatibility: 'Requires a skills-compatible agent with repository file access a metadata: owner: vstack maturity: stable +allowed-tools: 'execute read search' argument-hint: '[files, PR, or change to review]' user-invocable: true disable-model-invocation: false diff --git a/.github/skills/codeql/SKILL.md b/.github/skills/codeql/SKILL.md index ce8f67a..855da54 100644 --- a/.github/skills/codeql/SKILL.md +++ b/.github/skills/codeql/SKILL.md @@ -6,6 +6,7 @@ compatibility: 'Requires a skills-compatible agent with repository file access a metadata: owner: vstack maturity: stable +allowed-tools: 'execute read search edit web' argument-hint: '[languages and setup type: default or advanced]' user-invocable: true disable-model-invocation: false diff --git a/.github/skills/concise/SKILL.md b/.github/skills/concise/SKILL.md index 8ff9138..dac193d 100644 --- a/.github/skills/concise/SKILL.md +++ b/.github/skills/concise/SKILL.md @@ -6,6 +6,7 @@ compatibility: 'Requires a skills-compatible agent with session memory and repos metadata: owner: vstack maturity: stable +allowed-tools: 'execute read search' argument-hint: '[normal|compact|ultra|status|on|off]' user-invocable: true disable-model-invocation: false diff --git a/.github/skills/consult/SKILL.md b/.github/skills/consult/SKILL.md index 6532b9f..2571f4e 100644 --- a/.github/skills/consult/SKILL.md +++ b/.github/skills/consult/SKILL.md @@ -6,6 +6,7 @@ compatibility: 'Requires a skills-compatible agent with repository file access a metadata: owner: vstack maturity: stable +allowed-tools: 'execute read search' argument-hint: '[API, tool, or workflow to consult]' user-invocable: true disable-model-invocation: false diff --git a/.github/skills/container/SKILL.md b/.github/skills/container/SKILL.md index 8f0a472..2d9c924 100644 --- a/.github/skills/container/SKILL.md +++ b/.github/skills/container/SKILL.md @@ -6,6 +6,7 @@ compatibility: 'Requires a skills-compatible agent with repository file access a metadata: owner: vstack maturity: stable +allowed-tools: 'execute read search edit' argument-hint: '[service to containerise]' user-invocable: true disable-model-invocation: false diff --git a/.github/skills/conventional-commit/SKILL.md b/.github/skills/conventional-commit/SKILL.md index 5f66f2d..77a4b00 100644 --- a/.github/skills/conventional-commit/SKILL.md +++ b/.github/skills/conventional-commit/SKILL.md @@ -6,6 +6,7 @@ compatibility: 'Requires a skills-compatible agent with repository file access a metadata: owner: vstack maturity: stable +allowed-tools: 'execute read search edit' argument-hint: '[changes to commit and desired release intent]' user-invocable: true disable-model-invocation: false diff --git a/.github/skills/debug/SKILL.md b/.github/skills/debug/SKILL.md index d808802..76ec3b7 100644 --- a/.github/skills/debug/SKILL.md +++ b/.github/skills/debug/SKILL.md @@ -6,6 +6,7 @@ compatibility: 'Requires a skills-compatible agent with repository file access a metadata: owner: vstack maturity: stable +allowed-tools: 'execute read search edit' argument-hint: '[issue or error to debug]' user-invocable: true disable-model-invocation: false diff --git a/.github/skills/dependabot/SKILL.md b/.github/skills/dependabot/SKILL.md index 5fd9934..ad9aeff 100644 --- a/.github/skills/dependabot/SKILL.md +++ b/.github/skills/dependabot/SKILL.md @@ -6,6 +6,7 @@ compatibility: 'Requires a skills-compatible agent with repository file access. metadata: owner: vstack maturity: stable +allowed-tools: 'execute read search edit' argument-hint: '[repository type: library | service | monorepo, and ecosystems to cover]' user-invocable: true disable-model-invocation: false diff --git a/.github/skills/dependency/SKILL.md b/.github/skills/dependency/SKILL.md index 1b5db8d..3313f88 100644 --- a/.github/skills/dependency/SKILL.md +++ b/.github/skills/dependency/SKILL.md @@ -6,6 +6,7 @@ compatibility: 'Requires a skills-compatible agent with repository file access a metadata: owner: vstack maturity: stable +allowed-tools: 'execute read search edit web' argument-hint: '[project or package manifest to audit]' user-invocable: true disable-model-invocation: false diff --git a/.github/skills/design/SKILL.md b/.github/skills/design/SKILL.md index 3d8428c..e732ac8 100644 --- a/.github/skills/design/SKILL.md +++ b/.github/skills/design/SKILL.md @@ -6,6 +6,7 @@ compatibility: 'Requires a skills-compatible agent with repository file access a metadata: owner: vstack maturity: stable +allowed-tools: 'execute read search edit' argument-hint: '[API or service to design]' user-invocable: true disable-model-invocation: false diff --git a/.github/skills/docs/SKILL.md b/.github/skills/docs/SKILL.md index 98ef5c2..be97300 100644 --- a/.github/skills/docs/SKILL.md +++ b/.github/skills/docs/SKILL.md @@ -6,6 +6,7 @@ compatibility: 'Requires a skills-compatible agent with repository file access a metadata: owner: vstack maturity: stable +allowed-tools: 'execute read search edit' argument-hint: '[release or change to document]' user-invocable: true disable-model-invocation: false diff --git a/.github/skills/explore/SKILL.md b/.github/skills/explore/SKILL.md index 5ef71cf..aa6af6a 100644 --- a/.github/skills/explore/SKILL.md +++ b/.github/skills/explore/SKILL.md @@ -6,6 +6,7 @@ compatibility: 'Requires a skills-compatible agent with repository file access a metadata: owner: vstack maturity: stable +allowed-tools: 'execute read search' argument-hint: '[repository or system to explore]' user-invocable: true disable-model-invocation: false diff --git a/.github/skills/gdpr/SKILL.md b/.github/skills/gdpr/SKILL.md index 387f14a..b6ff4fd 100644 --- a/.github/skills/gdpr/SKILL.md +++ b/.github/skills/gdpr/SKILL.md @@ -6,6 +6,7 @@ compatibility: 'Requires a skills-compatible agent with repository file access.' metadata: owner: vstack maturity: stable +allowed-tools: 'execute read search edit' argument-hint: '[component or feature: data model | API | logging | retention | erasure | infra | PR review]' user-invocable: true disable-model-invocation: false diff --git a/.github/skills/gh-issues/SKILL.md b/.github/skills/gh-issues/SKILL.md index 9a6bede..3b5a19a 100644 --- a/.github/skills/gh-issues/SKILL.md +++ b/.github/skills/gh-issues/SKILL.md @@ -6,6 +6,7 @@ compatibility: 'Requires a skills-compatible agent with terminal command executi metadata: owner: vstack maturity: stable +allowed-tools: 'execute read search edit web' argument-hint: '[what to create or which issue number to update]' user-invocable: true disable-model-invocation: false diff --git a/.github/skills/gh-release/SKILL.md b/.github/skills/gh-release/SKILL.md index 11960e8..e3b1c68 100644 --- a/.github/skills/gh-release/SKILL.md +++ b/.github/skills/gh-release/SKILL.md @@ -6,6 +6,7 @@ compatibility: 'Requires a skills-compatible agent with repository file access, metadata: owner: vstack maturity: stable +allowed-tools: 'execute read search edit web' argument-hint: '[version/tag and release notes source]' user-invocable: true disable-model-invocation: false diff --git a/.github/skills/guardrails/SKILL.md b/.github/skills/guardrails/SKILL.md index 3a85808..820fe2e 100644 --- a/.github/skills/guardrails/SKILL.md +++ b/.github/skills/guardrails/SKILL.md @@ -6,6 +6,7 @@ compatibility: 'Requires a skills-compatible agent with repository file access a metadata: owner: vstack maturity: stable +allowed-tools: 'execute read search edit' argument-hint: '[task]' user-invocable: true disable-model-invocation: true diff --git a/.github/skills/helm/SKILL.md b/.github/skills/helm/SKILL.md index acea595..1746edd 100644 --- a/.github/skills/helm/SKILL.md +++ b/.github/skills/helm/SKILL.md @@ -6,6 +6,7 @@ compatibility: 'Requires a skills-compatible agent with repository file access. metadata: owner: vstack maturity: stable +allowed-tools: 'execute read search edit' argument-hint: '[chart path, release name, namespace, and scope: chart review | install | upgrade | rollback]' user-invocable: true disable-model-invocation: false diff --git a/.github/skills/incident/SKILL.md b/.github/skills/incident/SKILL.md index 7b17180..784e4ac 100644 --- a/.github/skills/incident/SKILL.md +++ b/.github/skills/incident/SKILL.md @@ -6,6 +6,7 @@ compatibility: 'Requires a skills-compatible agent with repository file access a metadata: owner: vstack maturity: stable +allowed-tools: 'execute read search edit' argument-hint: '[incident or outage to analyse]' user-invocable: true disable-model-invocation: false diff --git a/.github/skills/inspect/SKILL.md b/.github/skills/inspect/SKILL.md index c1dba2d..9a2346a 100644 --- a/.github/skills/inspect/SKILL.md +++ b/.github/skills/inspect/SKILL.md @@ -6,6 +6,7 @@ compatibility: 'Requires a skills-compatible agent with repository file access a metadata: owner: vstack maturity: stable +allowed-tools: 'execute read search' argument-hint: '[component or service to inspect]' user-invocable: true disable-model-invocation: false diff --git a/.github/skills/k8s/SKILL.md b/.github/skills/k8s/SKILL.md index 7375f27..42ca2a6 100644 --- a/.github/skills/k8s/SKILL.md +++ b/.github/skills/k8s/SKILL.md @@ -6,6 +6,7 @@ compatibility: 'Requires a skills-compatible agent with repository file access. metadata: owner: vstack maturity: stable +allowed-tools: 'execute read search edit' argument-hint: '[cluster/context, namespace, and scope: manifest review | deploy | rollout debug | hardening]' user-invocable: true disable-model-invocation: false diff --git a/.github/skills/migrate/SKILL.md b/.github/skills/migrate/SKILL.md index 21f26b6..07e1934 100644 --- a/.github/skills/migrate/SKILL.md +++ b/.github/skills/migrate/SKILL.md @@ -6,6 +6,7 @@ compatibility: 'Requires a skills-compatible agent with repository file access a metadata: owner: vstack maturity: stable +allowed-tools: 'execute read search edit' argument-hint: '[migration file or schema change to review]' user-invocable: true disable-model-invocation: false diff --git a/.github/skills/onboard/SKILL.md b/.github/skills/onboard/SKILL.md index 929214c..da290f0 100644 --- a/.github/skills/onboard/SKILL.md +++ b/.github/skills/onboard/SKILL.md @@ -6,6 +6,7 @@ compatibility: 'Requires a skills-compatible agent with repository file access a metadata: owner: vstack maturity: stable +allowed-tools: 'execute read search edit' argument-hint: '[repository or service to document]' user-invocable: true disable-model-invocation: false diff --git a/.github/skills/openapi/SKILL.md b/.github/skills/openapi/SKILL.md index 976d3ee..77a8b91 100644 --- a/.github/skills/openapi/SKILL.md +++ b/.github/skills/openapi/SKILL.md @@ -6,6 +6,7 @@ compatibility: 'Requires a skills-compatible agent with repository file access a metadata: owner: vstack maturity: stable +allowed-tools: 'execute read search edit' argument-hint: '[API or spec file to write or review]' user-invocable: true disable-model-invocation: false diff --git a/.github/skills/performance/SKILL.md b/.github/skills/performance/SKILL.md index 26c45d2..4bd99dc 100644 --- a/.github/skills/performance/SKILL.md +++ b/.github/skills/performance/SKILL.md @@ -6,6 +6,7 @@ compatibility: 'Requires a skills-compatible agent with repository file access a metadata: owner: vstack maturity: stable +allowed-tools: 'execute read search edit' argument-hint: '[endpoint or function to profile]' user-invocable: true disable-model-invocation: false diff --git a/.github/skills/postmortem/SKILL.md b/.github/skills/postmortem/SKILL.md index 07df9dc..12508ec 100644 --- a/.github/skills/postmortem/SKILL.md +++ b/.github/skills/postmortem/SKILL.md @@ -6,6 +6,7 @@ compatibility: 'Requires a skills-compatible agent with repository file access a metadata: owner: vstack maturity: stable +allowed-tools: 'execute read search edit' argument-hint: '[incident to write a post-mortem for]' user-invocable: true disable-model-invocation: false diff --git a/.github/skills/pr/SKILL.md b/.github/skills/pr/SKILL.md index 23c4910..bd8368a 100644 --- a/.github/skills/pr/SKILL.md +++ b/.github/skills/pr/SKILL.md @@ -6,6 +6,7 @@ compatibility: 'Requires a skills-compatible agent with repository file access a metadata: owner: vstack maturity: stable +allowed-tools: 'execute read search edit web' argument-hint: '[task]' user-invocable: true disable-model-invocation: false diff --git a/.github/skills/rancher/SKILL.md b/.github/skills/rancher/SKILL.md index 17f1727..3d58218 100644 --- a/.github/skills/rancher/SKILL.md +++ b/.github/skills/rancher/SKILL.md @@ -6,6 +6,7 @@ compatibility: 'Requires a skills-compatible agent with repository file access. metadata: owner: vstack maturity: stable +allowed-tools: 'execute read search edit' argument-hint: '[rancher server/context, cluster/project, and scope: deploy | governance | fleet | troubleshooting]' user-invocable: true disable-model-invocation: false diff --git a/.github/skills/rca/SKILL.md b/.github/skills/rca/SKILL.md index e5114e9..0a302a5 100644 --- a/.github/skills/rca/SKILL.md +++ b/.github/skills/rca/SKILL.md @@ -6,6 +6,7 @@ compatibility: 'Requires a skills-compatible agent with repository file access a metadata: owner: vstack maturity: stable +allowed-tools: 'execute read search edit' argument-hint: '[incident or issue to analyse]' user-invocable: true disable-model-invocation: false diff --git a/.github/skills/refactor/SKILL.md b/.github/skills/refactor/SKILL.md index efb2840..1110f6b 100644 --- a/.github/skills/refactor/SKILL.md +++ b/.github/skills/refactor/SKILL.md @@ -6,6 +6,7 @@ compatibility: 'Requires a skills-compatible agent with repository file access a metadata: owner: vstack maturity: stable +allowed-tools: 'execute read search edit' argument-hint: '[module, file, or area to refactor]' user-invocable: true disable-model-invocation: false diff --git a/.github/skills/release-notes/SKILL.md b/.github/skills/release-notes/SKILL.md index 19081b7..39d4c98 100644 --- a/.github/skills/release-notes/SKILL.md +++ b/.github/skills/release-notes/SKILL.md @@ -6,6 +6,7 @@ compatibility: 'Requires a skills-compatible agent with repository file access a metadata: owner: vstack maturity: stable +allowed-tools: 'execute read search edit' argument-hint: '[version or changes to release]' user-invocable: true disable-model-invocation: false diff --git a/.github/skills/requirements/SKILL.md b/.github/skills/requirements/SKILL.md index efe5ee4..5d1e7d7 100644 --- a/.github/skills/requirements/SKILL.md +++ b/.github/skills/requirements/SKILL.md @@ -6,6 +6,7 @@ compatibility: 'Requires a skills-compatible agent with repository file access a metadata: owner: vstack maturity: stable +allowed-tools: 'execute read search edit' argument-hint: '[feature or system to document]' user-invocable: true disable-model-invocation: false diff --git a/.github/skills/secret-scan/SKILL.md b/.github/skills/secret-scan/SKILL.md index a378d37..0c0674b 100644 --- a/.github/skills/secret-scan/SKILL.md +++ b/.github/skills/secret-scan/SKILL.md @@ -6,6 +6,7 @@ compatibility: 'Requires repository access and GitHub Advanced Security (private metadata: owner: vstack maturity: stable +allowed-tools: 'execute read search edit web' argument-hint: '[scope: enable | configure push-protection | custom-pattern | triage alerts | remediate]' user-invocable: true disable-model-invocation: false diff --git a/.github/skills/security/SKILL.md b/.github/skills/security/SKILL.md index a237075..eb35c48 100644 --- a/.github/skills/security/SKILL.md +++ b/.github/skills/security/SKILL.md @@ -6,6 +6,7 @@ compatibility: 'Requires a skills-compatible agent with repository file access a metadata: owner: vstack maturity: stable +allowed-tools: 'execute read search edit' argument-hint: '[component or service to audit]' user-invocable: true disable-model-invocation: false diff --git a/.github/skills/terraform/SKILL.md b/.github/skills/terraform/SKILL.md index 2554e0c..3ba5145 100644 --- a/.github/skills/terraform/SKILL.md +++ b/.github/skills/terraform/SKILL.md @@ -6,6 +6,7 @@ compatibility: 'Requires a skills-compatible agent with repository file access. metadata: owner: vstack maturity: stable +allowed-tools: 'execute read search edit' argument-hint: '[provider: aws | azure | gcp | generic, and scope: new resource | module | state migration | security review]' user-invocable: true disable-model-invocation: false diff --git a/.github/skills/terragrunt/SKILL.md b/.github/skills/terragrunt/SKILL.md index baccafa..77326de 100644 --- a/.github/skills/terragrunt/SKILL.md +++ b/.github/skills/terragrunt/SKILL.md @@ -6,6 +6,7 @@ compatibility: 'Requires a skills-compatible agent with repository file access. metadata: owner: vstack maturity: stable +allowed-tools: 'execute read search edit' argument-hint: '[scope: new layout | dependency graph | state migration | run-all workflow | security review]' user-invocable: true disable-model-invocation: false diff --git a/.github/skills/threat-model/SKILL.md b/.github/skills/threat-model/SKILL.md index fb8e7ce..d2a81e5 100644 --- a/.github/skills/threat-model/SKILL.md +++ b/.github/skills/threat-model/SKILL.md @@ -6,6 +6,7 @@ compatibility: 'Requires a skills-compatible agent with repository file access a metadata: owner: vstack maturity: stable +allowed-tools: 'execute read search edit' argument-hint: '[system, component, or architecture to threat model]' user-invocable: true disable-model-invocation: false diff --git a/.github/skills/verify/SKILL.md b/.github/skills/verify/SKILL.md index 55cd71c..585a7df 100644 --- a/.github/skills/verify/SKILL.md +++ b/.github/skills/verify/SKILL.md @@ -6,6 +6,7 @@ compatibility: 'Requires a skills-compatible agent with repository file access a metadata: owner: vstack maturity: stable +allowed-tools: 'execute read search edit' argument-hint: '[component or feature to verify]' user-invocable: true disable-model-invocation: false diff --git a/.github/skills/vision/SKILL.md b/.github/skills/vision/SKILL.md index 2e0d25f..16dbb1b 100644 --- a/.github/skills/vision/SKILL.md +++ b/.github/skills/vision/SKILL.md @@ -6,6 +6,7 @@ compatibility: 'Requires a skills-compatible agent with repository file access a metadata: owner: vstack maturity: stable +allowed-tools: 'execute read search edit' argument-hint: '[plan or idea to review]' user-invocable: true disable-model-invocation: false diff --git a/.vstack/vstack.json b/.vstack/vstack.json index b9c002e..85f21fa 100644 --- a/.vstack/vstack.json +++ b/.vstack/vstack.json @@ -2,315 +2,315 @@ "manifest_version": 2, "hash_algorithm": "sha256", "vstack_version": "0.0.0.post3.dev0+df3fe6e", - "installed_at": "2026-05-09T21:42:06.107627+00:00", + "installed_at": "2026-05-09T22:36:50.257153+00:00", "artifacts": { "skills": [ { "name": "adr", "file": "skills/adr/SKILL.md", "version": "20260421003", - "checksum": "0838ffc14c5b86ea8b3df93cf6ce76c9bcd27b6c55c4ba47a3d01cc000fd7ee0", + "checksum": "871263fddee0b3ae8e2c2e2243aad4f2b3aa9e0dea0d2519c7d21c3e98430a83", "checksum_algorithm": "sha256" }, { "name": "analyse", "file": "skills/analyse/SKILL.md", "version": "20260421004", - "checksum": "8ff12f1d1f12ac9c46a2cb36981b85aeea97a8bc0876bbef37300511ea0eb7b3", + "checksum": "79ced25692671a5286a26cbcd0e59ed4d641ac7493fd8e2b58b232f97e467224", "checksum_algorithm": "sha256" }, { "name": "architecture", "file": "skills/architecture/SKILL.md", "version": "20260421005", - "checksum": "4ed22957e392aa89cf4949d2a88e707bf913948c2805a257039f5376810af754", + "checksum": "e2d0342184829c888a70ece6f82ad6a7450ad5d8c958ff6e028d7f245f1e5960", "checksum_algorithm": "sha256" }, { "name": "aws-cli", "file": "skills/aws-cli/SKILL.md", "version": "20260502033", - "checksum": "e5b2688de029ab0cc6d3e3862237c5bdb7f3ad4aab9baa4e1bceac433d699b79", + "checksum": "4868a5adc17b6ac52839c85f3a09986991c2498501913ef48a8f85a6291e1b21", "checksum_algorithm": "sha256" }, { "name": "cicd", "file": "skills/cicd/SKILL.md", "version": "20260421006", - "checksum": "ffe0df7fe8c425844e9fc1a20976a9d6c116d828af96c1a568317728bfdefdcf", + "checksum": "ba6c0cf0e644e24b1290c1b78969bad86dd7d5b39030edc7bd430d4d1832cfd7", "checksum_algorithm": "sha256" }, { "name": "cloudformation", "file": "skills/cloudformation/SKILL.md", "version": "20260502032", - "checksum": "d172ffc2b30c75986446a72b625baecae123d1cc3882b20ca803c60b46dcd75d", + "checksum": "0e61163cfc356105a309bbecf89ee26a5b10b9b5db58c331d76efa4a508e760e", "checksum_algorithm": "sha256" }, { "name": "code-review", "file": "skills/code-review/SKILL.md", "version": "20260421007", - "checksum": "5bcdddc03ce0a54997037210b38e7b4ed22828cfba6a76153afaa94cf616527a", + "checksum": "58b7caa9dd3a5da6bef8d8d4d1f7b6d513740d33ad16731a06b1b2a907d93a13", "checksum_algorithm": "sha256" }, { "name": "codeql", "file": "skills/codeql/SKILL.md", "version": "20260502026", - "checksum": "1b1b5800be204cc0e5dc6b4a8fcb9a7b916cfba2f96d5f99dbb6d7e213b8e95c", + "checksum": "95f29c975e5850307620c21a68df174b90ecaafd5b1e36659bb3c34d0027b5df", "checksum_algorithm": "sha256" }, { "name": "concise", "file": "skills/concise/SKILL.md", "version": "20260421008", - "checksum": "3a07860ba6c83a97c9ad5496e124be4e5277fc0b811fe25dcd5bfdb7d8252b98", + "checksum": "9555de7da6a925e76992b6f5e51ddccd6b4de3887ea31ed2ceb9113c9fe9bc7e", "checksum_algorithm": "sha256" }, { "name": "consult", "file": "skills/consult/SKILL.md", "version": "20260421009", - "checksum": "90e1b0d0ff757c8e5879832c5ac6c41ec4054e9bff5b12a14531d427c09bcbad", + "checksum": "a340883900309261213ec0ea0d4e9e5b72ca83fb25c7cd6c3506e1f95f40cf76", "checksum_algorithm": "sha256" }, { "name": "container", "file": "skills/container/SKILL.md", "version": "20260421010", - "checksum": "e135b18cb972d30b77de4492db0437853ff1f49bad60a9113a034e39154d30e1", + "checksum": "f0a4408a756195faca9a58dca76e56f950df227415d77af293bf55280191abd4", "checksum_algorithm": "sha256" }, { "name": "conventional-commit", "file": "skills/conventional-commit/SKILL.md", "version": "20260502024", - "checksum": "76477b42b17c9f6eec92baa36dc172e7ee98b20a86399eabdd2b7d2a90923509", + "checksum": "8f1ce569106b5210a8d4ffe5bc9123eb0dec6b24ff56cf680922eccdd9891012", "checksum_algorithm": "sha256" }, { "name": "debug", "file": "skills/debug/SKILL.md", "version": "20260421011", - "checksum": "8e46a2723004bc86f6aee50f492b73045acdab66c964787c58a988c98684750b", + "checksum": "b4c1c6de7142b7b74326d1c19a5cc27f3957d1faa48aa48bd6b1fc45832edf3b", "checksum_algorithm": "sha256" }, { "name": "dependabot", "file": "skills/dependabot/SKILL.md", "version": "20260502027", - "checksum": "3ce5836bf870f73805800f672d379aa10f5aff5a522017a77875d9339bb99984", + "checksum": "fa3f595740faa0b8093ffe4f32fb8e4b85f5834ea10817752ee9c94be8f38c2d", "checksum_algorithm": "sha256" }, { "name": "dependency", "file": "skills/dependency/SKILL.md", "version": "20260421012", - "checksum": "46e75e8a28b1af5a60da7d9b3d1e46f0914b2f10d8733b37e6577e62b0074724", + "checksum": "2cbbecca8e6697b8523301325a889a5ca88c520f6d35a3b98807b6c3449239f5", "checksum_algorithm": "sha256" }, { "name": "design", "file": "skills/design/SKILL.md", "version": "20260421013", - "checksum": "e21a674324244412f7b4a5bc010da4a7b9dfcf3522d54281bc9cf69f0b1126bd", + "checksum": "2d56e34aca8d3cf5a3f327e3b69ab15707393ffc47607c91ec91dd5be86cd65b", "checksum_algorithm": "sha256" }, { "name": "docs", "file": "skills/docs/SKILL.md", "version": "20260421014", - "checksum": "7d34421800bf04fe5a0782abfc5702a421419d31a5c66fd495d7dc8a6e2c64e3", + "checksum": "d6ebf7d270eac7911aea09a34cb7ec4006c70064b7e6f58dfa84a405e6620d18", "checksum_algorithm": "sha256" }, { "name": "explore", "file": "skills/explore/SKILL.md", "version": "20260421015", - "checksum": "a257941d7b577f782b5d41703b1fbd25c5ab13f664e696d83cc3a9fd91a5565f", + "checksum": "914323f796ac95a0f73d404f1b7eb38d94ba8950ed1a4acc5d80600ba03fae8f", "checksum_algorithm": "sha256" }, { "name": "gdpr", "file": "skills/gdpr/SKILL.md", "version": "20260502029", - "checksum": "94207650498b56f246392449f57b4af4659bdfe511e7a6c2f23022ff2f68b08d", + "checksum": "dfe60c4c59b7ae4dc66e98a885ec42b0e60eccc61fa1cb6fd269cd7eb3e0a94d", "checksum_algorithm": "sha256" }, { "name": "gh-issues", "file": "skills/gh-issues/SKILL.md", "version": "20260502025", - "checksum": "7a2b9b7463a40436fe77c77d066dd455ddb4abef877726ba041b89cbaf030280", + "checksum": "bc1f595283e7643de63c7519e994500b9434a0343154bfc404b842ce8e7028a8", "checksum_algorithm": "sha256" }, { "name": "gh-release", "file": "skills/gh-release/SKILL.md", "version": "20260502023", - "checksum": "beed7f98cb52222f688fed5a83d376acea1215c6654c570487412792bb577462", + "checksum": "10abe52f681d261588608418a546580e7798e883179888061ed6d2285050d870", "checksum_algorithm": "sha256" }, { "name": "guardrails", "file": "skills/guardrails/SKILL.md", "version": "20260421016", - "checksum": "8ec7213e1f8c85b4975ebb032d897e84e372c0279e3265da1098fc86cf98695f", + "checksum": "1c1a44c95461eb4eeef7ad64b5846a1efbb202cf1b75186421c5d0b8f9fd03f1", "checksum_algorithm": "sha256" }, { "name": "helm", "file": "skills/helm/SKILL.md", "version": "20260502037", - "checksum": "13600308860723f683802431ac7a8c2c3834c40e521055854cdf3d3290570689", + "checksum": "6e6579ede2dadb6dbe895d07c362d208e62414338445bd035f47bd1404be501b", "checksum_algorithm": "sha256" }, { "name": "incident", "file": "skills/incident/SKILL.md", "version": "20260503002", - "checksum": "d337c4f145c09af856f4a05f41b11f2e46f3822f77e067e1a6e39de5f2af1fe8", + "checksum": "546229fe91121dae2192aceb0b215096cc8cf5ba0cfb3d1300e30c73ff3f6276", "checksum_algorithm": "sha256" }, { "name": "inspect", "file": "skills/inspect/SKILL.md", "version": "20260421018", - "checksum": "7997cb4477f9b1a8a6753082a4f3ea0c9b5a1a77520a3f20527f8c153c968a27", + "checksum": "3a4aee3620bbd669602001c041cf81788da5fac9dc055a9f1b0c155e50e88b46", "checksum_algorithm": "sha256" }, { "name": "k8s", "file": "skills/k8s/SKILL.md", "version": "20260502036", - "checksum": "d178a4d86f6ca5f4144cd8212eaf09979bb48afa55cb482409609aa50c5f0f88", + "checksum": "2bfbe14c187809211a7296578644262e2e47c89b4a2fc28fc3a30a79fdaeb580", "checksum_algorithm": "sha256" }, { "name": "migrate", "file": "skills/migrate/SKILL.md", "version": "20260421019", - "checksum": "c6c18c750319c0feb14526d637cb591f51ffde341b1385b4063aeb1edf12fd8b", + "checksum": "eecc00aae2c58c573000a434fbf7ae891a3e85dcc99a42fec0e8bcadc3047b7d", "checksum_algorithm": "sha256" }, { "name": "onboard", "file": "skills/onboard/SKILL.md", "version": "20260421020", - "checksum": "b96fb4067121eabbdf686641b55d32f253282e0b734fa4b2df6e4a9e85938cde", + "checksum": "f16f2d78002e3be8e7d1d9e2bf376c1b37eb181d821f3cd113e0d5aecf5236ae", "checksum_algorithm": "sha256" }, { "name": "openapi", "file": "skills/openapi/SKILL.md", "version": "20260421021", - "checksum": "75d7cab59b1cde6dc91490867eb8537821059960957ba21ebd24d922e9fe7a91", + "checksum": "a3dbcded8e1a8b268a1976b9185f41de74ad0ad6e7aae98c399d6135de920529", "checksum_algorithm": "sha256" }, { "name": "performance", "file": "skills/performance/SKILL.md", "version": "20260421022", - "checksum": "e8cf2eba2a3fdc162d03afeaba260bd0c2f0aac47121c26dc9d63614bff971ec", + "checksum": "fa4062c77381facacedac289b54a3ce7e6cf242dfa2b36f55401aebfe5fb8ae1", "checksum_algorithm": "sha256" }, { "name": "postmortem", "file": "skills/postmortem/SKILL.md", "version": "20260503001", - "checksum": "ead97f2a5b28672f4ac483e467eec5b0d2a7e3b96eceaaa2017c918972c2d827", + "checksum": "fc70f26420e705e0979ff76476ea8176386258f2315fcb57f5ac9049a37f6c90", "checksum_algorithm": "sha256" }, { "name": "pr", "file": "skills/pr/SKILL.md", "version": "20260502013", - "checksum": "ed4b13b69b325b21d9abf17ae9be34c2b2fa6ff1dd78cc6ee04af73d428438b8", + "checksum": "fc7b2b08696982afd48033a3cc29b9fa4ac8b590fc61c828b84135585c88b345", "checksum_algorithm": "sha256" }, { "name": "rancher", "file": "skills/rancher/SKILL.md", "version": "20260502038", - "checksum": "c86759f257553554f2069cb6d0eda93bd98fd5c2fb251b1c8cc423302f33b01d", + "checksum": "3000f24c2ee2f99fdb363f30d6075993ed26533663d081089b7800098a28e648", "checksum_algorithm": "sha256" }, { "name": "rca", "file": "skills/rca/SKILL.md", "version": "20260503001", - "checksum": "4d2347e1bab1d0717669b78160a5703ba4d1bb3486a9499924ca569e6f049f0e", + "checksum": "8bebb57c166c153d89e4589097ea419266b249df871a79e24c840112b5f8ad0c", "checksum_algorithm": "sha256" }, { "name": "refactor", "file": "skills/refactor/SKILL.md", "version": "20260421023", - "checksum": "7559d9148ee0ed7434162f672fe47329a417fd332af92a3637c47030e6b8f271", + "checksum": "aa7fb995b6ac649b24b61066f15a6fe7fddab81bdcec8af27eba65092b957d68", "checksum_algorithm": "sha256" }, { "name": "release-notes", "file": "skills/release-notes/SKILL.md", "version": "20260502014", - "checksum": "81fd326b93789aad264e1ad27a73d00efd76c4486b5a20935bafcb18ebd43758", + "checksum": "589f001bcbf0e27c4da6ff9c530371a1f183b7936e3a7fc421879282bd7f9af3", "checksum_algorithm": "sha256" }, { "name": "requirements", "file": "skills/requirements/SKILL.md", "version": "20260421024", - "checksum": "312e1c24bc43828798e86b881204196ebcad4f90ff5cfa63d099b8836e4f1e49", + "checksum": "7d3338f0b7c5d493ab714a90b7d1732de56467503a3fe4dacdd40031bfea356c", "checksum_algorithm": "sha256" }, { "name": "secret-scan", "file": "skills/secret-scan/SKILL.md", "version": "20260502028", - "checksum": "7a83d376b062bcf497ea89dd1e44a8f9fb945349d6b66513018ce1abe265e277", + "checksum": "95df047af89a93a8fa3716899b8fe703b270d7f40b93fe65915f7d1bdb849370", "checksum_algorithm": "sha256" }, { "name": "security", "file": "skills/security/SKILL.md", "version": "20260421025", - "checksum": "f58a629804bec6f6b5839eea1e7754458bf85cceb7403b9470f80513119a306d", + "checksum": "e9bcd451ee25b1c752239e76e20ca67302061356b59214fae7b134a180022df8", "checksum_algorithm": "sha256" }, { "name": "terraform", "file": "skills/terraform/SKILL.md", "version": "20260502030", - "checksum": "b24b008c171aafbf81902b3f52661b4acc09e8980e42ebf579ac3566dc138de5", + "checksum": "2bb933512f72b61dae3a3b632a7afd43c62aad066de003ee0a565fc8f1895215", "checksum_algorithm": "sha256" }, { "name": "terragrunt", "file": "skills/terragrunt/SKILL.md", "version": "20260502031", - "checksum": "33a208ccc4dfff01b59110445e0fd851e5c9d5e094711456c1cd1efdc68d440d", + "checksum": "e0d3dc0feefeafb9cd41d8d6885d8b4d53baed52ba70df7dc8d33bfc7410bae8", "checksum_algorithm": "sha256" }, { "name": "threat-model", "file": "skills/threat-model/SKILL.md", "version": "20260502021", - "checksum": "11504d508ddfc6e9e2ded8dfac8fe6fb7af691c7ef27fdc539c5f8f870fc06bb", + "checksum": "7fdf69113167e9bb8c866681a86f4b4d6416145bb9def0ebe18ceba690c8fbe9", "checksum_algorithm": "sha256" }, { "name": "verify", "file": "skills/verify/SKILL.md", "version": "20260421026", - "checksum": "b8f80fda903c0d55c556374626e308dcc61787d59e2e642f9c9513d06b0ac2b9", + "checksum": "aa56d1a674ed827606af34b8e7c5bd9c2de61a6cb6f92f5fd9bdce28e46bcb30", "checksum_algorithm": "sha256" }, { "name": "vision", "file": "skills/vision/SKILL.md", "version": "20260421027", - "checksum": "a74271c10816e3cee75fc1489d0b2defb391035b22f8d3da0cc951ae1afb7ef2", + "checksum": "c0058202130905d8495a3e565b5db546ad2d0ff6d654596358a0b12c484ce310", "checksum_algorithm": "sha256" } ], @@ -319,14 +319,14 @@ "name": "architect", "file": "agents/architect.agent.md", "version": "20260503022", - "checksum": "e02b3c3f71c5081f70556971672aef2ddbc52c855185cf1a58a5357c847cafdc", + "checksum": "95f6d8b98e8cbd8e6309b1708ff58462791959868b4052b44199c62dea4e0960", "checksum_algorithm": "sha256" }, { "name": "designer", "file": "agents/designer.agent.md", "version": "20260503024", - "checksum": "186f51a4f10003bc220a95c2d9d89c33546e19a970150e536c90822714c634d6", + "checksum": "2d593399bcf4c16475e0139d255bff80e4180856720e5bbc0735e226349c2727", "checksum_algorithm": "sha256" }, { @@ -340,14 +340,14 @@ "name": "product", "file": "agents/product.agent.md", "version": "20260503021", - "checksum": "e1928713b16fa07ca769dd62457a42d7415be6e531ae529af5a9e46e4c639518", + "checksum": "4bc343ed7b912988016aad7572da280a3824e46b00c86fa305fb334499a973b2", "checksum_algorithm": "sha256" }, { "name": "release", "file": "agents/release.agent.md", "version": "20260503020", - "checksum": "fa76f7fea418f40ffd3a18ba02f645a196e603ceda293e629ee41ce31e27dc6f", + "checksum": "f8496412e799033356ac67eeaaeb5addf29981d7f22235bf72971a533f6efc04", "checksum_algorithm": "sha256" }, { From 0d0ff4f2c76060887bc35fe2361602816afdd1d6 Mon Sep 17 00:00:00 2001 From: Erik Schaareman Date: Sun, 10 May 2026 00:50:56 +0200 Subject: [PATCH 24/25] docs(roadmap): correct orchestrated pipeline status from in-progress to candidate The workflow contract (workflow.stages, gate, hitl, handoffs) ships in this release, satisfying the prerequisite for ADR-024. However the planner agent and worker-agent wiring are not yet implemented; no planner template exists in src/vstack/_templates/agents/. Align the feature table (was 'in progress') with the detail section header (already 'candidate') and update the detail body to reference the shipped prerequisite and list concrete next steps. --- docs/product/roadmap.md | 75 ++++++++++++++++++++--------------------- 1 file changed, 37 insertions(+), 38 deletions(-) diff --git a/docs/product/roadmap.md b/docs/product/roadmap.md index 4ecc617..bbbe49c 100644 --- a/docs/product/roadmap.md +++ b/docs/product/roadmap.md @@ -7,36 +7,36 @@ ______________________________________________________________________ ## feature status table -| 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/.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. | shipped | `workflow:` block in `.vstack/config.yaml`; `gate`, `hitl`, `handoffs` schema; `vstack migrate` command (ADR-023, ADR-026) | -| optional orchestrated role pipeline | t.b.d. | in progress | `planner` coordinator agent using VS Code native subagents (ADR-024); supersedes ADR-004 | -| 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 | +| 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/.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. | shipped | `workflow:` block in `.vstack/config.yaml`; `gate`, `hitl`, `handoffs` schema; `vstack migrate` command (ADR-023, ADR-026) | +| optional orchestrated role pipeline | t.b.d. | candidate | `planner` coordinator agent using VS Code native subagents (ADR-024); prerequisite workflow contract shipped in this release; implementation not yet started | +| 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 | ______________________________________________________________________ @@ -260,15 +260,14 @@ Not yet implemented (deferred to orchestrated pipeline milestone): ### optional orchestrated role pipeline [candidate — t.b.d.] -Possible future workflow with explicit orchestration (only if real coordination bottlenecks appear): +ADR-024 is accepted and the prerequisite workflow contract (`workflow.stages`, `gate`, `hitl`, +`handoffs`) is shipped as of this release. The `planner` coordinator agent and worker-agent +wiring are not yet implemented. Next steps: -- 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 - -See `docs/design/workflow.md` for current execution and the orchestrated future model. +- Add `planner` agent template (`src/vstack/_templates/agents/planner/`) +- Set `user-invocable: false` on worker agents (or add a `planner`-scoped variant) +- Implement gate evaluation and `hitl` pause logic in the planner body +- Wire `runSubagent` calls based on `workflow.stages` order ### multi-IDE support [candidate — t.b.d.] From 7aa0c8b83d0118bd630e6afb53066979f2132a44 Mon Sep 17 00:00:00 2001 From: Erik Schaareman Date: Sun, 10 May 2026 01:03:15 +0200 Subject: [PATCH 25/25] fix(ci): add FORCE_JAVASCRIPT_ACTIONS_TO_NODE24 to all workflows; fix security.yml env Node.js 24 is now the supported runtime for GitHub Actions JavaScript runners. FORCE_JAVASCRIPT_ACTIONS_TO_NODE24 was only set in publish.yml; add it to check, commit, verify, security, release, and automerge. Also: - security.yml was missing POETRY_VIRTUALENVS_IN_PROJECT=true despite running poetry install; without it the venv lands in the global cache dir, making the 'cache: poetry' step in setup-python point at the wrong location - publish.yml had a dead 'cache: poetry' / 'cache-dependency-path' on the setup-python step; publish only runs 'poetry build', never 'poetry install', so no venv is created and the cache is never populated or restored --- .github/workflows/automerge.yml | 3 +++ .github/workflows/check.yml | 1 + .github/workflows/commit.yml | 1 + .github/workflows/publish.yml | 2 -- .github/workflows/release.yml | 3 +++ .github/workflows/security.yml | 3 +++ .github/workflows/verify.yml | 1 + 7 files changed, 12 insertions(+), 2 deletions(-) diff --git a/.github/workflows/automerge.yml b/.github/workflows/automerge.yml index 68e2486..712a65d 100644 --- a/.github/workflows/automerge.yml +++ b/.github/workflows/automerge.yml @@ -13,6 +13,9 @@ permissions: contents: write pull-requests: write +env: + FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true" + jobs: auto-merge: if: github.actor == 'dependabot[bot]' diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml index 4a4a383..e82876c 100644 --- a/.github/workflows/check.yml +++ b/.github/workflows/check.yml @@ -27,6 +27,7 @@ env: PYTHON_VERSION: "3.11" POETRY_VERSION: "2.3.4" POETRY_VIRTUALENVS_IN_PROJECT: "true" + FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true" jobs: test: diff --git a/.github/workflows/commit.yml b/.github/workflows/commit.yml index 20fc7c2..db1c6f7 100644 --- a/.github/workflows/commit.yml +++ b/.github/workflows/commit.yml @@ -28,6 +28,7 @@ env: PYTHON_VERSION: "3.11" POETRY_VERSION: "2.3.4" POETRY_VIRTUALENVS_IN_PROJECT: "true" + FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true" jobs: validate: diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 7e62612..7eef947 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -88,8 +88,6 @@ jobs: uses: actions/setup-python@v6 with: python-version: ${{ env.PYTHON_VERSION }} - cache: poetry - cache-dependency-path: poetry.lock - name: Validate release tag checkout run: | diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 7912aae..16db063 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -17,6 +17,9 @@ concurrency: permissions: contents: read +env: + FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true" + jobs: release: if: github.ref == 'refs/heads/main' diff --git a/.github/workflows/security.yml b/.github/workflows/security.yml index 8c85521..f8dd294 100644 --- a/.github/workflows/security.yml +++ b/.github/workflows/security.yml @@ -22,8 +22,11 @@ env: PYTHON_VERSION: "3.11" # Pin Poetry CLI version for deterministic CI behavior. POETRY_VERSION: "2.3.4" + # Keep Poetry virtual environments inside the workspace for deterministic paths. + POETRY_VIRTUALENVS_IN_PROJECT: "true" # Pin pip-audit to avoid non-deterministic CI failures from tool updates. PIP_AUDIT_VERSION: "2.9.0" + FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true" jobs: security: diff --git a/.github/workflows/verify.yml b/.github/workflows/verify.yml index ef79361..b8b3ef4 100644 --- a/.github/workflows/verify.yml +++ b/.github/workflows/verify.yml @@ -24,6 +24,7 @@ env: POETRY_VERSION: "2.3.4" # Keep Poetry virtual environments inside the workspace for deterministic paths. POETRY_VIRTUALENVS_IN_PROJECT: "true" + FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true" jobs: test-matrix: