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.
- vstack/__init__.py +5 -0
- vstack/__main__.py +5 -0
- vstack/_templates/agents/_partials/agent-skill-boundary.md +5 -0
- vstack/_templates/agents/architect/config.yaml +38 -0
- vstack/_templates/agents/architect/template.md +84 -0
- vstack/_templates/agents/designer/config.yaml +36 -0
- vstack/_templates/agents/designer/template.md +99 -0
- vstack/_templates/agents/engineer/config.yaml +36 -0
- vstack/_templates/agents/engineer/template.md +88 -0
- vstack/_templates/agents/product/config.yaml +37 -0
- vstack/_templates/agents/product/template.md +87 -0
- vstack/_templates/agents/release/config.yaml +35 -0
- vstack/_templates/agents/release/template.md +86 -0
- vstack/_templates/agents/tester/config.yaml +41 -0
- vstack/_templates/agents/tester/template.md +90 -0
- vstack/_templates/instructions/git/config.yaml +4 -0
- vstack/_templates/instructions/git/template.md +36 -0
- vstack/_templates/instructions/python/config.yaml +4 -0
- vstack/_templates/instructions/python/template.md +37 -0
- vstack/_templates/prompts/code-review/config.yaml +10 -0
- vstack/_templates/prompts/code-review/template.md +39 -0
- vstack/_templates/skills/_partials/base-branch.md +8 -0
- vstack/_templates/skills/_partials/observability-checklist.md +36 -0
- vstack/_templates/skills/_partials/run-tests.md +22 -0
- vstack/_templates/skills/_partials/skill-context.md +21 -0
- vstack/_templates/skills/adr/config.yaml +17 -0
- vstack/_templates/skills/adr/template.md +167 -0
- vstack/_templates/skills/analyse/config.yaml +16 -0
- vstack/_templates/skills/analyse/template.md +188 -0
- vstack/_templates/skills/architecture/config.yaml +18 -0
- vstack/_templates/skills/architecture/template.md +213 -0
- vstack/_templates/skills/cicd/config.yaml +16 -0
- vstack/_templates/skills/cicd/template.md +169 -0
- vstack/_templates/skills/code-review/config.yaml +16 -0
- vstack/_templates/skills/code-review/template.md +180 -0
- vstack/_templates/skills/concise/config.yaml +16 -0
- vstack/_templates/skills/concise/template.md +128 -0
- vstack/_templates/skills/consult/config.yaml +18 -0
- vstack/_templates/skills/consult/template.md +195 -0
- vstack/_templates/skills/container/config.yaml +17 -0
- vstack/_templates/skills/container/template.md +122 -0
- vstack/_templates/skills/debug/config.yaml +16 -0
- vstack/_templates/skills/debug/template.md +247 -0
- vstack/_templates/skills/dependency/config.yaml +18 -0
- vstack/_templates/skills/dependency/template.md +293 -0
- vstack/_templates/skills/design/config.yaml +16 -0
- vstack/_templates/skills/design/template.md +231 -0
- vstack/_templates/skills/docs/config.yaml +17 -0
- vstack/_templates/skills/docs/template.md +128 -0
- vstack/_templates/skills/explore/config.yaml +17 -0
- vstack/_templates/skills/explore/template.md +188 -0
- vstack/_templates/skills/guardrails/config.yaml +16 -0
- vstack/_templates/skills/guardrails/template.md +45 -0
- vstack/_templates/skills/incident/config.yaml +17 -0
- vstack/_templates/skills/incident/template.md +293 -0
- vstack/_templates/skills/inspect/config.yaml +16 -0
- vstack/_templates/skills/inspect/template.md +105 -0
- vstack/_templates/skills/migrate/config.yaml +17 -0
- vstack/_templates/skills/migrate/template.md +298 -0
- vstack/_templates/skills/onboard/config.yaml +18 -0
- vstack/_templates/skills/onboard/template.md +289 -0
- vstack/_templates/skills/openapi/config.yaml +17 -0
- vstack/_templates/skills/openapi/template.md +382 -0
- vstack/_templates/skills/performance/config.yaml +15 -0
- vstack/_templates/skills/performance/template.md +198 -0
- vstack/_templates/skills/pr/config.yaml +15 -0
- vstack/_templates/skills/pr/template.md +108 -0
- vstack/_templates/skills/refactor/config.yaml +18 -0
- vstack/_templates/skills/refactor/template.md +283 -0
- vstack/_templates/skills/release-notes/config.yaml +16 -0
- vstack/_templates/skills/release-notes/template.md +127 -0
- vstack/_templates/skills/requirements/config.yaml +17 -0
- vstack/_templates/skills/requirements/template.md +187 -0
- vstack/_templates/skills/security/config.yaml +17 -0
- vstack/_templates/skills/security/template.md +256 -0
- vstack/_templates/skills/verify/config.yaml +17 -0
- vstack/_templates/skills/verify/template.md +201 -0
- vstack/_templates/skills/vision/config.yaml +19 -0
- vstack/_templates/skills/vision/template.md +169 -0
- vstack/agents/__init__.py +5 -0
- vstack/agents/config.py +67 -0
- vstack/agents/constants.py +14 -0
- vstack/agents/generator.py +20 -0
- vstack/artifacts/__init__.py +17 -0
- vstack/artifacts/config.py +111 -0
- vstack/artifacts/constants.py +6 -0
- vstack/artifacts/generator.py +406 -0
- vstack/artifacts/models.py +55 -0
- vstack/artifacts/protocol.py +50 -0
- vstack/cli/__init__.py +3 -0
- vstack/cli/commands.py +596 -0
- vstack/cli/constants.py +33 -0
- vstack/cli/manifest.py +166 -0
- vstack/cli/parser.py +156 -0
- vstack/constants.py +84 -0
- vstack/frontmatter/__init__.py +8 -0
- vstack/frontmatter/parser.py +272 -0
- vstack/frontmatter/schema.py +142 -0
- vstack/frontmatter/serializer.py +208 -0
- vstack/instructions/__init__.py +5 -0
- vstack/instructions/config.py +21 -0
- vstack/instructions/constants.py +9 -0
- vstack/instructions/generator.py +13 -0
- vstack/main.py +71 -0
- vstack/models.py +35 -0
- vstack/prompts/__init__.py +5 -0
- vstack/prompts/config.py +21 -0
- vstack/prompts/constants.py +9 -0
- vstack/prompts/generator.py +13 -0
- vstack/skills/__init__.py +5 -0
- vstack/skills/config.py +58 -0
- vstack/skills/constants.py +17 -0
- vstack/skills/generator.py +20 -0
- vstack/skills/models.py +15 -0
- vstack-0.0.0.dist-info/METADATA +725 -0
- vstack-0.0.0.dist-info/RECORD +119 -0
- vstack-0.0.0.dist-info/WHEEL +4 -0
- vstack-0.0.0.dist-info/entry_points.txt +3 -0
- 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,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
|
vstack/prompts/config.py
ADDED
|
@@ -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)
|
vstack/skills/config.py
ADDED
|
@@ -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)
|