vstack 0.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (119) hide show
  1. vstack/__init__.py +5 -0
  2. vstack/__main__.py +5 -0
  3. vstack/_templates/agents/_partials/agent-skill-boundary.md +5 -0
  4. vstack/_templates/agents/architect/config.yaml +38 -0
  5. vstack/_templates/agents/architect/template.md +84 -0
  6. vstack/_templates/agents/designer/config.yaml +36 -0
  7. vstack/_templates/agents/designer/template.md +99 -0
  8. vstack/_templates/agents/engineer/config.yaml +36 -0
  9. vstack/_templates/agents/engineer/template.md +88 -0
  10. vstack/_templates/agents/product/config.yaml +37 -0
  11. vstack/_templates/agents/product/template.md +87 -0
  12. vstack/_templates/agents/release/config.yaml +35 -0
  13. vstack/_templates/agents/release/template.md +86 -0
  14. vstack/_templates/agents/tester/config.yaml +41 -0
  15. vstack/_templates/agents/tester/template.md +90 -0
  16. vstack/_templates/instructions/git/config.yaml +4 -0
  17. vstack/_templates/instructions/git/template.md +36 -0
  18. vstack/_templates/instructions/python/config.yaml +4 -0
  19. vstack/_templates/instructions/python/template.md +37 -0
  20. vstack/_templates/prompts/code-review/config.yaml +10 -0
  21. vstack/_templates/prompts/code-review/template.md +39 -0
  22. vstack/_templates/skills/_partials/base-branch.md +8 -0
  23. vstack/_templates/skills/_partials/observability-checklist.md +36 -0
  24. vstack/_templates/skills/_partials/run-tests.md +22 -0
  25. vstack/_templates/skills/_partials/skill-context.md +21 -0
  26. vstack/_templates/skills/adr/config.yaml +17 -0
  27. vstack/_templates/skills/adr/template.md +167 -0
  28. vstack/_templates/skills/analyse/config.yaml +16 -0
  29. vstack/_templates/skills/analyse/template.md +188 -0
  30. vstack/_templates/skills/architecture/config.yaml +18 -0
  31. vstack/_templates/skills/architecture/template.md +213 -0
  32. vstack/_templates/skills/cicd/config.yaml +16 -0
  33. vstack/_templates/skills/cicd/template.md +169 -0
  34. vstack/_templates/skills/code-review/config.yaml +16 -0
  35. vstack/_templates/skills/code-review/template.md +180 -0
  36. vstack/_templates/skills/concise/config.yaml +16 -0
  37. vstack/_templates/skills/concise/template.md +128 -0
  38. vstack/_templates/skills/consult/config.yaml +18 -0
  39. vstack/_templates/skills/consult/template.md +195 -0
  40. vstack/_templates/skills/container/config.yaml +17 -0
  41. vstack/_templates/skills/container/template.md +122 -0
  42. vstack/_templates/skills/debug/config.yaml +16 -0
  43. vstack/_templates/skills/debug/template.md +247 -0
  44. vstack/_templates/skills/dependency/config.yaml +18 -0
  45. vstack/_templates/skills/dependency/template.md +293 -0
  46. vstack/_templates/skills/design/config.yaml +16 -0
  47. vstack/_templates/skills/design/template.md +231 -0
  48. vstack/_templates/skills/docs/config.yaml +17 -0
  49. vstack/_templates/skills/docs/template.md +128 -0
  50. vstack/_templates/skills/explore/config.yaml +17 -0
  51. vstack/_templates/skills/explore/template.md +188 -0
  52. vstack/_templates/skills/guardrails/config.yaml +16 -0
  53. vstack/_templates/skills/guardrails/template.md +45 -0
  54. vstack/_templates/skills/incident/config.yaml +17 -0
  55. vstack/_templates/skills/incident/template.md +293 -0
  56. vstack/_templates/skills/inspect/config.yaml +16 -0
  57. vstack/_templates/skills/inspect/template.md +105 -0
  58. vstack/_templates/skills/migrate/config.yaml +17 -0
  59. vstack/_templates/skills/migrate/template.md +298 -0
  60. vstack/_templates/skills/onboard/config.yaml +18 -0
  61. vstack/_templates/skills/onboard/template.md +289 -0
  62. vstack/_templates/skills/openapi/config.yaml +17 -0
  63. vstack/_templates/skills/openapi/template.md +382 -0
  64. vstack/_templates/skills/performance/config.yaml +15 -0
  65. vstack/_templates/skills/performance/template.md +198 -0
  66. vstack/_templates/skills/pr/config.yaml +15 -0
  67. vstack/_templates/skills/pr/template.md +108 -0
  68. vstack/_templates/skills/refactor/config.yaml +18 -0
  69. vstack/_templates/skills/refactor/template.md +283 -0
  70. vstack/_templates/skills/release-notes/config.yaml +16 -0
  71. vstack/_templates/skills/release-notes/template.md +127 -0
  72. vstack/_templates/skills/requirements/config.yaml +17 -0
  73. vstack/_templates/skills/requirements/template.md +187 -0
  74. vstack/_templates/skills/security/config.yaml +17 -0
  75. vstack/_templates/skills/security/template.md +256 -0
  76. vstack/_templates/skills/verify/config.yaml +17 -0
  77. vstack/_templates/skills/verify/template.md +201 -0
  78. vstack/_templates/skills/vision/config.yaml +19 -0
  79. vstack/_templates/skills/vision/template.md +169 -0
  80. vstack/agents/__init__.py +5 -0
  81. vstack/agents/config.py +67 -0
  82. vstack/agents/constants.py +14 -0
  83. vstack/agents/generator.py +20 -0
  84. vstack/artifacts/__init__.py +17 -0
  85. vstack/artifacts/config.py +111 -0
  86. vstack/artifacts/constants.py +6 -0
  87. vstack/artifacts/generator.py +406 -0
  88. vstack/artifacts/models.py +55 -0
  89. vstack/artifacts/protocol.py +50 -0
  90. vstack/cli/__init__.py +3 -0
  91. vstack/cli/commands.py +596 -0
  92. vstack/cli/constants.py +33 -0
  93. vstack/cli/manifest.py +166 -0
  94. vstack/cli/parser.py +156 -0
  95. vstack/constants.py +84 -0
  96. vstack/frontmatter/__init__.py +8 -0
  97. vstack/frontmatter/parser.py +272 -0
  98. vstack/frontmatter/schema.py +142 -0
  99. vstack/frontmatter/serializer.py +208 -0
  100. vstack/instructions/__init__.py +5 -0
  101. vstack/instructions/config.py +21 -0
  102. vstack/instructions/constants.py +9 -0
  103. vstack/instructions/generator.py +13 -0
  104. vstack/main.py +71 -0
  105. vstack/models.py +35 -0
  106. vstack/prompts/__init__.py +5 -0
  107. vstack/prompts/config.py +21 -0
  108. vstack/prompts/constants.py +9 -0
  109. vstack/prompts/generator.py +13 -0
  110. vstack/skills/__init__.py +5 -0
  111. vstack/skills/config.py +58 -0
  112. vstack/skills/constants.py +17 -0
  113. vstack/skills/generator.py +20 -0
  114. vstack/skills/models.py +15 -0
  115. vstack-0.0.0.dist-info/METADATA +725 -0
  116. vstack-0.0.0.dist-info/RECORD +119 -0
  117. vstack-0.0.0.dist-info/WHEEL +4 -0
  118. vstack-0.0.0.dist-info/entry_points.txt +3 -0
  119. vstack-0.0.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,142 @@
1
+ """Frontmatter schema types — FieldSpec, FrontmatterSchema, FieldType.
2
+
3
+ These are the building blocks used to declare which fields are valid in a
4
+ frontmatter block, how to serialise them, and how to validate them.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import re
10
+ from dataclasses import dataclass, field
11
+ from typing import Literal
12
+
13
+ FieldType = Literal["str", "bool", "list", "object-list", "raw"]
14
+
15
+
16
+ @dataclass
17
+ class FieldSpec:
18
+ """Describes a single frontmatter field and how to serialise it.
19
+
20
+ Attributes:
21
+ name: Field key as it appears in the frontmatter block.
22
+ type: Serialisation type: ``"str"``, ``"bool"``, ``"list"``,
23
+ ``"object-list"``, or ``"raw"`` (verbatim indented YAML block).
24
+ required: When ``True``, :meth:`FrontmatterSchema.validate_meta` will
25
+ flag its absence as an error.
26
+ max_length: Truncate string values to this many characters before quoting.
27
+ quoted: Wrap string values in single quotes (``'…'``).
28
+ normalize_whitespace: Collapse runs of whitespace before serialising.
29
+ pattern: Optional regular expression that string values must match.
30
+ item_schema: For ``"object-list"`` fields: an optional
31
+ :class:`FrontmatterSchema` that describes the keys of each
32
+ list item. When set, serialisation output is ordered and
33
+ filtered by this schema, and :meth:`FrontmatterSchema.validate_meta`
34
+ recurses into each item.
35
+ """
36
+
37
+ name: str
38
+ type: FieldType = "str"
39
+ required: bool = False
40
+ max_length: int | None = None
41
+ quoted: bool = True
42
+ normalize_whitespace: bool = False
43
+ pattern: str | None = None
44
+ item_schema: FrontmatterSchema | None = None
45
+
46
+
47
+ @dataclass
48
+ class FrontmatterSchema:
49
+ """Ordered list of fields recognised in a frontmatter block.
50
+
51
+ Fields present in *meta* but absent from the schema are silently dropped
52
+ when building output. The declaration order determines the output order.
53
+ """
54
+
55
+ fields: list[FieldSpec] = field(default_factory=list)
56
+
57
+ def get(self, name: str) -> FieldSpec | None:
58
+ """Return the :class:`FieldSpec` for *name*, or ``None`` if not declared."""
59
+ return next((f for f in self.fields if f.name == name), None)
60
+
61
+ @staticmethod
62
+ def _validate_bool_field(spec: FieldSpec, value: object, errors: list[str]) -> None:
63
+ """Validate one bool-like field value."""
64
+ if str(value).lower() not in ("true", "false"):
65
+ errors.append(f"field '{spec.name}' must be 'true' or 'false', got: {value!r}")
66
+
67
+ @staticmethod
68
+ def _validate_list_field(spec: FieldSpec, value: object, errors: list[str]) -> None:
69
+ """Validate one list field value."""
70
+ if not isinstance(value, list):
71
+ errors.append(f"field '{spec.name}' must be a list, got: {value!r}")
72
+
73
+ @staticmethod
74
+ def _validate_object_list_field(spec: FieldSpec, value: object, errors: list[str]) -> None:
75
+ """Validate one object-list field value."""
76
+ if not isinstance(value, list):
77
+ errors.append(f"field '{spec.name}' must be a list, got: {value!r}")
78
+ return
79
+
80
+ for i, item in enumerate(value):
81
+ if not isinstance(item, dict):
82
+ errors.append(f"field '{spec.name}[{i}]' must be a mapping, got: {item!r}")
83
+ continue
84
+ if spec.item_schema is None:
85
+ continue
86
+ for err in spec.item_schema.validate_meta(item):
87
+ errors.append(f"{spec.name}[{i}].{err}")
88
+
89
+ @staticmethod
90
+ def _validate_str_field(spec: FieldSpec, value: object, errors: list[str]) -> None:
91
+ """Validate one string-like field value."""
92
+ if spec.max_length and isinstance(value, str) and len(value) > spec.max_length:
93
+ errors.append(
94
+ f"field '{spec.name}' exceeds max length {spec.max_length} ({len(value)} chars)"
95
+ )
96
+ if spec.pattern and isinstance(value, str) and not re.fullmatch(spec.pattern, value):
97
+ errors.append(
98
+ f"field '{spec.name}' does not match required pattern {spec.pattern!r}: {value!r}"
99
+ )
100
+
101
+ def _validate_field_value(self, spec: FieldSpec, value: object, errors: list[str]) -> None:
102
+ """Validate a field value according to its declared field type."""
103
+ if spec.type == "bool":
104
+ self._validate_bool_field(spec, value, errors)
105
+ return
106
+ if spec.type == "list":
107
+ self._validate_list_field(spec, value, errors)
108
+ return
109
+ if spec.type == "object-list":
110
+ self._validate_object_list_field(spec, value, errors)
111
+ return
112
+ if spec.type == "raw":
113
+ return
114
+ self._validate_str_field(spec, value, errors)
115
+
116
+ def validate_meta(self, meta: dict) -> list[str]:
117
+ """Validate *meta* against declared schema fields.
118
+
119
+ Checks performed:
120
+
121
+ * Required fields are present and non-empty.
122
+ * ``"bool"`` fields contain ``"true"`` or ``"false"`` (case-insensitive).
123
+ * ``"list"`` fields contain a Python list.
124
+ * ``"object-list"`` fields contain a list of dicts; when
125
+ :attr:`FieldSpec.item_schema` is set each item is validated recursively.
126
+ * ``"str"`` fields do not exceed :attr:`FieldSpec.max_length`.
127
+
128
+ Unknown fields in *meta* (not declared in the schema) are silently ignored.
129
+
130
+ Returns:
131
+ A list of human-readable error strings. An empty list means valid.
132
+ """
133
+ errors: list[str] = []
134
+ for spec in self.fields:
135
+ value = meta.get(spec.name)
136
+ if spec.required and not value:
137
+ errors.append(f"required field '{spec.name}' is missing or empty")
138
+ continue
139
+ if value is None:
140
+ continue
141
+ self._validate_field_value(spec, value, errors)
142
+ return errors
@@ -0,0 +1,208 @@
1
+ """Frontmatter serializer — schema-filtered YAML output.
2
+
3
+ :class:`FrontmatterSerializer` converts a metadata dict to a ``---`` / ``---`` YAML
4
+ frontmatter block, filtered and ordered by a :class:`~vstack.frontmatter.FrontmatterSchema`.
5
+ Fields absent from the schema are silently dropped so generator-internal
6
+ metadata (``version``, etc.) never leaks into output files.
7
+
8
+ Main entry point: :meth:`FrontmatterSerializer.serialize`.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import re
14
+ import textwrap
15
+
16
+ from vstack.frontmatter.schema import FieldSpec, FrontmatterSchema
17
+
18
+
19
+ class FrontmatterSerializer:
20
+ """Frontmatter serializer — converts metadata dict to YAML.
21
+
22
+ Instantiate once, then call :meth:`serialize` to render frontmatter.
23
+ No mutable instance state is retained between calls.
24
+ """
25
+
26
+ def _serialize_scalar(self, spec: FieldSpec, value: object) -> str:
27
+ """Serialize a single ``"str"`` value according to *spec* options."""
28
+ text = str(value)
29
+ if spec.normalize_whitespace:
30
+ text = re.sub(r"\s+", " ", text).strip()
31
+ if spec.max_length:
32
+ text = text[: spec.max_length]
33
+ if spec.quoted:
34
+ return f"'{text.replace(chr(39), chr(39) * 2)}'"
35
+ return text
36
+
37
+ def _serialize_multiline_scalar(
38
+ self, name: str, value: object, base_indent: str = ""
39
+ ) -> list[str]:
40
+ """Serialize a string scalar as YAML folded block (``>-``) lines."""
41
+ text = str(value).strip()
42
+ wrapped_lines: list[str] = []
43
+ for paragraph in text.splitlines() or [text]:
44
+ if not paragraph.strip():
45
+ wrapped_lines.append("")
46
+ continue
47
+ wrapped_lines.extend(textwrap.wrap(paragraph.strip(), width=100))
48
+ out = [f"{base_indent}{name}: >-"]
49
+ out.extend(f"{base_indent} {line}" for line in wrapped_lines if line != "")
50
+ return out
51
+
52
+ def _should_emit_multiline(self, value: object, preserve_multiline: bool) -> bool:
53
+ """Return ``True`` when a scalar should use folded-block YAML output."""
54
+ if not preserve_multiline:
55
+ return False
56
+ text = str(value)
57
+ return "\n" in text or len(text) > 90
58
+
59
+ def _serialize_bool(self, value: object) -> str:
60
+ """Return ``"true"`` or ``"false"`` regardless of input representation."""
61
+ if isinstance(value, bool):
62
+ return str(value).lower()
63
+ return "true" if str(value).strip().lower() == "true" else "false"
64
+
65
+ def _serialize_object_unschematized(
66
+ self, item: dict, preserve_multiline: bool = False
67
+ ) -> list[str]:
68
+ """Serialize object without schema — accepts any keys/values."""
69
+ lines: list[str] = []
70
+ for k, v in item.items():
71
+ v_str = str(v).strip()
72
+ if isinstance(v, bool) or v_str.lower() in ("true", "false"):
73
+ lines.append(f"{k}: {self._serialize_bool(v)}")
74
+ elif self._should_emit_multiline(v, preserve_multiline):
75
+ lines.extend(self._serialize_multiline_scalar(k, v))
76
+ else:
77
+ safe = v_str.replace("'", "''")
78
+ lines.append(f"{k}: '{safe}'")
79
+ return lines
80
+
81
+ def _serialize_object_field_pair(
82
+ self,
83
+ spec: FieldSpec,
84
+ value: object,
85
+ preserve_multiline: bool = False,
86
+ ) -> list[str]:
87
+ """Serialize a single schematized field/value pair for an object-list item."""
88
+ if spec.type == "bool":
89
+ return [f"{spec.name}: {self._serialize_bool(value)}"]
90
+ if spec.type == "list":
91
+ if isinstance(value, list) and value:
92
+ lines = [f"{spec.name}:"]
93
+ lines.extend(f" - {item_v}" for item_v in value)
94
+ return lines
95
+ return []
96
+ if self._should_emit_multiline(value, preserve_multiline):
97
+ return self._serialize_multiline_scalar(spec.name, value)
98
+ return [f"{spec.name}: {self._serialize_scalar(spec, value)}"]
99
+
100
+ def _serialize_object(
101
+ self,
102
+ item: dict,
103
+ item_schema: FrontmatterSchema | None,
104
+ preserve_multiline: bool = False,
105
+ ) -> list[str]:
106
+ """Serialize one object-list item to YAML lines (without leading `` - ``)."""
107
+ if item_schema is None:
108
+ return self._serialize_object_unschematized(item, preserve_multiline)
109
+
110
+ pairs = [
111
+ (spec, item.get(spec.name))
112
+ for spec in item_schema.fields
113
+ if item.get(spec.name) is not None
114
+ ]
115
+ ordered_lines: list[str] = []
116
+ for spec, value in pairs:
117
+ ordered_lines.extend(self._serialize_object_field_pair(spec, value, preserve_multiline))
118
+ return ordered_lines
119
+
120
+ def _append_object_list_items(
121
+ self,
122
+ lines: list[str],
123
+ value: list,
124
+ item_schema: FrontmatterSchema | None,
125
+ preserve_multiline: bool = False,
126
+ ) -> None:
127
+ """Append object-list items to lines with proper YAML indentation."""
128
+ for item in value:
129
+ if not isinstance(item, dict):
130
+ continue
131
+ obj_lines = self._serialize_object(
132
+ item,
133
+ item_schema,
134
+ preserve_multiline=preserve_multiline,
135
+ )
136
+ for i, obj_line in enumerate(obj_lines):
137
+ prefix = " - " if i == 0 else " "
138
+ lines.append(f"{prefix}{obj_line}")
139
+
140
+ def _append_raw_field(self, lines: list[str], name: str, value: object) -> None:
141
+ """Append a raw YAML field value without additional serialization."""
142
+ raw_str = str(value).strip() if value is not None else ""
143
+ if raw_str:
144
+ lines.append(f"{name}:")
145
+ for raw_line in str(value).split("\n"):
146
+ lines.append(raw_line)
147
+
148
+ def _append_field_by_type(
149
+ self,
150
+ lines: list[str],
151
+ spec: FieldSpec,
152
+ value: object,
153
+ preserve_multiline: bool = False,
154
+ ) -> None:
155
+ """Dispatch field rendering by type."""
156
+ if spec.type == "bool":
157
+ lines.append(f"{spec.name}: {self._serialize_bool(value)}")
158
+ return
159
+ if spec.type == "list":
160
+ if isinstance(value, list) and value:
161
+ lines.append(f"{spec.name}:")
162
+ for item in value:
163
+ lines.append(f" - {item}")
164
+ return
165
+ if spec.type == "object-list":
166
+ if isinstance(value, list) and value:
167
+ lines.append(f"{spec.name}:")
168
+ self._append_object_list_items(
169
+ lines,
170
+ value,
171
+ spec.item_schema,
172
+ preserve_multiline=preserve_multiline,
173
+ )
174
+ return
175
+ if spec.type == "raw":
176
+ self._append_raw_field(lines, spec.name, value)
177
+ return
178
+ if self._should_emit_multiline(value, preserve_multiline):
179
+ lines.extend(self._serialize_multiline_scalar(spec.name, value))
180
+ else:
181
+ lines.append(f"{spec.name}: {self._serialize_scalar(spec, value)}")
182
+
183
+ def serialize(
184
+ self,
185
+ meta: dict,
186
+ schema: FrontmatterSchema,
187
+ preserve_multiline: bool = False,
188
+ ) -> str:
189
+ """Serialize metadata into a VS Code frontmatter block.
190
+
191
+ Args:
192
+ meta: Input metadata values to serialize.
193
+ schema: Ordered frontmatter schema that filters and formats fields.
194
+ preserve_multiline: When ``True``, emit long strings as folded YAML
195
+ block scalars where appropriate.
196
+
197
+ Returns:
198
+ A YAML frontmatter block including opening and closing ``---`` lines.
199
+ """
200
+ lines = ["---"]
201
+ for spec in schema.fields:
202
+ value = meta.get(spec.name)
203
+ if value is None:
204
+ continue
205
+ self._append_field_by_type(lines, spec, value, preserve_multiline)
206
+ lines.append("---")
207
+ lines.append("")
208
+ return "\n".join(lines)
@@ -0,0 +1,5 @@
1
+ """Package initialization for vstack.instructions."""
2
+
3
+ from vstack.instructions.generator import InstructionGenerator
4
+
5
+ __all__ = ["InstructionGenerator"]
@@ -0,0 +1,21 @@
1
+ """Instruction artifact type configuration."""
2
+
3
+ from vstack.artifacts.config import INSTRUCTION_SCHEMA, ArtifactTypeConfig
4
+ from vstack.instructions.constants import (
5
+ INSTRUCTION_OUTPUT_SUBDIR,
6
+ INSTRUCTION_OUTPUT_SUFFIX,
7
+ INSTRUCTION_TEMPLATES_SUBDIR,
8
+ )
9
+
10
+ INSTRUCTION_TYPE = ArtifactTypeConfig(
11
+ type_name="instruction",
12
+ templates_dir=INSTRUCTION_TEMPLATES_SUBDIR,
13
+ output_subdir=INSTRUCTION_OUTPUT_SUBDIR,
14
+ output_pattern=f"{{name}}{INSTRUCTION_OUTPUT_SUFFIX}",
15
+ add_frontmatter=True,
16
+ artifact_is_dir=False,
17
+ partials_subdir=None,
18
+ auto_gen_footer=True,
19
+ fail_on_unresolved=False,
20
+ frontmatter_schema=INSTRUCTION_SCHEMA,
21
+ )
@@ -0,0 +1,9 @@
1
+ """Constants for instruction template and output locations."""
2
+
3
+ from vstack.constants import TEMPLATES_ROOT
4
+
5
+ INSTRUCTION_OUTPUT_SUFFIX = ".instructions.md"
6
+ INSTRUCTION_TEMPLATES_SUBDIR = "instructions"
7
+ INSTRUCTION_OUTPUT_SUBDIR = "instructions"
8
+
9
+ INSTRUCTION_TEMPLATES_DIR = TEMPLATES_ROOT / INSTRUCTION_TEMPLATES_SUBDIR
@@ -0,0 +1,13 @@
1
+ """Thin instruction generator wrapper over ``GenericArtifactGenerator``."""
2
+
3
+ from vstack.artifacts.generator import GenericArtifactGenerator
4
+ from vstack.constants import TEMPLATES_ROOT
5
+ from vstack.instructions.config import INSTRUCTION_TYPE
6
+
7
+
8
+ class InstructionGenerator(GenericArtifactGenerator):
9
+ """Generate instruction artifacts using the built-in instruction configuration."""
10
+
11
+ def __init__(self) -> None:
12
+ """Create an instruction generator bound to the built-in template root."""
13
+ super().__init__(INSTRUCTION_TYPE, TEMPLATES_ROOT)
vstack/main.py ADDED
@@ -0,0 +1,71 @@
1
+ """CLI entrypoint and command dispatch helpers."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import sys
6
+
7
+ from vstack.cli.commands import CommandLineInterface
8
+ from vstack.cli.parser import CommandLineParser
9
+ from vstack.constants import TEMPLATES_ROOT
10
+
11
+ _GLOBAL_SUPPORTED_TYPES = ["agent", "instruction", "prompt", "skill"]
12
+
13
+
14
+ def _resolve_only_for_scope(args: object) -> list[str] | None:
15
+ """Resolve the active artifact-type filter for the parsed CLI arguments.
16
+
17
+ Args:
18
+ args: Parsed CLI arguments object, typically from ``argparse``.
19
+
20
+ Returns:
21
+ The explicit ``--only`` filter for the active scope, the default
22
+ global-profile artifact set, or ``None`` when no filter applies.
23
+ """
24
+ requested_only = getattr(args, "only", None)
25
+ if not getattr(args, "use_global", False):
26
+ return requested_only
27
+
28
+ if requested_only is None:
29
+ return list(_GLOBAL_SUPPORTED_TYPES)
30
+
31
+ disallowed = [t for t in requested_only if t not in _GLOBAL_SUPPORTED_TYPES]
32
+ if disallowed:
33
+ print(
34
+ "ERROR: --global supports only agents, prompts, and instructions. "
35
+ f"Unsupported type(s): {', '.join(disallowed)}",
36
+ file=sys.stderr,
37
+ )
38
+ sys.exit(1)
39
+
40
+ return requested_only
41
+
42
+
43
+ def main() -> None:
44
+ """Parse CLI arguments and dispatch the selected top-level command."""
45
+ cli_parser = CommandLineParser()
46
+ parser = cli_parser.build()
47
+ args = parser.parse_args()
48
+ cli = CommandLineInterface(templates_root=TEMPLATES_ROOT)
49
+
50
+ if args.command == "validate":
51
+ sys.exit(cli.validate(only=getattr(args, "only", None)))
52
+
53
+ install_dir = cli_parser.resolve_targets(args)
54
+ only = _resolve_only_for_scope(args) if args.command in {"install", "verify"} else None
55
+ dispatch = {
56
+ "verify": lambda: cli.verify(
57
+ install_dir=install_dir,
58
+ source=getattr(args, "source", True),
59
+ output=getattr(args, "output", True),
60
+ only=only,
61
+ ),
62
+ "install": lambda: cli.install(
63
+ install_dir,
64
+ only=only,
65
+ force=getattr(args, "force", False),
66
+ update=getattr(args, "update", False),
67
+ dry_run=getattr(args, "dry_run", False),
68
+ ),
69
+ "uninstall": lambda: cli.uninstall(install_dir),
70
+ }
71
+ sys.exit(dispatch[args.command]())
vstack/models.py ADDED
@@ -0,0 +1,35 @@
1
+ """Shared validation result models."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass, field
6
+
7
+
8
+ @dataclass
9
+ class CheckMessage:
10
+ """Represent a single validation message with a pass/fail level."""
11
+
12
+ level: str # "pass" | "fail"
13
+ message: str
14
+
15
+
16
+ @dataclass
17
+ class ValidationResult:
18
+ """Store validation messages and expose aggregate result helpers."""
19
+
20
+ messages: list[CheckMessage] = field(default_factory=list)
21
+
22
+ @property
23
+ def passes(self) -> int:
24
+ """Return the number of passing validation messages."""
25
+ return sum(1 for m in self.messages if m.level == "pass")
26
+
27
+ @property
28
+ def failures(self) -> int:
29
+ """Return the number of failing validation messages."""
30
+ return sum(1 for m in self.messages if m.level == "fail")
31
+
32
+ @property
33
+ def ok(self) -> bool:
34
+ """Return ``True`` when the validation result contains no failures."""
35
+ return self.failures == 0
@@ -0,0 +1,5 @@
1
+ """Package initialization for vstack.prompts."""
2
+
3
+ from vstack.prompts.generator import PromptGenerator
4
+
5
+ __all__ = ["PromptGenerator"]
@@ -0,0 +1,21 @@
1
+ """Prompt artifact type configuration."""
2
+
3
+ from vstack.artifacts.config import PROMPT_SCHEMA, ArtifactTypeConfig
4
+ from vstack.prompts.constants import (
5
+ PROMPT_OUTPUT_SUBDIR,
6
+ PROMPT_OUTPUT_SUFFIX,
7
+ PROMPT_TEMPLATES_SUBDIR,
8
+ )
9
+
10
+ PROMPT_TYPE = ArtifactTypeConfig(
11
+ type_name="prompt",
12
+ templates_dir=PROMPT_TEMPLATES_SUBDIR,
13
+ output_subdir=PROMPT_OUTPUT_SUBDIR,
14
+ output_pattern=f"{{name}}{PROMPT_OUTPUT_SUFFIX}",
15
+ add_frontmatter=True,
16
+ artifact_is_dir=False,
17
+ partials_subdir=None,
18
+ auto_gen_footer=True,
19
+ fail_on_unresolved=False,
20
+ frontmatter_schema=PROMPT_SCHEMA,
21
+ )
@@ -0,0 +1,9 @@
1
+ """Constants for prompt template and output locations."""
2
+
3
+ from vstack.constants import TEMPLATES_ROOT
4
+
5
+ PROMPT_OUTPUT_SUFFIX = ".prompt.md"
6
+ PROMPT_TEMPLATES_SUBDIR = "prompts"
7
+ PROMPT_OUTPUT_SUBDIR = "prompts"
8
+
9
+ PROMPT_TEMPLATES_DIR = TEMPLATES_ROOT / PROMPT_TEMPLATES_SUBDIR
@@ -0,0 +1,13 @@
1
+ """Thin prompt generator wrapper over ``GenericArtifactGenerator``."""
2
+
3
+ from vstack.artifacts.generator import GenericArtifactGenerator
4
+ from vstack.constants import TEMPLATES_ROOT
5
+ from vstack.prompts.config import PROMPT_TYPE
6
+
7
+
8
+ class PromptGenerator(GenericArtifactGenerator):
9
+ """Generate prompt artifacts using the built-in prompt configuration."""
10
+
11
+ def __init__(self) -> None:
12
+ """Create a prompt generator bound to the built-in template root."""
13
+ super().__init__(PROMPT_TYPE, TEMPLATES_ROOT)
@@ -0,0 +1,5 @@
1
+ """Package initialization for vstack.skills."""
2
+
3
+ from vstack.skills.generator import SkillGenerator
4
+
5
+ __all__ = ["SkillGenerator"]
@@ -0,0 +1,58 @@
1
+ """Skill artifact type configuration and frontmatter schema.
2
+
3
+ Defines the :data:`SKILL_SCHEMA` (which frontmatter fields a ``SKILL.md`` file
4
+ uses) and the :data:`SKILL_TYPE` descriptor that configures the generator for
5
+ the ``skills`` artifact family.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from vstack.artifacts.config import ArtifactTypeConfig
11
+ from vstack.frontmatter import FieldSpec, FrontmatterSchema
12
+ from vstack.skills.constants import (
13
+ SKILL_OUTPUT_SUBDIR,
14
+ SKILL_TEMPLATES_SUBDIR,
15
+ SKILL_TMPL_NAME,
16
+ )
17
+
18
+ #: Fields recognised in VS Code Agent Skill files (``SKILL.md``).
19
+ SKILL_SCHEMA = FrontmatterSchema(
20
+ [
21
+ FieldSpec(
22
+ "name",
23
+ required=True,
24
+ quoted=False,
25
+ max_length=64,
26
+ pattern=r"[a-z0-9]+(?:-[a-z0-9]+)*",
27
+ ),
28
+ FieldSpec("description", required=True, max_length=1024, normalize_whitespace=True),
29
+ FieldSpec("license", max_length=256),
30
+ FieldSpec("compatibility", max_length=500, normalize_whitespace=True),
31
+ # Agent Skills spec: arbitrary key/value mapping. We preserve this as raw YAML.
32
+ FieldSpec("metadata", type="raw"),
33
+ FieldSpec("argument-hint"),
34
+ FieldSpec("user-invocable", type="bool"),
35
+ FieldSpec("disable-model-invocation", type="bool"),
36
+ ]
37
+ )
38
+
39
+ #: Type descriptor for the ``skills`` artifact family.
40
+ SKILL_TYPE = ArtifactTypeConfig(
41
+ type_name="skill",
42
+ templates_dir=SKILL_TEMPLATES_SUBDIR,
43
+ output_subdir=SKILL_OUTPUT_SUBDIR,
44
+ output_pattern="{name}/SKILL.md",
45
+ add_frontmatter=True,
46
+ artifact_is_dir=True,
47
+ partials_subdir="_partials",
48
+ auto_gen_footer=True,
49
+ placeholders={
50
+ "SKILL_CONTEXT": "skill-context.md",
51
+ "BASE_BRANCH": "base-branch.md",
52
+ "RUN_TESTS": "run-tests.md",
53
+ "OBSERVABILITY_CHECKLIST": "observability-checklist.md",
54
+ },
55
+ fail_on_unresolved=True,
56
+ template_filename=SKILL_TMPL_NAME,
57
+ frontmatter_schema=SKILL_SCHEMA,
58
+ )
@@ -0,0 +1,17 @@
1
+ """Constants for skill template and partial locations."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from vstack.constants import TEMPLATES_ROOT
6
+
7
+ #: Source template filename.
8
+ SKILL_TMPL_NAME = "template.md"
9
+
10
+ #: Subdirectory name under ``templates/`` holding skill source templates.
11
+ SKILL_TEMPLATES_SUBDIR = "skills"
12
+
13
+ #: Subdirectory name under the install root (e.g. ``.github/``) for skill output.
14
+ SKILL_OUTPUT_SUBDIR = "skills"
15
+
16
+ SKILLS_TEMPLATES_DIR = TEMPLATES_ROOT / SKILL_TEMPLATES_SUBDIR
17
+ SKILLS_PARTIALS_DIR = TEMPLATES_ROOT / SKILL_TEMPLATES_SUBDIR / "_partials"
@@ -0,0 +1,20 @@
1
+ """SkillGenerator — thin wrapper around GenericArtifactGenerator for skills.
2
+
3
+ Import :class:`SkillGenerator` to get a generator pre-configured for the
4
+ ``skills`` artifact type. All behaviour is inherited from
5
+ :class:`~vstack.artifacts.generator.GenericArtifactGenerator`.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from vstack.artifacts.generator import GenericArtifactGenerator
11
+ from vstack.constants import TEMPLATES_ROOT
12
+ from vstack.skills.config import SKILL_TYPE
13
+
14
+
15
+ class SkillGenerator(GenericArtifactGenerator):
16
+ """Generate skill artifacts using the built-in skill type configuration."""
17
+
18
+ def __init__(self) -> None:
19
+ """Create a skill generator bound to the built-in template root."""
20
+ super().__init__(SKILL_TYPE, TEMPLATES_ROOT)