docs(frontmatter): create canonical schema to standardize component metadata

feat(hooks): add schema validator and enhance skill generation
refactor(*): align all components with the new frontmatter schema
refactor(commands): replace arguments object with simple argument-hint
refactor(agents): remove invalid version field from agent frontmatter
test(hooks): add unit tests for frontmatter validation and generation
This commit is contained in:
Fred Amaral 2026-04-06 09:52:28 -07:00
parent 102575dc7c
commit fa6c4c87e8
No known key found for this signature in database
GPG key ID: ADFE56C96F4AC56A
100 changed files with 1810 additions and 1033 deletions

View file

@ -157,6 +157,7 @@ Before adding any content to prompts, skills, agents, or documentation:
| Critical rules | CLAUDE.md |
| Language patterns | docs/PROMPT_ENGINEERING.md |
| Agent schemas | docs/AGENT_DESIGN.md |
| Frontmatter fields | docs/FRONTMATTER_SCHEMA.md |
| Workflows | docs/WORKFLOWS.md |
| Plugin overview | README.md |
| Agent requirements | CLAUDE.md (Agent Modification section) |
@ -201,6 +202,7 @@ When content is reused across multiple skills within a plugin:
| [Agent Output Schemas](#agent-output-schema-archetypes) | Schema summary + [full docs](docs/AGENT_DESIGN.md) |
| [Compliance Rules](#compliance-rules) | TDD, Review, Commit rules |
| [Standards-Agent Synchronization](#5-standards-agent-synchronization-must-check) | Standards ↔ Agent mapping |
| [Frontmatter Schema](docs/FRONTMATTER_SCHEMA.md) | Canonical YAML frontmatter field reference |
| [Documentation Sync](#documentation-sync-checklist) | Files to update |
---
@ -495,7 +497,7 @@ python default/hooks/generate-skills-ref.py # Generate skill overview
| Workflow | Quick Reference |
|----------|-----------------|
| Add skill | `mkdir default/skills/name/` → create `SKILL.md` with frontmatter |
| Add skill | `mkdir default/skills/name/` → create `SKILL.md` with frontmatter per [Frontmatter Schema](docs/FRONTMATTER_SCHEMA.md) |
| Add agent | Create `*/agents/name.md` → verify required sections per [Agent Design](docs/AGENT_DESIGN.md) |
| Modify hooks | Edit `*/hooks/hooks.json` → test with `bash */hooks/session-start.sh` |
| Code review | `/ring:codereview` dispatches 7 parallel reviewers |
@ -513,7 +515,7 @@ See [docs/WORKFLOWS.md](docs/WORKFLOWS.md) for detailed instructions.
### Code Organization
- **Skill Structure**: `default/skills/{name}/SKILL.md` with YAML frontmatter
- **Skill Structure**: `default/skills/{name}/SKILL.md` with YAML frontmatter (see [Frontmatter Schema](docs/FRONTMATTER_SCHEMA.md))
- **Agent Output**: Required markdown sections per `default/agents/*.md:output_schema`
- **Hook Scripts**: Must output JSON with success/error fields
- **Shared Patterns**: Reference via `default/skills/shared-patterns/*.md`
@ -614,6 +616,7 @@ Root Documentation:
Reference Documentation:
├── docs/PROMPT_ENGINEERING.md # Assertive language patterns
├── docs/AGENT_DESIGN.md # Output schemas, standards compliance
├── docs/FRONTMATTER_SCHEMA.md # Canonical YAML frontmatter fields
└── docs/WORKFLOWS.md # Detailed workflow instructions
Plugin Hooks (inject context at session start):

View file

@ -1,6 +1,5 @@
---
name: ring:business-logic-reviewer
version: 6.4.0
description: "Correctness Review: reviews domain correctness, business rules, edge cases, and requirements. Uses mental execution to trace code paths and analyzes full file context, not just changes. Runs in parallel with ring:code-reviewer, ring:security-reviewer, ring:test-reviewer, ring:nil-safety-reviewer, ring:consequences-reviewer, and ring:dead-code-reviewer for fast feedback."
type: reviewer
output_schema:

View file

@ -1,6 +1,5 @@
---
name: ring:code-reviewer
version: 4.4.0
description: "Foundation Review: Reviews code quality, architecture, design patterns, algorithmic flow, and maintainability. Runs in parallel with ring:business-logic-reviewer, ring:security-reviewer, ring:test-reviewer, ring:nil-safety-reviewer, ring:consequences-reviewer, and ring:dead-code-reviewer for fast feedback."
type: reviewer
output_schema:

View file

@ -1,6 +1,5 @@
---
name: ring:codebase-explorer
version: 1.5.0
description: "Deep codebase exploration agent for architecture understanding, pattern discovery, and comprehensive code analysis. Deep codebase exploration agent for thorough analysis."
type: exploration
output_schema:

View file

@ -1,6 +1,5 @@
---
name: ring:consequences-reviewer
version: 1.0.0
description: "Ripple Effect Review: traces how code changes propagate through the codebase beyond the changed files. Walks caller chains, consumer contracts, shared state, and implicit dependencies to find breakage invisible in isolated review. Runs in parallel with ring:code-reviewer, ring:business-logic-reviewer, ring:security-reviewer, ring:test-reviewer, ring:nil-safety-reviewer, and ring:dead-code-reviewer for fast feedback."
type: reviewer
output_schema:

View file

@ -1,6 +1,5 @@
---
name: ring:dead-code-reviewer
version: 1.0.0
description: "Dead Code Review: identifies code that became orphaned, unreachable, or unnecessary as a consequence of changes. Walks the dependency graph outward from the diff to find abandoned helpers, unused types, orphaned modules, and zombie test infrastructure across three concentric rings: target files, first-derivative dependents, and transitive ripple effect. Runs in parallel with ring:code-reviewer, ring:business-logic-reviewer, ring:security-reviewer, ring:test-reviewer, ring:nil-safety-reviewer, and ring:consequences-reviewer for fast feedback."
type: reviewer
output_schema:

View file

@ -1,6 +1,5 @@
---
name: ring:nil-safety-reviewer
version: 1.1.0
description: "Nil/Null Safety Review: Traces nil/null pointer risks from git diff changes through the codebase. Identifies missing guards, unsafe dereferences, panic paths, and API response consistency in Go and TypeScript. Runs in parallel with ring:code-reviewer, ring:business-logic-reviewer, ring:security-reviewer, ring:test-reviewer, ring:consequences-reviewer, and ring:dead-code-reviewer."
type: reviewer
output_schema:

View file

@ -1,6 +1,5 @@
---
name: ring:review-slicer
version: 2.0.0
description: "Review Slicer: Adaptive classification engine that evaluates semantic cohesion to decide whether slicing improves review quality. Sits between Mithril pre-analysis and reviewer dispatch. Classification-only — does NOT read source code."
type: orchestrator
output_schema:

View file

@ -1,6 +1,5 @@
---
name: ring:security-reviewer
version: 4.2.0
description: "Safety Review: Reviews vulnerabilities, authentication, input validation, and OWASP risks. Runs in parallel with ring:code-reviewer, ring:business-logic-reviewer, ring:test-reviewer, ring:nil-safety-reviewer, ring:consequences-reviewer, and ring:dead-code-reviewer for fast feedback."
type: reviewer
output_schema:

View file

@ -1,6 +1,5 @@
---
name: ring:test-reviewer
version: 1.4.0
description: "Test Quality Review: Reviews test coverage, edge cases, test independence, assertion quality, and test anti-patterns across unit, integration, and E2E tests. Runs in parallel with ring:code-reviewer, ring:business-logic-reviewer, ring:security-reviewer, ring:nil-safety-reviewer, ring:consequences-reviewer, and ring:dead-code-reviewer for fast feedback."
type: reviewer
output_schema:

View file

@ -1,6 +1,5 @@
---
name: ring:write-plan
version: 1.1.0
description: "Implementation Planning: Creates comprehensive plans for engineers with zero codebase context. Plans are executable by developers unfamiliar with the codebase, with bite-sized tasks (2-5 min each) and code review checkpoints."
type: planning
output_schema:

View file

@ -1,16 +1,7 @@
---
name: ring:create-handoff
description: Create a handoff document capturing current session state, with automatic context-clear and resume via Plan Mode
user_invocable: true
allowed-tools:
- Skill
arguments:
- name: session-name
description: Short name for the session/feature (e.g., "auth-refactor")
required: false
- name: description
description: Brief description of current work
required: false
argument-hint: "[session-name] [description]"
---
# /ring:create-handoff Command

View file

@ -1,6 +1,7 @@
---
name: ring:release-guide
description: Generate an Ops Update Guide from git diff between two refs
argument-hint: "[base-ref] [target-ref]"
---
Generate an internal Operations-facing update/migration guide based on git diff analysis.

View file

@ -8,6 +8,9 @@ New schema fields:
- description: WHAT the skill does (method/technique)
- trigger: WHEN to use (specific conditions) - primary decision field
- skip_when: WHEN NOT to use (exclusions) - differentiation field
- NOT_skip_when: WHEN to STILL use despite skip_when signals - override field
- prerequisites: What must be true/done before using this skill
- verification: HOW to verify the skill's gate passed (e.g., coverage thresholds, build success)
- sequence.after: Skills that should come before
- sequence.before: Skills that typically follow
- related.similar: Skills that seem similar but differ
@ -22,15 +25,33 @@ from typing import Dict, List, Optional, Any
# Category patterns for grouping skills
CATEGORIES = {
'Pre-Dev Workflow': [r'^pre-dev-'],
'Testing & Debugging': [r'^test-', r'-debugging$', r'^condition-', r'^defense-', r'^root-cause'],
'Collaboration': [r'-review$', r'^dispatching-', r'^sharing-'],
'Planning & Execution': [r'^brainstorming$', r'^writing-plans$', r'^executing-plans$', r'-worktrees$', r'^subagent-driven'],
'Meta Skills': [r'^using-', r'^writing-skills$', r'^testing-skills', r'^testing-agents'],
"Pre-Dev Workflow": [r"^pre-dev-"],
"Testing & Debugging": [
r"^test-",
r"-debugging$",
r"^condition-",
r"^defense-",
r"^root-cause",
],
"Collaboration": [r"-review$", r"^dispatching-", r"^sharing-"],
"Planning & Execution": [
r"^brainstorming$",
r"^writing-plans$",
r"^executing-plans$",
r"-worktrees$",
r"^subagent-driven",
],
"Meta Skills": [
r"^using-",
r"^writing-skills$",
r"^testing-skills",
r"^testing-agents",
],
}
try:
import yaml
YAML_AVAILABLE = True
except ImportError:
YAML_AVAILABLE = False
@ -40,15 +61,27 @@ except ImportError:
class Skill:
"""Represents a skill with its metadata."""
def __init__(self, name: str, description: str, directory: str,
trigger: str = "", skip_when: str = "",
sequence: Optional[Dict[str, List[str]]] = None,
related: Optional[Dict[str, List[str]]] = None):
def __init__(
self,
name: str,
description: str,
directory: str,
trigger: str = "",
skip_when: str = "",
not_skip_when: str = "",
prerequisites: Any = "",
verification: Any = "",
sequence: Optional[Dict[str, List[str]]] = None,
related: Optional[Dict[str, List[str]]] = None,
):
self.name = name
self.description = description
self.directory = directory
self.trigger = trigger
self.skip_when = skip_when
self.trigger = trigger or ""
self.skip_when = skip_when or ""
self.not_skip_when = not_skip_when or ""
self.prerequisites = prerequisites if prerequisites is not None else ""
self.verification = verification if verification is not None else ""
self.sequence = sequence or {}
self.related = related or {}
self.category = self._categorize()
@ -59,7 +92,7 @@ class Skill:
for pattern in patterns:
if re.search(pattern, self.directory):
return category
return 'Other'
return "Other"
def __repr__(self):
return f"Skill(name={self.name}, category={self.category})"
@ -70,13 +103,13 @@ def first_line(text: str) -> str:
if not text:
return ""
# Remove leading/trailing whitespace, take first line
lines = text.strip().split('\n')
lines = text.strip().split("\n")
for line in lines:
line = line.strip()
# Skip list markers and empty lines
if line and not line.startswith('-'):
if line and not line.startswith("-"):
return line
elif line.startswith('- '):
elif line.startswith("- "):
return line[2:] # Return first list item without marker
return lines[0].strip() if lines else ""
@ -87,7 +120,7 @@ def parse_frontmatter_yaml(content: str) -> Optional[Dict[str, Any]]:
return None
# Extract frontmatter between --- delimiters
match = re.match(r'^---\s*\n(.*?)\n---\s*\n', content, re.DOTALL)
match = re.match(r"^---\s*\n(.*?)\n---\s*\n", content, re.DOTALL)
if not match:
return None
@ -103,49 +136,66 @@ def parse_frontmatter_fallback(content: str) -> Optional[Dict[str, Any]]:
"""Fallback parser using regex when pyyaml unavailable.
Handles:
- Simple scalar fields: name, description, trigger, skip_when, when_to_use
- Simple scalar fields: name, description, trigger, skip_when, NOT_skip_when, when_to_use, prerequisites, verification
- Multi-line block scalars (|) - extracts first meaningful line
- Nested structures: sequence, related - parses sub-fields with arrays
"""
match = re.match(r'^---\s*\n(.*?)\n---\s*\n', content, re.DOTALL)
match = re.match(r"^---\s*\n(.*?)\n---\s*\n", content, re.DOTALL)
if not match:
return None
frontmatter_text = match.group(1)
# Size guard: prevent pathological regex backtracking on oversized frontmatter
if len(frontmatter_text) > 10000:
print(
"Warning: Oversized frontmatter, skipping fallback parse", file=sys.stderr
)
return None
result = {}
# Extract simple/block scalar fields
# Known top-level field names (prevents false matches on "error:" etc in values)
simple_fields = ['name', 'description', 'trigger', 'skip_when', 'when_to_use']
all_fields = simple_fields + ['sequence', 'related']
fields_pattern = '|'.join(all_fields)
simple_fields = [
"name",
"description",
"trigger",
"skip_when",
"NOT_skip_when",
"when_to_use",
"prerequisites",
"verification",
]
all_fields = simple_fields + ["sequence", "related"]
fields_pattern = "|".join(all_fields)
for field in simple_fields:
# Match field: value OR field: | followed by indented content
# Capture until next known top-level field or end of frontmatter
# Using explicit field list prevents matching "error:" inside values
pattern = rf'^{field}:\s*\|?\s*\n?(.*?)(?=^(?:{fields_pattern}):|\Z)'
pattern = rf"^{field}:\s*\|?\s*\n?(.*?)(?=^(?:{fields_pattern}):|\Z)"
field_match = re.search(pattern, frontmatter_text, re.MULTILINE | re.DOTALL)
if field_match:
raw_value = field_match.group(1).strip()
if raw_value:
# Extract lines, clean indentation
lines = []
for line in raw_value.split('\n'):
for line in raw_value.split("\n"):
cleaned = line.strip()
# Remove list marker prefix for cleaner display
if cleaned.startswith('- '):
if cleaned.startswith("- "):
cleaned = cleaned[2:]
if cleaned and not cleaned.startswith('#'):
if cleaned and not cleaned.startswith("#"):
lines.append(cleaned)
if lines:
# For quick reference, use first meaningful line
result[field] = lines[0]
# Handle nested structures: sequence and related
for nested_field in ['sequence', 'related']:
for nested_field in ["sequence", "related"]:
# Match the nested block (indented content under field:)
pattern = rf'^{nested_field}:\s*\n((?:[ \t]+[^\n]*\n?)+)'
pattern = rf"^{nested_field}:\s*\n((?:[ \t]+[^\n]*\n?)+)"
nested_match = re.search(pattern, frontmatter_text, re.MULTILINE)
if nested_match:
nested_text = nested_match.group(1)
@ -153,15 +203,15 @@ def parse_frontmatter_fallback(content: str) -> Optional[Dict[str, Any]]:
# Parse sub-fields: after, before, similar, complementary
# Format: subfield: [item1, item2] or subfield: [item1]
subfields = ['after', 'before', 'similar', 'complementary']
subfields = ["after", "before", "similar", "complementary"]
for subfield in subfields:
# Match: subfield: [contents]
sub_pattern = rf'^\s*{subfield}:\s*\[([^\]]*)\]'
sub_pattern = rf"^\s*{subfield}:\s*\[([^\]]*)\]"
sub_match = re.search(sub_pattern, nested_text, re.MULTILINE)
if sub_match:
items_str = sub_match.group(1)
# Parse comma-separated items, strip whitespace
items = [s.strip() for s in items_str.split(',') if s.strip()]
items = [s.strip() for s in items_str.split(",") if s.strip()]
if items:
result[nested_field][subfield] = items
@ -175,7 +225,7 @@ def parse_frontmatter_fallback(content: str) -> Optional[Dict[str, Any]]:
def parse_skill_file(skill_path: Path) -> Optional[Skill]:
"""Parse a SKILL.md file and extract metadata."""
try:
with open(skill_path, 'r', encoding='utf-8') as f:
with open(skill_path, "r", encoding="utf-8") as f:
content = f.read()
# Try YAML parser first, fall back to regex
@ -183,30 +233,35 @@ def parse_skill_file(skill_path: Path) -> Optional[Skill]:
if not frontmatter:
frontmatter = parse_frontmatter_fallback(content)
if not frontmatter or 'name' not in frontmatter:
if not frontmatter or "name" not in frontmatter:
print(f"Warning: Missing name in {skill_path}", file=sys.stderr)
return None
# Handle backward compatibility: use when_to_use as trigger if trigger not set
trigger = frontmatter.get('trigger', '')
trigger = frontmatter.get("trigger", "")
if not trigger:
trigger = frontmatter.get('when_to_use', '')
trigger = frontmatter.get("when_to_use", "")
if not trigger:
# Fall back to description for old-style skills
trigger = frontmatter.get('description', '')
trigger = frontmatter.get("description", "")
# Get description - prefer dedicated description field
description = frontmatter.get('description', '')
description = frontmatter.get("description", "")
directory = skill_path.parent.name
return Skill(
name=frontmatter['name'],
name=frontmatter["name"],
description=description,
directory=directory,
trigger=trigger,
skip_when=frontmatter.get('skip_when', ''),
sequence=frontmatter.get('sequence', {}),
related=frontmatter.get('related', {})
skip_when=frontmatter.get("skip_when") or "",
not_skip_when=frontmatter.get("NOT_skip_when") or "",
prerequisites=frontmatter.get("prerequisites")
or frontmatter.get("prerequisite")
or "",
verification=frontmatter.get("verification") or "",
sequence=frontmatter.get("sequence") or {},
related=frontmatter.get("related") or {},
)
except Exception as e:
@ -226,7 +281,7 @@ def scan_skills_directory(skills_dir: Path) -> List[Skill]:
if not skill_dir.is_dir():
continue
skill_file = skill_dir / 'SKILL.md'
skill_file = skill_dir / "SKILL.md"
if not skill_file.exists():
print(f"Warning: No SKILL.md in {skill_dir.name}", file=sys.stderr)
continue
@ -238,6 +293,60 @@ def scan_skills_directory(skills_dir: Path) -> List[Skill]:
return skills
def _safe_display_text(value: Any) -> str:
"""Extract a single display line from a value that may be str, dict, list, or None."""
if value is None:
return ""
if isinstance(value, str):
return first_line(value)
if isinstance(value, list):
items = [
item.get("name", str(item)) if isinstance(item, dict) else str(item)
for item in value
if item is not None
]
return ", ".join(items) if items else ""
# dict or other types — not suitable for one-line display
return ""
def _format_prerequisites(value: Any) -> str:
"""Format prerequisites which may be a string, list of dicts, or list of strings."""
if value is None:
return ""
if isinstance(value, list):
names = [
item.get("name", str(item)) if isinstance(item, dict) else str(item)
for item in value
if item is not None
]
return ", ".join(names) if names else ""
if isinstance(value, str):
return first_line(value)
return ""
def _format_verification(value: Any) -> str:
"""Format verification which may be a string, nested dict, or None."""
if value is None:
return ""
if isinstance(value, str):
return first_line(value)
if isinstance(value, dict):
# Extract first automated command description for display
automated = value.get("automated", [])
if automated and isinstance(automated, list):
first = automated[0]
if isinstance(first, dict):
return first.get("description", first.get("command", ""))
manual = value.get("manual", [])
if manual and isinstance(manual, list):
first_manual = manual[0]
return str(first_manual) if first_manual else ""
return ""
return ""
def generate_markdown(skills: List[Skill]) -> str:
"""Generate markdown quick reference from skills list.
@ -258,28 +367,41 @@ def generate_markdown(skills: List[Skill]) -> str:
categorized[category].append(skill)
# Sort categories (predefined order, then Other)
category_order = list(CATEGORIES.keys()) + ['Other']
category_order = list(CATEGORIES.keys()) + ["Other"]
sorted_categories = [cat for cat in category_order if cat in categorized]
# Build markdown
lines = ['# Ring Skills Quick Reference\n']
lines = ["# Ring Skills Quick Reference\n"]
for category in sorted_categories:
category_skills = categorized[category]
lines.append(f'## {category} ({len(category_skills)} skills)\n')
lines.append(f"## {category} ({len(category_skills)} skills)\n")
for skill in sorted(category_skills, key=lambda s: s.name):
# Skill name and description
lines.append(f'- **{skill.name}**: {first_line(skill.description)}')
lines.append(f"- **{skill.name}**: {first_line(skill.description)}")
# Optional decision fields (only shown when present)
skip_text = _safe_display_text(skill.skip_when)
not_skip_text = _safe_display_text(skill.not_skip_when)
prereq_text = _format_prerequisites(skill.prerequisites)
verification_text = _format_verification(skill.verification)
if skip_text:
lines.append(f" - Skip when: {skip_text}")
if not_skip_text:
lines.append(f" - NOT skip when: {not_skip_text}")
if prereq_text:
lines.append(f" - Prerequisites: {prereq_text}")
if verification_text:
lines.append(f" - Verification: {verification_text}")
lines.append('') # Blank line between categories
lines.append("") # Blank line between categories
# Add usage section
lines.append('## Usage\n')
lines.append('To use a skill: Use the Skill tool with skill name')
lines.append('Example: `ring:brainstorming`')
lines.append("## Usage\n")
lines.append("To use a skill: Use the Skill tool with skill name")
lines.append("Example: `ring:brainstorming`")
return '\n'.join(lines)
return "\n".join(lines)
def main():
@ -287,7 +409,7 @@ def main():
# Determine plugin root (parent of hooks directory)
script_dir = Path(__file__).parent.resolve()
plugin_root = script_dir.parent
skills_dir = plugin_root / 'skills'
skills_dir = plugin_root / "skills"
# Scan and parse skills
skills = scan_skills_directory(skills_dir)
@ -304,5 +426,5 @@ def main():
print(f"Generated reference for {len(skills)} skills", file=sys.stderr)
if __name__ == '__main__':
if __name__ == "__main__":
main()

View file

View file

@ -0,0 +1,336 @@
#!/usr/bin/env python3
"""Tests for generate-skills-ref.py frontmatter parsing and markdown generation."""
import sys
from pathlib import Path
# Add parent directory to path so we can import the module
sys.path.insert(0, str(Path(__file__).parent.parent))
# We need to import the module by its filename (contains hyphens in concept but not in actual name)
import importlib.util
spec = importlib.util.spec_from_file_location(
"generate_skills_ref",
Path(__file__).parent.parent / "generate-skills-ref.py",
)
mod = importlib.util.module_from_spec(spec)
spec.loader.exec_module(mod)
Skill = mod.Skill
first_line = mod.first_line
parse_frontmatter_yaml = mod.parse_frontmatter_yaml
parse_frontmatter_fallback = mod.parse_frontmatter_fallback
parse_skill_file = mod.parse_skill_file
generate_markdown = mod.generate_markdown
_safe_display_text = mod._safe_display_text
_format_prerequisites = mod._format_prerequisites
_format_verification = mod._format_verification
# ---------------------------------------------------------------------------
# first_line()
# ---------------------------------------------------------------------------
class TestFirstLine:
def test_empty_string(self):
assert first_line("") == ""
def test_none_input(self):
assert first_line(None) == ""
def test_single_line(self):
assert first_line("hello world") == "hello world"
def test_multiline_takes_first(self):
assert first_line("first\nsecond\nthird") == "first"
def test_list_item_strips_marker(self):
assert first_line("- first item\n- second") == "first item"
def test_whitespace_stripped(self):
assert first_line(" spaced \n text ") == "spaced"
# ---------------------------------------------------------------------------
# _safe_display_text()
# ---------------------------------------------------------------------------
class TestSafeDisplayText:
def test_none_returns_empty(self):
assert _safe_display_text(None) == ""
def test_string_returns_first_line(self):
assert _safe_display_text("hello\nworld") == "hello"
def test_empty_string_returns_empty(self):
assert _safe_display_text("") == ""
def test_dict_returns_empty(self):
"""Dicts are not suitable for one-line display."""
assert _safe_display_text({"key": "value"}) == ""
def test_list_joins_names(self):
assert _safe_display_text(["a", "b"]) == "a, b"
def test_list_of_dicts_extracts_names(self):
result = _safe_display_text([{"name": "pkg_a"}, {"name": "pkg_b"}])
assert result == "pkg_a, pkg_b"
def test_list_with_none_items_filtered(self):
assert _safe_display_text(["a", None, "b"]) == "a, b"
def test_empty_list_returns_empty(self):
assert _safe_display_text([]) == ""
# ---------------------------------------------------------------------------
# _format_prerequisites()
# ---------------------------------------------------------------------------
class TestFormatPrerequisites:
def test_none_returns_empty(self):
assert _format_prerequisites(None) == ""
def test_string(self):
assert _format_prerequisites("framework installed") == "framework installed"
def test_list_of_strings(self):
assert _format_prerequisites(["a", "b"]) == "a, b"
def test_list_of_dicts_with_name(self):
result = _format_prerequisites([{"name": "pkg_a"}, {"name": "pkg_b"}])
assert result == "pkg_a, pkg_b"
def test_empty_list(self):
assert _format_prerequisites([]) == ""
def test_list_with_none_items(self):
assert _format_prerequisites(["a", None, "b"]) == "a, b"
def test_mixed_list(self):
result = _format_prerequisites([{"name": "a"}, "b"])
assert result == "a, b"
def test_dict_without_name_falls_back_to_str(self):
result = _format_prerequisites([{"other": "x"}])
assert "other" in result # str(dict) representation
# ---------------------------------------------------------------------------
# _format_verification()
# ---------------------------------------------------------------------------
class TestFormatVerification:
def test_none_returns_empty(self):
assert _format_verification(None) == ""
def test_string(self):
assert _format_verification("all tests pass") == "all tests pass"
def test_empty_string(self):
assert _format_verification("") == ""
def test_nested_dict_with_automated(self):
val = {
"automated": [{"command": "go test ./...", "description": "Go tests pass"}],
"manual": None,
}
result = _format_verification(val)
assert result == "Go tests pass"
def test_nested_dict_automated_without_description(self):
val = {"automated": [{"command": "go test ./..."}]}
result = _format_verification(val)
assert result == "go test ./..."
def test_nested_dict_manual_only(self):
val = {"automated": [], "manual": ["Check logs"]}
result = _format_verification(val)
assert result == "Check logs"
def test_empty_dict(self):
assert _format_verification({}) == ""
def test_dict_with_none_manual(self):
val = {"automated": [], "manual": None}
assert _format_verification(val) == ""
def test_does_not_produce_raw_dict_string(self):
"""Critical: must never produce {'automated': ...} in output."""
val = {"automated": [{"command": "test"}], "manual": None}
result = _format_verification(val)
assert "{'automated'" not in result
assert "{'" not in result
# ---------------------------------------------------------------------------
# Skill constructor — None coalescing
# ---------------------------------------------------------------------------
class TestSkillConstructor:
def test_none_skip_when_coalesced(self):
s = Skill(name="test", description="d", directory="d", skip_when=None)
assert s.skip_when == ""
def test_none_not_skip_when_coalesced(self):
s = Skill(name="test", description="d", directory="d", not_skip_when=None)
assert s.not_skip_when == ""
def test_none_prerequisites_coalesced(self):
s = Skill(name="test", description="d", directory="d", prerequisites=None)
assert s.prerequisites == ""
def test_none_verification_coalesced(self):
s = Skill(name="test", description="d", directory="d", verification=None)
assert s.verification == ""
def test_dict_prerequisites_preserved(self):
"""Dicts should be preserved (not coalesced to '') for _format_prerequisites."""
val = [{"name": "a"}]
s = Skill(name="test", description="d", directory="d", prerequisites=val)
assert s.prerequisites == val
def test_dict_verification_preserved(self):
"""Dicts should be preserved for _format_verification."""
val = {"automated": [{"command": "test"}]}
s = Skill(name="test", description="d", directory="d", verification=val)
assert s.verification == val
# ---------------------------------------------------------------------------
# parse_frontmatter_yaml()
# ---------------------------------------------------------------------------
class TestParseFrontmatterYaml:
def test_valid_frontmatter(self):
content = "---\nname: test\ndescription: desc\n---\n# Body\n"
result = parse_frontmatter_yaml(content)
if result is not None: # Only if pyyaml available
assert result["name"] == "test"
def test_no_frontmatter(self):
assert parse_frontmatter_yaml("# Just markdown") is None
def test_empty_yaml_value_returns_none_key(self):
"""YAML empty value produces None — verify we handle this."""
content = "---\nname: test\nverification:\n---\n# Body\n"
result = parse_frontmatter_yaml(content)
if result is not None:
assert "verification" in result
assert result["verification"] is None # This is the dict.get trap
# ---------------------------------------------------------------------------
# parse_frontmatter_fallback()
# ---------------------------------------------------------------------------
class TestParseFrontmatterFallback:
def test_valid_frontmatter(self):
content = "---\nname: test\ndescription: A skill\n---\n# Body\n"
result = parse_frontmatter_fallback(content)
assert result is not None
assert result["name"] == "test"
def test_no_frontmatter(self):
assert parse_frontmatter_fallback("# Just markdown") is None
def test_includes_new_fields(self):
content = (
"---\n"
"name: test\n"
"NOT_skip_when: override\n"
"prerequisites: some prereq\n"
"verification: all pass\n"
"---\n"
)
result = parse_frontmatter_fallback(content)
assert result is not None
assert "NOT_skip_when" in result
assert "prerequisites" in result
assert "verification" in result
def test_oversized_frontmatter_rejected(self):
"""Size guard: frontmatter > 10KB should return None."""
huge = "---\nname: test\ndescription: " + "x" * 11000 + "\n---\n"
result = parse_frontmatter_fallback(huge)
assert result is None
# ---------------------------------------------------------------------------
# generate_markdown() — integration
# ---------------------------------------------------------------------------
class TestGenerateMarkdown:
def test_empty_skills_list(self):
result = generate_markdown([])
assert "No skills found" in result
def test_single_skill_basic(self):
s = Skill(name="ring:test", description="Test skill", directory="test")
result = generate_markdown([s])
assert "ring:test" in result
assert "Test skill" in result
def test_skip_when_rendered(self):
s = Skill(
name="ring:test",
description="d",
directory="test",
skip_when="No code changes",
)
result = generate_markdown([s])
assert "Skip when: No code changes" in result
def test_not_skip_when_rendered(self):
s = Skill(
name="ring:test",
description="d",
directory="test",
not_skip_when="Code is simple still needs review",
)
result = generate_markdown([s])
assert "NOT skip when:" in result
def test_none_values_not_rendered(self):
"""None values should not appear as 'None' string in output."""
s = Skill(
name="ring:test",
description="d",
directory="test",
skip_when=None,
not_skip_when=None,
prerequisites=None,
verification=None,
)
result = generate_markdown([s])
assert "None" not in result
def test_dict_verification_not_raw(self):
"""Dict verification should not appear as raw Python repr."""
s = Skill(
name="ring:test",
description="d",
directory="test",
verification={"automated": [{"command": "test"}]},
)
result = generate_markdown([s])
assert "{'automated'" not in result
def test_prerequisites_list_rendered(self):
s = Skill(
name="ring:test",
description="d",
directory="test",
prerequisites=[{"name": "a"}, {"name": "b"}],
)
result = generate_markdown([s])
assert "Prerequisites: a, b" in result

View file

@ -0,0 +1,245 @@
#!/usr/bin/env python3
"""Tests for validate-frontmatter.py schema validation logic."""
import sys
from pathlib import Path
# Import the module
import importlib.util
spec = importlib.util.spec_from_file_location(
"validate_frontmatter",
Path(__file__).parent.parent / "validate-frontmatter.py",
)
mod = importlib.util.module_from_spec(spec)
spec.loader.exec_module(mod)
validate_skill = mod.validate_skill
validate_command = mod.validate_command
validate_agent = mod.validate_agent
parse_frontmatter = mod.parse_frontmatter
Issue = mod.Issue
# ---------------------------------------------------------------------------
# validate_skill()
# ---------------------------------------------------------------------------
class TestValidateSkill:
def test_valid_skill_no_issues(self):
fm = {
"name": "ring:test",
"description": "A test skill",
"trigger": "when needed",
"skip_when": "not needed",
}
issues = validate_skill("test.md", fm)
assert all(i.level != "ERROR" for i in issues)
def test_missing_required_name(self):
fm = {"description": "A test skill"}
issues = validate_skill("test.md", fm)
assert any(i.level == "ERROR" and "name" in i.message for i in issues)
def test_missing_required_description(self):
fm = {"name": "ring:test"}
issues = validate_skill("test.md", fm)
assert any(i.level == "ERROR" and "description" in i.message for i in issues)
def test_missing_recommended_trigger(self):
fm = {"name": "ring:test", "description": "d"}
issues = validate_skill("test.md", fm)
assert any(i.level == "WARNING" and "trigger" in i.message for i in issues)
def test_deprecated_when_to_use(self):
fm = {"name": "ring:test", "description": "d", "when_to_use": "old"}
issues = validate_skill("test.md", fm)
assert any(
"deprecated" in i.message and "when_to_use" in i.message for i in issues
)
def test_invalid_field_version(self):
fm = {"name": "ring:test", "description": "d", "version": "1.0.0"}
issues = validate_skill("test.md", fm)
assert any(
"invalid" in i.message.lower() and "version" in i.message for i in issues
)
def test_invalid_field_examples(self):
fm = {"name": "ring:test", "description": "d", "examples": []}
issues = validate_skill("test.md", fm)
assert any(
"invalid" in i.message.lower() and "examples" in i.message for i in issues
)
def test_valid_optional_fields(self):
fm = {
"name": "ring:test",
"description": "d",
"trigger": "t",
"skip_when": "s",
"NOT_skip_when": "n",
"prerequisites": "p",
"verification": "v",
"sequence": {},
"related": {},
}
issues = validate_skill("test.md", fm)
# No errors, possibly warnings for recommended fields
assert all(i.level != "ERROR" for i in issues)
# ---------------------------------------------------------------------------
# validate_command()
# ---------------------------------------------------------------------------
class TestValidateCommand:
def test_valid_command_no_errors(self):
fm = {
"name": "test-cmd",
"description": "A command",
"argument-hint": "[target]",
}
issues = validate_command("test.md", fm)
assert all(i.level != "ERROR" for i in issues)
def test_missing_required_name(self):
fm = {"description": "A command"}
issues = validate_command("test.md", fm)
assert any(i.level == "ERROR" and "name" in i.message for i in issues)
def test_deprecated_arguments(self):
fm = {"name": "test", "description": "d", "arguments": []}
issues = validate_command("test.md", fm)
assert any("argument-hint" in i.message for i in issues)
def test_deprecated_args(self):
fm = {"name": "test", "description": "d", "args": []}
issues = validate_command("test.md", fm)
assert any("argument-hint" in i.message for i in issues)
# ---------------------------------------------------------------------------
# validate_agent()
# ---------------------------------------------------------------------------
class TestValidateAgent:
def test_valid_agent_no_errors(self):
fm = {
"name": "ring:test",
"description": "An agent",
"type": "specialist",
"output_schema": {"format": "markdown"},
}
issues = validate_agent("test.md", fm)
assert all(i.level != "ERROR" for i in issues)
def test_missing_required_type(self):
fm = {"name": "ring:test", "description": "d", "output_schema": {}}
issues = validate_agent("test.md", fm)
assert any(i.level == "ERROR" and "type" in i.message for i in issues)
def test_invalid_type_enum(self):
fm = {
"name": "ring:test",
"description": "d",
"type": "invalid_type",
"output_schema": {},
}
issues = validate_agent("test.md", fm)
assert any("not in allowed values" in i.message for i in issues)
def test_valid_type_enums(self):
for agent_type in [
"reviewer",
"specialist",
"orchestrator",
"planning",
"exploration",
"analyst",
"calculator",
]:
fm = {
"name": "t",
"description": "d",
"type": agent_type,
"output_schema": {},
}
issues = validate_agent("test.md", fm)
assert not any("not in allowed values" in i.message for i in issues), (
f"Type '{agent_type}' should be valid"
)
def test_invalid_field_version(self):
fm = {
"name": "t",
"description": "d",
"type": "specialist",
"output_schema": {},
"version": "1.0",
}
issues = validate_agent("test.md", fm)
assert any(
"invalid" in i.message.lower() and "version" in i.message for i in issues
)
def test_invalid_field_tools(self):
fm = {
"name": "t",
"description": "d",
"type": "specialist",
"output_schema": {},
"tools": ["Bash"],
}
issues = validate_agent("test.md", fm)
assert any(
"invalid" in i.message.lower() and "tools" in i.message for i in issues
)
# ---------------------------------------------------------------------------
# parse_frontmatter()
# ---------------------------------------------------------------------------
class TestParseFrontmatter:
def test_valid_yaml(self):
content = "---\nname: test\ndescription: desc\n---\n# Body\n"
result = parse_frontmatter(content)
assert result is not None
assert result["name"] == "test"
def test_no_frontmatter(self):
assert parse_frontmatter("# Just markdown") is None
def test_empty_content(self):
assert parse_frontmatter("") is None
# ---------------------------------------------------------------------------
# main() — CLI
# ---------------------------------------------------------------------------
class TestMainCLI:
def test_unknown_plugin_returns_error(self):
"""--plugin with invalid name should return exit code 1."""
original_argv = sys.argv
try:
sys.argv = ["validate-frontmatter.py", "--plugin", "nonexistent"]
result = mod.main()
assert result == 1
finally:
sys.argv = original_argv
def test_strict_mode_type(self):
"""Verify strict mode is an argparse flag (not a positional)."""
import argparse
parser = argparse.ArgumentParser()
parser.add_argument("--strict", action="store_true")
args = parser.parse_args(["--strict"])
assert args.strict is True

View file

@ -0,0 +1,483 @@
#!/usr/bin/env python3
"""
Validate YAML frontmatter in Ring skill, command, and agent files against
the canonical schema defined in docs/FRONTMATTER_SCHEMA.md.
Scans all 6 plugins (default/, dev-team/, pm-team/, pmo-team/, finops-team/,
tw-team/) and reports errors and warnings.
Usage:
python default/hooks/validate-frontmatter.py
python default/hooks/validate-frontmatter.py --strict
python default/hooks/validate-frontmatter.py --plugin dev-team
"""
import argparse
import re
import sys
from pathlib import Path
from typing import Any, Dict, List, Optional, Tuple
try:
import yaml
YAML_AVAILABLE = True
except ImportError:
YAML_AVAILABLE = False
# ---------------------------------------------------------------------------
# Schema definitions (derived from docs/FRONTMATTER_SCHEMA.md)
# ---------------------------------------------------------------------------
# -- Skills --
SKILL_REQUIRED = {"name", "description"}
SKILL_RECOMMENDED = {"trigger", "skip_when"}
SKILL_VALID = (
SKILL_REQUIRED
| SKILL_RECOMMENDED
| {
"NOT_skip_when",
"prerequisites",
"verification",
"when_to_use", # deprecated but still valid
"prerequisite", # deprecated but still valid
"sequence",
"related",
"compliance_rules",
"composition",
"input_schema",
"output_schema",
}
)
SKILL_DEPRECATED = {
"when_to_use": "trigger",
"prerequisite": "prerequisites",
}
SKILL_INVALID = {
"version", "allowed-tools", "examples", "category", "tier", "slug",
"user_invocable", "title", "type", "role", "dependencies", "author",
"license", "compatibility", "metadata", "agent_selection", "tdd_policy",
"research_modes", "trigger_when",
}
# -- Commands --
COMMAND_REQUIRED = {"name", "description"}
COMMAND_RECOMMENDED = {"argument-hint"}
COMMAND_VALID = COMMAND_REQUIRED | COMMAND_RECOMMENDED
COMMAND_DEPRECATED: Dict[str, str] = {
"arguments": "argument-hint",
"args": "argument-hint",
}
COMMAND_INVALID = {"arguments", "args", "version"}
# -- Agents --
AGENT_REQUIRED = {"name", "description", "type", "output_schema"}
AGENT_TYPE_ENUM = {
"reviewer",
"specialist",
"orchestrator",
"planning",
"exploration",
"analyst",
"calculator",
}
AGENT_VALID = AGENT_REQUIRED | {"input_schema"}
AGENT_INVALID = {"version", "color", "project_rules_integration", "allowed-tools", "tools"}
# -- Plugin directories --
ALL_PLUGINS = ["default", "dev-team", "pm-team", "pmo-team", "finops-team", "tw-team"]
# ---------------------------------------------------------------------------
# Frontmatter parsing
# ---------------------------------------------------------------------------
def parse_frontmatter_yaml(content: str) -> Optional[Dict[str, Any]]:
"""Parse YAML frontmatter using the pyyaml library."""
if not YAML_AVAILABLE:
return None
match = re.match(r"^---\s*\n(.*?)\n---\s*\n", content, re.DOTALL)
if not match:
return None
try:
data = yaml.safe_load(match.group(1))
return data if isinstance(data, dict) else None
except yaml.YAMLError:
return None
def parse_frontmatter_fallback(content: str) -> Optional[Dict[str, Any]]:
"""Regex-based fallback parser (mirrors generate-skills-ref.py approach).
Extracts top-level keys and their scalar values. Nested objects are
represented as non-empty dicts so presence checks work, but deep
validation is not attempted in fallback mode.
"""
match = re.match(r"^---\s*\n(.*?)\n---\s*\n", content, re.DOTALL)
if not match:
return None
text = match.group(1)
result: Dict[str, Any] = {}
# Identify all top-level keys (lines starting at column 0 with "key:")
top_keys = re.findall(r"^([A-Za-z_][A-Za-z0-9_-]*):", text, re.MULTILINE)
for key in top_keys:
# Grab everything from "key:" to the next top-level key or end
pat = rf"^{re.escape(key)}:\s*(.*?)(?=^[A-Za-z_][A-Za-z0-9_-]*:|\Z)"
m = re.search(pat, text, re.MULTILINE | re.DOTALL)
if m:
raw = m.group(1).strip()
if raw.startswith("|") or raw.startswith(">"):
# Block scalar -- grab indented lines that follow
block_lines = []
for line in raw.split("\n")[1:]:
if line and not line[0].isspace():
break
block_lines.append(line.strip())
result[key] = "\n".join(block_lines).strip() or raw
elif raw == "" or raw.startswith("\n"):
# Nested mapping or list -- mark as present with a placeholder
result[key] = {"_nested": True}
else:
# Simple scalar (possibly quoted)
first_line = raw.split("\n")[0].strip()
if first_line.startswith('"') and first_line.endswith('"'):
first_line = first_line[1:-1]
elif first_line.startswith("'") and first_line.endswith("'"):
first_line = first_line[1:-1]
result[key] = first_line
return result if result else None
def parse_frontmatter(content: str) -> Optional[Dict[str, Any]]:
"""Try YAML first, then regex fallback."""
data = parse_frontmatter_yaml(content)
if data is not None:
return data
return parse_frontmatter_fallback(content)
# ---------------------------------------------------------------------------
# Validation logic
# ---------------------------------------------------------------------------
class Issue:
"""A single validation issue."""
def __init__(self, level: str, path: str, message: str):
self.level = level # "ERROR" or "WARNING"
self.path = path
self.message = message
def __str__(self) -> str:
return f"[{self.level}] {self.path}: {self.message}"
def validate_skill(file_path: str, fm: Dict[str, Any]) -> List[Issue]:
"""Validate a skill frontmatter dict."""
issues: List[Issue] = []
# Required fields
for field in sorted(SKILL_REQUIRED):
if field not in fm:
issues.append(Issue("ERROR", file_path, f"missing required field '{field}'"))
# Recommended fields
for field in sorted(SKILL_RECOMMENDED):
if field not in fm:
issues.append(Issue("WARNING", file_path, f"missing recommended field '{field}'"))
# Deprecated fields
for old_field, new_field in sorted(SKILL_DEPRECATED.items()):
if old_field in fm:
issues.append(
Issue(
"WARNING",
file_path,
f"deprecated field '{old_field}' -- use '{new_field}' instead",
)
)
# Unknown / explicitly invalid fields
for field in sorted(fm.keys()):
if field in SKILL_INVALID:
issues.append(
Issue("WARNING", file_path, f"invalid field '{field}' (not part of the schema)")
)
elif field not in SKILL_VALID:
issues.append(
Issue("WARNING", file_path, f"unknown field '{field}'")
)
return issues
def validate_command(file_path: str, fm: Dict[str, Any]) -> List[Issue]:
"""Validate a command frontmatter dict."""
issues: List[Issue] = []
# Required fields
for field in sorted(COMMAND_REQUIRED):
if field not in fm:
issues.append(Issue("ERROR", file_path, f"missing required field '{field}'"))
# Recommended fields
for field in sorted(COMMAND_RECOMMENDED):
if field not in fm:
issues.append(Issue("WARNING", file_path, f"missing recommended field '{field}'"))
# Deprecated / invalid fields
for old_field, new_field in sorted(COMMAND_DEPRECATED.items()):
if old_field in fm:
issues.append(
Issue(
"WARNING",
file_path,
f"invalid field '{old_field}' -- use '{new_field}' instead",
)
)
# Unknown fields
for field in sorted(fm.keys()):
if field in COMMAND_INVALID:
# Already covered by deprecated check above for args/arguments
if field not in COMMAND_DEPRECATED:
issues.append(
Issue("WARNING", file_path, f"invalid field '{field}' (not part of the schema)")
)
elif field not in COMMAND_VALID:
issues.append(
Issue("WARNING", file_path, f"unknown field '{field}'")
)
return issues
def validate_agent(file_path: str, fm: Dict[str, Any]) -> List[Issue]:
"""Validate an agent frontmatter dict."""
issues: List[Issue] = []
# Required fields
for field in sorted(AGENT_REQUIRED):
if field not in fm:
issues.append(Issue("ERROR", file_path, f"missing required field '{field}'"))
# Type enum check
agent_type = fm.get("type")
if agent_type is not None and agent_type not in AGENT_TYPE_ENUM:
issues.append(
Issue(
"WARNING",
file_path,
f"type '{agent_type}' not in allowed values: {sorted(AGENT_TYPE_ENUM)}",
)
)
# Explicitly invalid fields
for field in sorted(fm.keys()):
if field in AGENT_INVALID:
issues.append(
Issue("WARNING", file_path, f"invalid field '{field}' (not part of the schema)")
)
elif field not in AGENT_VALID:
issues.append(
Issue("WARNING", file_path, f"unknown field '{field}'")
)
return issues
# ---------------------------------------------------------------------------
# File discovery
# ---------------------------------------------------------------------------
def discover_files(repo_root: Path, plugins: List[str]) -> Tuple[List[Path], List[Path], List[Path]]:
"""Discover skill, command, and agent files across the requested plugins.
Returns (skills, commands, agents) as three sorted lists of Paths.
Skips shared-patterns/ directories.
"""
skills: List[Path] = []
commands: List[Path] = []
agents: List[Path] = []
for plugin in plugins:
plugin_dir = repo_root / plugin
# Skills: {plugin}/skills/*/SKILL.md (skip shared-patterns)
skills_dir = plugin_dir / "skills"
if skills_dir.is_dir():
for child in sorted(skills_dir.iterdir()):
if not child.is_dir():
continue
if child.name == "shared-patterns":
continue
skill_file = child / "SKILL.md"
if skill_file.is_file():
skills.append(skill_file)
# Commands: {plugin}/commands/*.md
commands_dir = plugin_dir / "commands"
if commands_dir.is_dir():
for child in sorted(commands_dir.iterdir()):
if child.is_file() and child.suffix == ".md":
commands.append(child)
# Agents: {plugin}/agents/*.md
agents_dir = plugin_dir / "agents"
if agents_dir.is_dir():
for child in sorted(agents_dir.iterdir()):
if child.is_file() and child.suffix == ".md":
agents.append(child)
return skills, commands, agents
# ---------------------------------------------------------------------------
# Main
# ---------------------------------------------------------------------------
def relative_path(path: Path, repo_root: Path) -> str:
"""Return a display-friendly path relative to the repo root."""
try:
return str(path.relative_to(repo_root))
except ValueError:
return str(path)
def main() -> int:
parser = argparse.ArgumentParser(
description="Validate YAML frontmatter in Ring skill, command, and agent files.",
)
parser.add_argument(
"--strict",
action="store_true",
help="Treat warnings as errors (exit code 1 if any warnings).",
)
parser.add_argument(
"--plugin",
type=str,
default=None,
help="Check only one plugin (e.g., --plugin dev-team).",
)
args = parser.parse_args()
# Resolve repo root (this script lives in default/hooks/)
script_dir = Path(__file__).resolve().parent
repo_root = script_dir.parent.parent
# Determine which plugins to scan
if args.plugin:
if args.plugin not in ALL_PLUGINS:
print(
f"Error: unknown plugin '{args.plugin}'. "
f"Valid plugins: {', '.join(ALL_PLUGINS)}",
file=sys.stderr,
)
return 1
plugins = [args.plugin]
else:
plugins = ALL_PLUGINS
# Discover files
skill_files, command_files, agent_files = discover_files(repo_root, plugins)
all_issues: List[Issue] = []
files_checked = 0
# Validate skills
for path in skill_files:
rel = relative_path(path, repo_root)
try:
content = path.read_text(encoding="utf-8")
except OSError as exc:
all_issues.append(Issue("ERROR", rel, f"cannot read file: {exc}"))
files_checked += 1
continue
fm = parse_frontmatter(content)
if fm is None:
all_issues.append(Issue("ERROR", rel, "no YAML frontmatter found"))
files_checked += 1
continue
all_issues.extend(validate_skill(rel, fm))
files_checked += 1
# Validate commands
for path in command_files:
rel = relative_path(path, repo_root)
try:
content = path.read_text(encoding="utf-8")
except OSError as exc:
all_issues.append(Issue("ERROR", rel, f"cannot read file: {exc}"))
files_checked += 1
continue
fm = parse_frontmatter(content)
if fm is None:
all_issues.append(Issue("ERROR", rel, "no YAML frontmatter found"))
files_checked += 1
continue
all_issues.extend(validate_command(rel, fm))
files_checked += 1
# Validate agents
for path in agent_files:
rel = relative_path(path, repo_root)
try:
content = path.read_text(encoding="utf-8")
except OSError as exc:
all_issues.append(Issue("ERROR", rel, f"cannot read file: {exc}"))
files_checked += 1
continue
fm = parse_frontmatter(content)
if fm is None:
all_issues.append(Issue("ERROR", rel, "no YAML frontmatter found"))
files_checked += 1
continue
all_issues.extend(validate_agent(rel, fm))
files_checked += 1
# Print issues
for issue in all_issues:
print(issue)
# Summary
error_count = sum(1 for i in all_issues if i.level == "ERROR")
warning_count = sum(1 for i in all_issues if i.level == "WARNING")
print(f"\n{error_count} errors, {warning_count} warnings across {files_checked} files")
# Exit code
if error_count > 0:
return 1
if args.strict and warning_count > 0:
return 1
return 0
if __name__ == "__main__":
sys.exit(main())

View file

@ -1,8 +1,13 @@
---
name: drawing-diagrams
name: ring:drawing-diagrams
description: Generate Mermaid diagrams from context and open them in mermaid.live in the browser. Use when the user asks for a diagram, visualization, flowchart, sequence diagram, ER diagram, or any visual representation of code, architecture, or processes. Produces lightweight, shareable mermaid.live URLs that open in the browser for interactive editing.
license: MIT
compatibility: Requires Python 3 (standard library only) and a browser. Uses `open` on macOS; Linux users need `xdg-open`.
trigger: |
- User asks for a diagram, chart, flowchart, or visualization
- User says "draw", "diagram", "visualize", "chart", "show me"
- Need to visualize architecture, data flow, state machines, sequences, or relationships
- Explaining complex systems where a visual would be more effective than prose
skip_when: The user needs a rich, branded, or styled HTML visualization (use ring:visual-explainer instead). This skill produces shareable mermaid.live URLs; visual-explainer produces self-contained Lerian-branded HTML files.
---

View file

@ -1,11 +1,17 @@
---
name: ring:gandalf-webhook
description: Send tasks to Gandalf (AI team member) via webhook and get responses back. Publish to Alfarrábio, send Slack notifications, ask for business context, and more.
user_invocable: true
allowed-tools:
- Bash
- Read
- Write
trigger: |
- Need to publish content to Alfarrábio (report server)
- Need to send Slack notifications via Gandalf
- Need to ask Gandalf for business context or information
- Need to delegate a task to Gandalf (AI team member on dedicated Mac mini)
skip_when: |
- Not connected to Lerian's Tailscale network
- Task can be completed locally without Gandalf's capabilities
- Publishing to a destination other than Alfarrábio
---
# Gandalf Webhook

View file

@ -1,13 +1,17 @@
---
name: ring:git-commit
description: Smart commit organization with atomic grouping, conventional commits, and trailer management
user_invocable: false
allowed-tools:
- Bash
- Read
- Glob
- Grep
- AskUserQuestion
trigger: |
- User asks to commit changes or says "commit"
- Working directory has staged or unstaged changes ready to commit
- End of a development task where changes need to be recorded
- Need to organize messy working directory into clean commit history
skip_when: |
- No changes in working directory (nothing to commit)
- Changes are still work-in-progress and not ready to commit
- User explicitly wants to use raw git commands without smart grouping
---
Analyze changes, group them into coherent atomic commits, and create signed commits following repository conventions. This skill transforms a messy working directory into a clean, logical commit history.

View file

@ -1,10 +1,18 @@
---
name: ring:production-readiness-audit
title: Production Readiness Audit
category: operations
tier: advanced
description: Comprehensive Ring-standards-aligned 44-dimension production readiness audit. Detects project stack, loads Ring standards via WebFetch, and runs in batches of 10 explorers appending incrementally to a single report file. Categories - Structure (pagination, errors, routes, bootstrap, runtime, core deps, naming, domain modeling, nil-safety, api-versioning, resource-leaks), Security (auth, IDOR, SQL, validation, secret-scanning, data-encryption, multi-tenant, rate-limiting, cors), Operations (telemetry, health, config, connections, logging, resilience, graceful-degradation), Quality (idempotency, docs, debt, testing, dependencies, performance, concurrency, migrations, linting, caching), Infrastructure (containers, hardening, cicd, async, makefile, license). Produces scored report (0-430, max 440 with multi-tenant) with severity ratings and standards cross-reference.
allowed-tools: Task, Read, Glob, Grep, Write, TodoWrite, WebFetch
trigger: |
- Preparing a service for production deployment
- Conducting periodic security or quality review of a codebase
- Onboarding to assess codebase health and maturity
- Evaluating technical debt before a major release
- Validating compliance with Ring engineering standards
skip_when: |
- Project is a prototype or throwaway proof-of-concept not heading to production
- Codebase is a library or SDK with no deployable service component
- User only needs a single-dimension check (use targeted review instead)
---
# Production Readiness Audit

View file

@ -1,6 +1,5 @@
---
name: ring:release-guide-info
version: 1.2.0
description: |
Generate Ops Update Guide from Git Diff. Produces internal Operations-facing
update/migration guides based on git diff analysis. Supports STRICT_NO_TOUCH (default)

View file

@ -10,6 +10,11 @@ trigger: |
- Before merge to main branch
- After fixing complex bug
skip_when: |
- Task is purely conversational or informational with no code changes
- Changes are limited to documentation or comments with zero logic modifications
- Code has not been modified since the last completed review cycle
NOT_skip_when: |
- "Code is simple" → Simple code can have security issues. Review required.
- "Just refactoring" → Refactoring may expose vulnerabilities. Review required.
@ -117,41 +122,6 @@ output_schema:
type: integer
description: "Total number of issues found by CodeRabbit across all units (0 if skipped)"
examples:
- name: "Feature review"
input:
unit_id: "task-001"
base_sha: "abc123"
head_sha: "def456"
implementation_summary: "Added user authentication with JWT"
requirements: "AC-1: User can login, AC-2: Invalid password returns error"
expected_output: |
## Review Summary
**Status:** PASS
**Reviewers:** 7/7 PASS
## Issues by Severity
| Severity | Count |
|
----------|-------|
| Critical | 0 |
| High | 0 |
| Medium | 0 |
| Low | 2 |
## Reviewer Verdicts
| Reviewer | Verdict |
|----------|---------|
| ring:code-reviewer | ✅ PASS |
| ring:business-logic-reviewer | ✅ PASS |
| ring:security-reviewer | ✅ PASS |
| ring:test-reviewer | ✅ PASS |
| ring:nil-safety-reviewer | ✅ PASS |
| ring:consequences-reviewer | ✅ PASS |
| ring:dead-code-reviewer | ✅ PASS |
## Handoff to Next Gate
- Ready for Gate 5: YES
---
# Code Review (Gate 4)

View file

@ -1,14 +1,17 @@
---
name: ring:session-handoff
description: Create handoff documents capturing session state for seamless context-clear and resume
user_invocable: false
allowed-tools:
- EnterPlanMode
- ExitPlanMode
- Write
- Bash
- Read
- Glob
trigger: |
- User is ending a session and wants to preserve context for later
- Context window is getting large and a fresh start would be beneficial
- Handing off work to another person or AI session
- User says "handoff", "save session", "wrap up", or "context transfer"
skip_when: |
- Session has minimal context that does not warrant a handoff document
- User simply wants to end the conversation without resuming later
- Work is fully complete with nothing pending for a future session
---
# Session Handoff Skill

View file

@ -1,11 +1,17 @@
---
name: ring:visual-explainer
description: Generate beautiful, self-contained HTML pages that visually explain systems, code changes, plans, and data. Use when the user asks for a diagram, architecture overview, diff review, plan review, project recap, comparison table, or any visual explanation of technical concepts. Also use proactively when you are about to render a complex ASCII table (4+ rows or 3+ columns) — present it as a styled HTML page instead.
license: MIT
compatibility: Requires a browser to view generated HTML files. Optional surf-cli for AI image generation.
metadata:
author: nicobailon
version: "0.3.0"
trigger: |
- User asks for a visual explanation, architecture overview, or comparison table
- About to render a complex ASCII table (4+ rows or 3+ columns) in the terminal
- Need a branded, self-contained HTML visualization with Lerian styling
- User asks for diff review, plan review, project recap, or dashboard visualization
skip_when: |
- User needs a lightweight, shareable mermaid.live URL (use ring:drawing-diagrams instead)
- Output is a simple table (fewer than 4 rows and 3 columns) that fits well in terminal
- User explicitly requests plain text or markdown output
---
# Visual Explainer

View file

@ -1,6 +1,5 @@
---
name: ring:backend-engineer-golang
version: 1.7.0
description: Senior Backend Engineer specialized in Go for high-demand financial systems. Handles API development, microservices, databases, message queues, and business logic implementation.
type: specialist
output_schema:

View file

@ -1,6 +1,5 @@
---
name: ring:backend-engineer-typescript
version: 1.5.0
description: Senior Backend Engineer specialized in TypeScript/Node.js for scalable systems. Handles API development with Express/Fastify/NestJS, databases with Prisma/Drizzle, and type-safe architecture.
type: specialist
output_schema:

View file

@ -1,6 +1,5 @@
---
name: ring:devops-engineer
version: 1.4.0
description: Senior DevOps Engineer specialized in cloud infrastructure for financial services. Handles containerization, IaC, and local development environments.
type: specialist
output_schema:

View file

@ -1,6 +1,5 @@
---
name: ring:frontend-bff-engineer-typescript
version: 2.5.0
description: Senior BFF (Backend for Frontend) Engineer specialized in Next.js API Routes with Clean Architecture, DDD, and Hexagonal patterns. Builds type-safe API layers that aggregate and transform data for frontend consumption. Supports dual-mode architecture (sindarian-server with decorators OR vanilla inversify).
type: specialist
output_schema:

View file

@ -1,6 +1,5 @@
---
name: ring:frontend-designer
version: 1.6.0
description: Senior UI/UX Designer with full design team capabilities - UX research, information architecture, visual design, content design, accessibility, mobile/touch, i18n, data visualization, and prototyping. Produces specifications, not code. Includes UI Library Mode detection for handoff.
type: specialist
output_schema:
@ -75,12 +74,6 @@ input_schema:
- name: "constraints"
type: "object"
description: "Technical constraints (framework, performance, a11y)"
project_rules_integration:
check_first:
- "docs/PROJECT_RULES.md (local project)"
ring_standards:
- "WebFetch: Ring Frontend Standards (MANDATORY)"
both_required: true
---
# Frontend Designer

View file

@ -1,6 +1,5 @@
---
name: ring:frontend-engineer
version: 3.5.0
description: Senior Frontend Engineer specialized in React/Next.js for financial dashboards and enterprise applications. Expert in App Router, Server Components, accessibility, performance optimization, modern React patterns, and dual-mode UI library support (sindarian-ui vs vanilla).
type: specialist
output_schema:

View file

@ -1,6 +1,5 @@
---
name: ring:helm-engineer
version: 1.0.0
description: Specialist Helm Chart Engineer for Lerian platform. Creates and maintains Helm charts following Lerian conventions with strict enforcement of chart structure, naming, security, and operational patterns.
type: specialist
output_schema:

View file

@ -1,6 +1,5 @@
---
name: ring:prompt-quality-reviewer
version: 2.0.1
description: |
Expert Agent Quality Analyst specialized in evaluating AI agent executions against best practices,
identifying prompt deficiencies, calculating quality scores, and generating precise improvement

View file

@ -1,6 +1,5 @@
---
name: ring:qa-analyst-frontend
version: 1.0.0
description: Senior Frontend QA Analyst specialized in React/Next.js testing. 5 modes - unit (Vitest + Testing Library), accessibility (axe-core, WCAG 2.1 AA), visual (snapshots, Storybook), e2e (Playwright), performance (Core Web Vitals, Lighthouse).
type: specialist
output_schema:

View file

@ -1,6 +1,5 @@
---
name: ring:qa-analyst
version: 1.7.0
description: Senior Quality Assurance Analyst specialized in testing financial systems. Handles test strategy, API testing, E2E automation, performance testing, and compliance validation. Supports unit (Gate 3), fuzz (Gate 4), property (Gate 5), integration (Gate 6), chaos (Gate 7), and goroutine-leak (detection) testing modes.
type: specialist
output_schema:

View file

@ -1,6 +1,5 @@
---
name: ring:sre
version: 1.5.0
description: Senior Site Reliability Engineer specialized in VALIDATING observability implementations for high-availability financial systems. Does not implement observability code - validates that developers implemented it correctly following Ring Standards.
type: specialist
output_schema:

View file

@ -1,6 +1,5 @@
---
name: ring:ui-engineer
version: 1.1.0
description: UI Implementation Engineer specialized in translating product-designer outputs (ux-criteria.md, user-flows.md, wireframes/) into production-ready React/Next.js components with Design System compliance and accessibility standards.
type: specialist
output_schema:

View file

@ -1,6 +1,7 @@
---
name: ring:dev-service-discovery
description: Scan project and identify Service, Modules, and Resources for tenant-manager
argument-hint: "[project-path]"
---
Scan the current Go project and produce a visual report of the **Service → Module → Resource** hierarchy for tenant-manager registration.

View file

@ -1,14 +1,13 @@
---
name: ring:cycle-management
description: Development cycle state management — status reporting and cycle cancellation
user_invocable: false
allowed-tools:
- Read
- Write
- Bash
- Glob
- Grep
- AskUserQuestion
trigger: |
- User wants to check the status of a running development cycle
- User wants to cancel an active development cycle
- Invoked by /ring:dev-status or /ring:dev-cancel commands
skip_when: |
- No development cycle is active or was recently started
- User is asking about general project status (not cycle-specific)
---
# Cycle Management

View file

@ -1,11 +1,5 @@
---
name: ring:dev-chaos-testing
title: Development cycle chaos testing (Gate 7)
category: development-cycle
tier: 1
when_to_use: |
Use after integration testing (Gate 6) is complete.
MANDATORY for all development tasks with external dependencies - verifies graceful degradation under failure.
description: |
Gate 7 of development cycle - ensures chaos tests exist using Toxiproxy
to verify graceful degradation under connection loss, latency, and partitions.
@ -15,6 +9,12 @@ trigger: |
- MANDATORY for all development tasks with external dependencies
- Verifies system behavior under failure conditions
skip_when: |
- Not inside a development cycle (ring:dev-cycle)
- Service has no external dependencies (no database, cache, queue, or external API)
- Task is documentation-only, configuration-only, or non-code
- Frontend-only project with no backend service dependencies
NOT_skip_when: |
- "Infrastructure is reliable" - All infrastructure fails eventually. Be prepared.
- "Integration tests cover failures" - Integration tests verify happy path. Chaos verifies failures.
@ -83,32 +83,6 @@ verification:
- "All external dependencies have failure scenarios"
- "Recovery verified after each failure injection"
examples:
- name: "Chaos tests for database operations"
input:
unit_id: "task-001"
external_dependencies: ["postgres", "redis"]
language: "go"
expected_output: |
## Chaos Testing Summary
**Status:** PASS
**Dependencies Tested:** 2
**Scenarios Tested:** 6
**Recovery Verified:** Yes
## Failure Scenarios
| Component | Scenario | Status | Recovery |
|
-----------|----------|--------|----------|
| PostgreSQL | Connection Loss | PASS | Yes |
| PostgreSQL | High Latency | PASS | Yes |
| PostgreSQL | Network Partition | PASS | Yes |
| Redis | Connection Loss | PASS | Yes |
| Redis | High Latency | PASS | Yes |
| Redis | Network Partition | PASS | Yes |
## Handoff to Next Gate
- Ready for Gate 8 (Code Review): YES
---
# Dev Chaos Testing (Gate 7)

View file

@ -10,7 +10,7 @@ trigger: |
- Resuming an interrupted frontend development cycle (--resume flag)
- After backend dev cycle completes (consuming handoff)
prerequisite: |
prerequisites: |
- Tasks file exists with structured subtasks
- Not already in a specific gate skill execution
@ -33,20 +33,6 @@ verification:
manual:
- "All gates for current task show PASS in state file"
examples:
- name: "New frontend from backend handoff"
invocation: "/ring:dev-cycle-frontend docs/pre-dev/auth/tasks-frontend.md"
expected_flow: |
1. Load tasks with subtasks
2. Detect UI library mode (sindarian-ui or fallback)
3. Load backend handoff if available
4. Ask user for execution mode
5. Execute Gate 0→1→2→3→4→5→6→7→8 for each task
6. Generate feedback report
- name: "Resume interrupted frontend cycle"
invocation: "/ring:dev-cycle-frontend --resume"
- name: "Direct prompt mode"
invocation: "/ring:dev-cycle-frontend Implement dashboard with transaction list and charts"
---
# Frontend Development Cycle Orchestrator

View file

@ -12,7 +12,14 @@ trigger: |
- Resuming an interrupted development cycle (--resume flag)
- Need structured, gate-based task execution with quality checkpoints
prerequisite: |
skip_when: |
- No tasks file exists or no structured subtasks to execute
- Task is documentation-only, research-only, or planning-only
- User explicitly requested manual workflow without gates
- Already inside a specific gate skill execution (avoid nesting)
- Frontend project (use ring:dev-cycle-frontend instead)
prerequisites: |
- Tasks file exists with structured subtasks
- Not already in a specific gate skill execution
- Human has not explicitly requested manual workflow
@ -40,38 +47,6 @@ verification:
manual:
- "All gates for current task show PASS in state file"
- "No tasks have status 'blocked' for more than 3 iterations"
examples:
- name: "New feature from PM workflow"
invocation: "/ring:dev-cycle docs/pre-dev/auth/tasks.md"
expected_flow: |
1. Load tasks with subtasks from tasks.md
2. Ask user for checkpoint mode (per-task/per-gate/continuous)
3. Execute Gate 0→0.5→1→2→3→4→5→6→7→8→9 for each task sequentially
4. Generate feedback report after completion
- name: "Resume interrupted cycle"
invocation: "/ring:dev-cycle --resume"
expected_state: "Continues from last saved gate in current-cycle.json"
- name: "Execute with per-gate checkpoints"
invocation: "/ring:dev-cycle tasks.md --checkpoint per-gate"
expected_flow: |
1. Execute Gate 0, pause for approval
2. User approves, execute Gate 1, pause
3. Continue until all 11 gates complete
- name: "Execute with custom context for agents"
invocation: "/ring:dev-cycle tasks.md \"Focus on error handling. Use existing UserRepository.\""
expected_flow: |
1. Load tasks and store custom_prompt in state
2. All agent dispatches include custom instructions as context
3. Custom context visible in execution report
- name: "Instructions-only mode (no tasks file)"
invocation: "/ring:dev-cycle \"Add webhook notification support for account status changes\""
expected_flow: |
1. Detect prompt-only mode (no task file provided)
2. Dispatch ring:codebase-explorer to analyze project
3. Generate tasks internally from prompt + codebase analysis
4. Present generated tasks for user confirmation
5. Execute Gate 0→0.5→1→2→3→4→5→6→7→8→9 for each generated task
---
# Development Cycle Orchestrator

View file

@ -1,6 +1,5 @@
---
name: ring:dev-delivery-verification
version: 1.0.0
description: |
Delivery Verification Gate — verifies that what was requested is actually delivered
as reachable, integrated code. Not quality review (Gate 8), not test verification
@ -13,6 +12,11 @@ trigger: |
- After any refactoring task claims completion
- When code is generated/scaffolded and needs integration verification
skip_when: |
- Not inside a development cycle (ring:dev-cycle or ring:dev-refactor)
- Task is documentation-only, configuration-only, or non-code
- No implementation was produced in Gate 0 (nothing to verify)
NOT_skip_when: |
- "Code compiles" → Compilation ≠ integration. Dead code compiles.
- "Tests pass" → Unit tests on isolated structs pass without wiring.

View file

@ -13,6 +13,10 @@ trigger: |
- Auditing existing dependencies for supply-chain risk
- Reviewing a PR that adds or updates dependencies
- Investigating a potential supply-chain compromise
skip_when: |
- No dependencies are being added, updated, or audited
- Task involves only internal code changes with no new imports
- Dependency is already vetted and pinned in lockfile
related:
complementary: [ring:dev-docker-security, ring:dev-sre, ring:dev-implementation]

View file

@ -10,6 +10,12 @@ trigger: |
- Implementation complete from Gate 0
- Need containerization or environment setup
skip_when: |
- Not inside a development cycle (ring:dev-cycle)
- Task is documentation-only, configuration-only, or non-code
- Project already has complete Docker and docker-compose setup unchanged by Gate 0
- Pure library package with no deployable service
NOT_skip_when: |
- "Application runs fine locally" → Docker ensures consistency across environments.
- "Docker is overkill" → Docker is baseline, not overkill.
@ -112,35 +118,6 @@ verification:
- "Verify docker-compose ps shows all services as 'Up (healthy)'"
- "Verify .env.example documents all required environment variables"
examples:
- name: "New Go service"
input:
unit_id: "task-001"
language: "go"
service_type: "api"
implementation_files: ["cmd/api/main.go", "internal/handler/user.go"]
new_services: ["postgres", "redis"]
expected_output: |
## DevOps Summary
**Status:** PASS
## Files Changed
| File | Action |
|
------|--------|
| Dockerfile | Created |
| docker-compose.yml | Created |
| .env.example | Created |
## Verification Results
| Check | Status |
|-------|--------|
| Build | ✅ PASS |
| Services Start | ✅ PASS |
| Health Checks | ✅ PASS |
## Handoff to Next Gate
- Ready for Gate 2: YES
---
# DevOps Setup (Gate 1)

View file

@ -10,6 +10,10 @@ trigger: |
- Auditing an existing Dockerfile for security
- Preparing images for Docker Hub publication
- Docker Hub health score is below grade A
skip_when: |
- Project has no Dockerfile and none is being created
- Changes are application-code only with no Docker modifications
- Using pre-built images without custom Dockerfile
related:
complementary: [ring:dev-devops, ring:dev-sre]

View file

@ -1,11 +1,5 @@
---
name: ring:dev-frontend-accessibility
title: Frontend development cycle accessibility testing (Gate 2)
category: development-cycle-frontend
tier: 1
when_to_use: |
Use after DevOps setup (Gate 1) is complete in the frontend dev cycle.
MANDATORY for all frontend development tasks - ensures WCAG 2.1 AA compliance.
description: |
Gate 2 of frontend development cycle - ensures all components pass axe-core
automated accessibility scans with zero WCAG 2.1 AA violations.
@ -15,6 +9,12 @@ trigger: |
- MANDATORY for all frontend development tasks
- Validates WCAG 2.1 AA compliance
skip_when: |
- Not inside a frontend development cycle (ring:dev-cycle-frontend)
- Backend-only project with no UI components
- Task is documentation-only, configuration-only, or non-code
- Changes are limited to build tooling, CI/CD, or infrastructure with no UI impact
NOT_skip_when: |
- "It's an internal tool" - WCAG compliance is mandatory for all applications.
- "The component library handles accessibility" - Library components can be misused.
@ -83,27 +83,6 @@ verification:
- "Keyboard navigation tests cover all interactive elements"
- "Focus management tests exist for modals and dialogs"
examples:
- name: "Accessibility tests for login form"
input:
unit_id: "task-001"
implementation_files: ["src/components/LoginForm.tsx"]
language: "typescript"
expected_output: |
## Accessibility Testing Summary
**Status:** PASS
**Components Tested:** 1
**Violations Found:** 0
**Keyboard Nav Tests:** 3
## Violations Report
| Component | Violations | Status |
|
-----------|-----------|--------|
| LoginForm | 0 | PASS |
## Handoff to Next Gate
- Ready for Gate 3 (Unit Testing): YES
---
# Dev Frontend Accessibility Testing (Gate 2)

View file

@ -1,11 +1,5 @@
---
name: ring:dev-frontend-e2e
title: Frontend development cycle E2E testing (Gate 5)
category: development-cycle-frontend
tier: 1
when_to_use: |
Use after visual testing (Gate 4) is complete in the frontend dev cycle.
MANDATORY for all frontend development tasks - validates complete user flows.
description: |
Gate 5 of frontend development cycle - ensures all user flows from
product-designer have passing E2E tests with Playwright across browsers.
@ -15,6 +9,12 @@ trigger: |
- MANDATORY for all frontend development tasks
- Validates user flows end-to-end
skip_when: |
- Not inside a frontend development cycle (ring:dev-cycle-frontend)
- Backend-only project with no UI components
- Task is documentation-only, configuration-only, or non-code
- No user-facing flows were added or changed in this cycle
NOT_skip_when: |
- "Unit tests cover the flow" - Unit tests don't test real browser + API interaction.
- "We only need Chromium" - Users use Firefox and Safari too.
@ -89,28 +89,6 @@ verification:
- "Responsive viewports covered"
- "3 consecutive passes without flaky failures"
examples:
- name: "E2E tests for transaction flow"
input:
unit_id: "task-001"
implementation_files: ["src/app/transactions/page.tsx"]
user_flows_path: "docs/pre-dev/transactions/user-flows.md"
expected_output: |
## E2E Testing Summary
**Status:** PASS
**Flows Tested:** 3/3
**Happy Path Tests:** 3
**Error Path Tests:** 6
**Browsers Passed:** 3/3
## Flow Coverage
| User Flow | Happy Path | Error Paths | Browsers | Status |
|
-----------|------------|-------------|----------|--------|
| Create Transaction | PASS | API 500, Validation | 3/3 | PASS |
## Handoff to Next Gate
- Ready for Gate 6 (Performance Testing): YES
---
# Dev Frontend E2E Testing (Gate 5)

View file

@ -1,11 +1,5 @@
---
name: ring:dev-frontend-performance
title: Frontend development cycle performance testing (Gate 6)
category: development-cycle-frontend
tier: 1
when_to_use: |
Use after E2E testing (Gate 5) is complete in the frontend dev cycle.
MANDATORY for all frontend development tasks - ensures performance meets thresholds.
description: |
Gate 6 of frontend development cycle - ensures Core Web Vitals compliance,
Lighthouse performance score > 90, and bundle size within budget.
@ -15,6 +9,12 @@ trigger: |
- MANDATORY for all frontend development tasks
- Validates performance before code review
skip_when: |
- Not inside a frontend development cycle (ring:dev-cycle-frontend)
- Backend-only project with no UI components
- Task is documentation-only, configuration-only, or non-code
- Changes are limited to test files, CI/CD, or non-rendered code
NOT_skip_when: |
- "Performance is fine on my machine" - Users have slower devices.
- "We'll optimize later" - Performance debt compounds.
@ -88,28 +88,6 @@ verification:
- "Bundle size increase < 10%"
- "No bare <img> tags (all use next/image)"
examples:
- name: "Performance tests for dashboard"
input:
unit_id: "task-001"
implementation_files: ["src/app/dashboard/page.tsx"]
expected_output: |
## Performance Testing Summary
**Status:** PASS
**LCP:** 1.8s (< 2.5s)
**CLS:** 0.03 (< 0.1)
**INP:** 95ms (< 200ms)
**Lighthouse:** 94 (> 90)
**Bundle Change:** +3.2% (< 10%)
## Core Web Vitals Report
| Page | LCP | CLS | INP | Status |
|
------|-----|-----|-----|--------|
| /dashboard | 1.8s | 0.03 | 95ms | PASS |
## Handoff to Next Gate
- Ready for Gate 7 (Code Review): YES
---
# Dev Frontend Performance Testing (Gate 6)

View file

@ -1,11 +1,5 @@
---
name: ring:dev-frontend-visual
title: Frontend development cycle visual/snapshot testing (Gate 4)
category: development-cycle-frontend
tier: 1
when_to_use: |
Use after unit testing (Gate 3) is complete in the frontend dev cycle.
MANDATORY for all frontend development tasks - ensures visual consistency.
description: |
Gate 4 of frontend development cycle - ensures all components have snapshot
tests covering all states, viewports, and edge cases.
@ -16,6 +10,12 @@ trigger: |
- Catches visual regressions before review
skip_when: |
- Not inside a frontend development cycle (ring:dev-cycle-frontend)
- Backend-only project with no UI components
- Task is documentation-only, configuration-only, or non-code
- No new UI components were added or visual changes made in this cycle
NOT_skip_when: |
- "Snapshots are brittle" - Brittle snapshots catch unintended changes.
- "We test visually in the browser" - Manual testing doesn't catch regressions.
- "Only default state matters" - Users see error, loading, and empty states too.
@ -85,31 +85,6 @@ verification:
- "Responsive viewports covered (375px, 768px, 1280px)"
- "No sindarian-ui component duplication in components/ui/"
examples:
- name: "Snapshot tests for transaction list"
input:
unit_id: "task-001"
implementation_files: ["src/components/TransactionList.tsx"]
expected_output: |
## Visual Testing Summary
**Status:** PASS
**Components with Snapshots:** 1
**Total Snapshots:** 8
**Snapshot Failures:** 0
## Snapshot Coverage
| Component | States | Viewports | Edge Cases | Status |
|
-----------|--------|-----------|------------|--------|
| TransactionList | 4/4 | 3/3 | Long text | PASS |
## Component Duplication Check
| Component in components/ui/ | In sindarian-ui? | Status |
|-----------------------------|------------------|--------|
| _No duplications found_ | - | PASS |
## Handoff to Next Gate
- Ready for Gate 5 (E2E Testing): YES
---
# Dev Frontend Visual Testing (Gate 4)

View file

@ -1,11 +1,5 @@
---
name: ring:dev-fuzz-testing
title: Development cycle fuzz testing (Gate 4)
category: development-cycle
tier: 1
when_to_use: |
Use after unit testing (Gate 3) is complete.
MANDATORY for all development tasks - discovers edge cases and crashes.
description: |
Gate 4 of development cycle - ensures fuzz tests exist with proper seed corpus
to discover edge cases, crashes, and unexpected input handling.
@ -15,6 +9,12 @@ trigger: |
- MANDATORY for all development tasks
- Discovers crashes and edge cases via random input generation
skip_when: |
- Not inside a development cycle (ring:dev-cycle)
- Task is documentation-only, configuration-only, or non-code
- No functions accept external or user-controlled input
- Frontend-only project (fuzz testing applies to backend code)
NOT_skip_when: |
- "Unit tests cover edge cases" - Fuzz tests find cases you didn't think of.
- "No time for fuzz testing" - Fuzz tests catch crashes before production.
@ -83,28 +83,6 @@ verification:
- "Seed corpus has at least 5 entries per function"
- "No crashes found during 30s fuzz run"
examples:
- name: "Fuzz tests for parser"
input:
unit_id: "task-001"
implementation_files: ["internal/parser/json.go"]
language: "go"
expected_output: |
## Fuzz Testing Summary
**Status:** PASS
**Fuzz Functions:** 2
**Corpus Entries:** 12
**Crashes Found:** 0
## Corpus Report
| Function | Entries | Crashes |
|
----------|---------|---------|
| FuzzParseJSON | 6 | 0 |
| FuzzParseConfig | 6 | 0 |
## Handoff to Next Gate
- Ready for Gate 5 (Property Testing): YES
---
# Dev Fuzz Testing (Gate 4)

View file

@ -1,8 +1,5 @@
---
name: ring:dev-goroutine-leak-testing
type: testing
author: ring-dev-team
version: 0.1.0
description: |
Goroutine leak detection skill - detects goroutine usage in Go code, runs goleak
to identify memory leaks, and dispatches ring:backend-engineer-golang to fix leaks
@ -14,6 +11,12 @@ trigger: |
- Suspected memory leak in production
- Need to verify goroutine-heavy code doesn't leak
skip_when: |
- Codebase contains no goroutine usage (no go func(), no go methodCall())
- Not a Go project
- Task is documentation-only, configuration-only, or non-code
- Changes do not touch or add any concurrent code paths
NOT_skip_when: |
- "Unit tests cover this" → Unit tests don't detect goroutine leaks. goleak does.
- "Goroutine will exit eventually" → Eventually = memory leak = OOM crash.

View file

@ -25,9 +25,6 @@ sequence:
after: [ring:dev-devops]
before: [ring:dev-sre]
dependencies: [ring:dev-devops]
role: orchestrator
related:
complementary: [ring:dev-devops, ring:dev-sre, ring:dev-implementation]
similar: [ring:dev-devops]

View file

@ -9,16 +9,10 @@ trigger: |
- Gate 0 of development cycle
- Tasks loaded at initialization
- Ready to write code
tdd_policy:
anti_patterns: |
- "Code already exists" → DELETE it. TDD is test-first.
- "Simple feature" → Simple ≠ exempt. TDD for all behavioral components.
- "Time pressure" → TDD saves time. No shortcuts.
- "PROJECT_RULES.md doesn't require" → Ring always requires TDD.
exempt_when: |
- Visual/presentational components (layout, styling, animations, static display) are exempt from TDD-RED and deferred to Gate 4 snapshots.
- Behavioral components (hooks, forms, state, conditional rendering, API) still require TDD.
skip_when: |
- Not inside a development cycle (ring:dev-cycle or ring:dev-refactor)
- Task is documentation-only, configuration-only, or non-code
- Implementation already completed for the current gate
sequence:
before: [ring:dev-devops]
@ -89,26 +83,6 @@ output_schema:
- name: tests_added
type: integer
agent_selection:
criteria:
- pattern: "*.go"
keywords: ["go.mod", "golang", "Go"]
agent: "ring:backend-engineer-golang"
- pattern: "*.ts"
keywords: ["express", "fastify", "nestjs", "backend", "api", "server"]
agent: "ring:backend-engineer-typescript"
- pattern: "*.tsx"
keywords: ["react", "next", "frontend", "component", "page"]
agent: "frontend-bff-engineer-typescript"
- pattern: "*.tsx"
keywords: ["ux-criteria", "wireframe", "user-flow", "design-spec", "product-designer"]
precondition: "docs/pre-dev/{feature}/ux-criteria.md exists"
agent: "ui-engineer"
- pattern: "*.css|*.scss"
keywords: ["design", "visual", "aesthetic", "styling", "ui"]
agent: "ring:frontend-designer"
fallback: "ASK_USER"
verification:
automated:
- command: "go build ./... 2>&1 | grep -c 'error'"
@ -121,33 +95,6 @@ verification:
- "TDD RED phase failure output captured before implementation"
- "Implementation follows project standards from PROJECT_RULES.md"
examples:
- name: "Go backend implementation"
input:
unit_id: "task-001"
requirements: "Add user authentication endpoint with JWT"
language: "go"
service_type: "api"
expected_output: |
## Implementation Summary
**Status:** PASS
**Agent:** ring:backend-engineer-golang
## TDD Results
| Phase | Status | Output |
|
-------|--------|--------|
| RED | ✅ | FAIL: TestUserAuth - expected token, got nil |
| GREEN | ✅ | PASS: TestUserAuth (0.003s) |
## Files Changed
| File | Action | Lines |
|------|--------|-------|
| internal/handler/auth.go | Created | +85 |
| internal/handler/auth_test.go | Created | +120 |
## Handoff to Next Gate
- Ready for Gate 1: YES
---
# Code Implementation (Gate 0)

View file

@ -1,11 +1,5 @@
---
name: ring:dev-integration-testing
title: Development cycle integration testing (Gate 6)
category: development-cycle
tier: 1
when_to_use: |
Use after property-based testing (Gate 5) is complete.
MANDATORY for all development tasks - verifies real service integration.
description: |
Gate 6 of development cycle - ensures integration tests pass for all
external dependency interactions using real containers via testcontainers.
@ -15,6 +9,12 @@ trigger: |
- MANDATORY for all development tasks
- Verifies real service integration with testcontainers
skip_when: |
- Not inside a development cycle (ring:dev-cycle)
- Task is documentation-only, configuration-only, or non-code
- Service has no external dependencies (no database, cache, queue, or external API)
- Pure library package with no integration points
NOT_skip_when: |
- "Unit tests cover it" - Unit tests mock. Integration tests verify real behavior.
- "No time for integration tests" - Integration bugs cost 10x more in production.
@ -95,37 +95,6 @@ verification:
- "No flaky tests (run 3x, all pass)"
- "All containers properly cleaned up"
examples:
- name: "Integration tests for user repository"
input:
unit_id: "task-001"
integration_scenarios: ["Create user in DB", "Find user by email", "Update user"]
external_dependencies: ["postgres"]
language: "go"
expected_output: |
## Integration Testing Summary
**Status:** PASS
**Scenarios:** 3 tested
**Tests:** 5 passed, 0 failed
## Scenario Coverage
| Scenario | Test File | Tests | Status |
|
----------|-----------|-------|--------|
| Create user in DB | user_integration_test.go | 2 | PASS |
| Find user by email | user_integration_test.go | 2 | PASS |
| Update user | user_integration_test.go | 1 | PASS |
## Quality Gate Results
| Check | Status |
|-------|--------|
| Build tags | PASS |
| No hardcoded ports | PASS |
| Testcontainers | PASS |
| No t.Parallel() | PASS |
## Handoff to Next Gate
- Ready for Gate 7 (Chaos Testing): YES
---
# Dev Integration Testing (Gate 6)

View file

@ -11,6 +11,10 @@ trigger: |
- Auditing an existing llms.txt for completeness
- Generating CLAUDE.md or AGENTS.md for AI coding agents
- Improving AI readability of a repository
skip_when: |
- Repository already has a complete, up-to-date llms.txt
- Task is code implementation with no documentation scope
- Repository is private/internal and does not need LLM discoverability
related:
complementary: [ring:dev-cycle, ring:dev-implementation]

View file

@ -16,7 +16,7 @@ skip_when: |
- Project is not Go → Not applicable
- Project does not use lib-commons → Not applicable
prerequisite: |
prerequisites: |
- Go project with go.mod containing lib-commons/v2 or lib-commons/v3
- docs/PROJECT_RULES.md exists (recommended but not blocking)
@ -35,19 +35,6 @@ verification:
description: "Zero v2/v3 direct imports remain"
success_pattern: "^0$"
examples:
- name: "Analyze and show visual report"
invocation: "/ring:migrate-v4"
expected_flow: "Scan → Map → Visual HTML report opened in browser"
- name: "Generate tasks for dev-cycle"
invocation: "/ring:migrate-v4 --tasks"
expected_flow: "Scan → Map → Visual report → migration-v4-tasks.md saved"
- name: "Full automatic migration"
invocation: "/ring:migrate-v4 --execute"
expected_flow: "Scan → Map → Visual report → tasks.md → ring:dev-cycle dispatched through all 10 gates"
- name: "Specific repository path"
invocation: "/ring:migrate-v4 /path/to/service --execute"
expected_flow: "Same as above but targets specific path"
---
# Dev Migrate v4

View file

@ -1,8 +1,5 @@
---
name: ring:dev-multi-tenant
slug: dev-multi-tenant
version: 2.0.0
type: skill
description: |
Multi-tenant development cycle orchestrator following Ring Standards.
Auto-detects the service stack (PostgreSQL, MongoDB, Redis, RabbitMQ, S3)
@ -23,7 +20,13 @@ trigger: |
- User asks to add tenant isolation to an existing service
- Task mentions "multi-tenant", "tenant isolation", "tenant-manager", "postgres.Manager", "WithPG", "WithMB", "EventListener", "TenantCache", "TenantLoader", "OnTenantAdded", "OnTenantRemoved"
prerequisite: |
skip_when: |
- Service is not a Go project
- Task does not involve multi-tenancy or tenant isolation
- Service is a shared infrastructure component that operates outside tenant context
- Task is documentation-only, configuration-only, or non-code
prerequisites: |
- Go service with existing single-tenant functionality
NOT_skip_when: |
@ -102,21 +105,6 @@ output_schema:
- name: total_files_changed
type: integer
examples:
- name: "Add multi-tenant to a service"
invocation: "/ring:dev-multi-tenant"
expected_flow: |
1. Gate 0: Auto-detect stack + determine if service has targetServices
2. Gate 1: Analyze codebase (build implementation roadmap)
3. Gate 1.5: Visual implementation preview (HTML report for developer approval)
4. Gates 2-5: Implementation (agent loads multi-tenant.md, follows roadmap)
5. Gate 5.5: M2M Secret Manager (if service has targetServices)
6. Gate 6: RabbitMQ multi-tenant (if RabbitMQ detected)
7. Gate 7: Metrics & Backward compatibility
8. Gate 8: Tests
9. Gate 9: Code review
10. Gate 10: User validation
11. Gate 11: Activation guide
---
# Multi-Tenant Development Cycle

View file

@ -1,11 +1,5 @@
---
name: ring:dev-property-testing
title: Development cycle property-based testing (Gate 5)
category: development-cycle
tier: 1
when_to_use: |
Use after fuzz testing (Gate 4) is complete.
MANDATORY for all development tasks - verifies domain invariants always hold.
description: |
Gate 5 of development cycle - ensures property-based tests exist
to verify domain invariants hold for all randomly generated inputs.
@ -15,6 +9,12 @@ trigger: |
- MANDATORY for all development tasks
- Verifies domain invariants via testing/quick package
skip_when: |
- Not inside a development cycle (ring:dev-cycle)
- Task is documentation-only, configuration-only, or non-code
- No domain logic with invariants was added or modified
- Frontend-only project (property testing applies to backend domain logic)
NOT_skip_when: |
- "Unit tests verify logic" - Property tests verify INVARIANTS across all inputs.
- "No domain invariants" - Every domain has invariants. Find them.
@ -87,30 +87,6 @@ verification:
- "At least one property per domain entity"
- "No counterexamples found"
examples:
- name: "Property tests for money calculations"
input:
unit_id: "task-001"
implementation_files: ["internal/domain/money.go"]
language: "go"
domain_invariants: ["Amount never negative", "Currency always valid"]
expected_output: |
## Property Testing Summary
**Status:** PASS
**Properties Tested:** 3
**Properties Passed:** 3
**Counterexamples Found:** 0
## Properties Report
| Property | Subject | Status |
|
----------|---------|--------|
| TestProperty_Money_AmountNeverNegative | Money | PASS |
| TestProperty_Money_CurrencyAlwaysValid | Money | PASS |
| TestProperty_Money_AdditionCommutative | Money | PASS |
## Handoff to Next Gate
- Ready for Gate 6 (Integration Testing): YES
---
# Dev Property Testing (Gate 5)

View file

@ -1,8 +1,5 @@
---
name: ring:dev-readyz
slug: dev-readyz
version: 1.0.0
type: skill
description: |
Implements comprehensive readiness probes (/readyz) and startup self-probes for
Lerian services. Goes beyond basic K8s liveness: validates every external dependency
@ -20,6 +17,12 @@ trigger: |
- Service lacks /readyz or has incomplete dependency checks
- Service missing startup self-probe
skip_when: |
- Pure library package with no deployable service or HTTP server
- Task is documentation-only, configuration-only, or non-code
- Service has no external dependencies and no network listeners
- CLI tool or batch job that does not serve HTTP traffic
NOT_skip_when: |
- "K8s TCP probe is enough" → TCP ≠ app ready. Monetarie proved it.
- "/health already exists" → /health without self-probe = blind. /readyz validates ALL deps.
@ -81,15 +84,6 @@ output_schema:
- name: dependencies_covered
type: integer
examples:
- name: "Go API service"
invocation: "/ring:dev-readyz"
expected_flow: |
1. Scan project for external dependencies
2. Validate /readyz endpoint covers all deps
3. Generate missing checks
4. Implement startup self-probe
5. Verify /health reflects self-probe result
---
# Readyz & Self-Probe Implementation

View file

@ -1,8 +1,5 @@
---
name: ring:dev-service-discovery
slug: dev-service-discovery
version: 1.2.0
type: skill
description: |
Scans the current Go project and identifies the Service → Module → Resource
hierarchy for tenant-manager registration. Detects service name and type,
@ -25,7 +22,13 @@ trigger: |
- Before running ring:dev-multi-tenant on a new service
- User asks about MongoDB indexes in a project
prerequisite: |
skip_when: |
- Not a Go project
- Task does not involve service discovery, tenant-manager, or resource mapping
- Task is documentation-only, configuration-only, or non-code
- Project has no external dependencies (no database, cache, or queue)
prerequisites: |
- Go project with go.mod in the current working directory
NOT_skip_when: |
@ -42,18 +45,6 @@ output_schema:
pattern: "^## Service Discovery Report"
required: true
examples:
- name: "Scan current project"
invocation: "/ring:dev-service-discovery"
expected_flow: |
1. Detect service identity (ApplicationName, type)
2. Detect modules (WithModule calls, component structure)
3. Detect resources per module (PostgreSQL, MongoDB, RabbitMQ)
4. Detect database names per module (from bootstrap config + .env.example)
5. Cross-module analysis: detect shared databases across modules
6. Detect MongoDB indexes (in-code + scripts)
7. Generate visual HTML report with shared database warnings
8. Open in browser for review
---
# Service Discovery for Tenant-Manager

View file

@ -10,6 +10,12 @@ trigger: |
- Gate 1 (DevOps) setup complete
- Service needs observability validation (logging, tracing)
skip_when: |
- Not inside a development cycle (ring:dev-cycle)
- Task is documentation-only, configuration-only, or non-code
- Pure library package with no deployable service
- Static frontend with no API calls or backend interactions
NOT_skip_when: |
- "Task says observability not required" → AI cannot self-exempt. all services need observability.
- "Pure frontend" → If it calls any API, backend needs observability. Frontend-only = static HTML.
@ -94,33 +100,6 @@ verification:
manual:
- "Verify logs include trace_id when tracing is enabled"
examples:
- name: "API service observability validation"
input:
unit_id: "task-001"
language: "go"
service_type: "api"
implementation_agent: "ring:backend-engineer-golang"
implementation_files: ["internal/handler/user.go", "internal/service/user.go"]
expected_output: |
## Validation Result
**Status:** PASS
**Iterations:** 1
## Instrumentation Coverage
| Layer | Instrumented | Total | Coverage |
|
-------|--------------|-------|----------|
| Handlers | 5 | 5 | 100% |
| Services | 8 | 8 | 100% |
| Repositories | 4 | 4 | 100% |
| **TOTAL** | 17 | 17 | **100%** |
## Issues Found
None
## Handoff to Next Gate
- Ready for Gate 3: YES
---
# SRE Validation (Gate 2)

View file

@ -9,6 +9,12 @@ trigger: |
- Task has acceptance criteria requiring test coverage
- Need to verify implementation meets requirements
skip_when: |
- Not inside a development cycle (ring:dev-cycle)
- Task is documentation-only, configuration-only, or non-code
- No code implementation was produced (nothing to test)
- Changes are limited to CI/CD, infrastructure, or deployment configuration
NOT_skip_when: |
- "Manual testing validates all criteria" → Manual tests are not executable. Gate 3 requires unit tests.
- "Integration tests are better" → Gate 3 scope is unit tests only.
@ -94,32 +100,6 @@ verification:
- "Every acceptance criterion has at least one test"
- "No skipped or pending tests"
examples:
- name: "TDD for auth service"
input:
unit_id: "task-001"
acceptance_criteria: ["User can login with valid credentials", "Invalid password returns error"]
implementation_files: ["internal/service/auth.go"]
language: "go"
expected_output: |
## Testing Summary
**Status:** PASS
**Coverage:** 89.5%
## Coverage Report
| Package | Coverage |
|
---------|----------|
| internal/service | 89.5% |
## Traceability Matrix
| AC | Test | Status |
|----|------|--------|
| AC-1 | TestAuthService_Login_ValidCredentials | ✅ |
| AC-2 | TestAuthService_Login_InvalidPassword | ✅ |
## Handoff to Next Gate
- Ready for Gate 4: YES
---
# Dev Unit Testing (Gate 3)

View file

@ -9,6 +9,11 @@ trigger: |
- Implementation and tests complete
- Need user sign-off on acceptance criteria
skip_when: |
- Not inside a development cycle (ring:dev-cycle or ring:dev-cycle-frontend)
- Task is documentation-only, configuration-only, or non-code
- No implementation or tests were produced in prior gates
NOT_skip_when: |
- "Already validated" → Each iteration needs fresh validation.
- "User will validate manually" → Gate 5 IS user validation. Cannot skip.
@ -32,23 +37,6 @@ verification:
- "All acceptance criteria have verified evidence"
- "Validation checklist presented to user"
examples:
- name: "Successful validation"
context: "4 acceptance criteria, all tests pass"
expected_flow: |
1. Gather evidence for each criterion
2. Build validation checklist with evidence types
3. Present to user with APPROVED/REJECTED options
4. User selects APPROVED
5. Document approval, proceed to feedback loop
- name: "Validation rejection"
context: "AC-3 not met (response time too slow)"
expected_flow: |
1. Present validation checklist
2. User identifies AC-3 failure
3. User selects REJECTED with reason
4. Create remediation task
5. Return to Gate 0 for fixes
---
# Dev Validation (Gate 5)

View file

@ -1,29 +1,36 @@
<!-- Copyright 2025 Lerian Studio. -->
---
name: "ring:systemplane-migration"
version: "2.0.0"
type: skill
name: ring:systemplane-migration
description: >
Gate-based systemplane migration orchestrator. Migrates Lerian Go services from
.env/YAML-based configuration to the systemplane — a database-backed, hot-reloadable
runtime configuration and settings management plane with full audit history, optimistic
concurrency, change feeds, component-granular bundle rebuilds, and atomic infrastructure
replacement. Requires lib-commons v4.3.0+.
trigger_when:
- User requests systemplane migration/adoption
- Task mentions runtime configuration, hot-reload, config management
trigger: |
- User requests systemplane migration or adoption
- Task mentions runtime configuration, hot-reload, or config management
- Service needs database-backed configuration with audit trail
- BundleFactory or Reconciler development
prerequisites:
- BundleFactory or Reconciler development is required
skip_when: |
- Project is not a Go service
- Service does not use lib-commons v4
- Task is unrelated to configuration management or systemplane
NOT_skip_when: |
- Service already has systemplane code (verify compliance, do not skip)
- "It looks like systemplane is already set up" (existence ≠ compliance)
prerequisites: |
- Go project
- lib-commons/v4 dependency (v4.3.0+ required; upgrade first if older)
- PostgreSQL or MongoDB backend available
NOT_skip_when:
- Service already has systemplane code (verify compliance, do not skip)
- "It looks like systemplane is already set up" (existence ≠ compliance)
sequence:
after: ["ring:dev-cycle"]
input:
input_schema:
type: object
properties:
execution_mode:
@ -41,20 +48,22 @@ input:
type: boolean
existing_systemplane:
type: boolean
output:
type: object
properties:
gates_completed:
type: array
items: { type: string }
compliance_status:
type: string
enum: ["COMPLIANT", "NON-COMPLIANT", "NEW"]
key_count:
type: integer
files_created:
type: array
items: { type: string }
output_schema:
format: markdown
required_sections:
- name: "Migration Summary"
pattern: "^## Migration Summary"
required: true
- name: "Gates Completed"
pattern: "^## Gates Completed"
required: true
- name: "Compliance Status"
pattern: "^## Compliance Status"
required: true
- name: "Files Created"
pattern: "^## Files Created"
required: true
---
# Systemplane Migration Orchestrator

186
docs/FRONTMATTER_SCHEMA.md Normal file
View file

@ -0,0 +1,186 @@
# Frontmatter Schema Reference
Canonical source of truth for YAML frontmatter fields in Ring skills, commands, and agents. The validator script (`default/hooks/validate-frontmatter.py`) checks against this schema.
All frontmatter uses standard YAML between `---` delimiters at the top of each `.md` file. The session-start hook (`default/hooks/generate-skills-ref.py`) parses skill frontmatter at load time to build the skills quick reference.
---
## Skills (`SKILL.md`)
Skills live in `{plugin}/skills/{name}/SKILL.md`.
### Required Fields
| Field | Type | Parsed by Hooks | Description |
|-------|------|-----------------|-------------|
| `name` | string | YES | Skill identifier. MUST use `ring:` prefix (e.g., `ring:brainstorming`) |
| `description` | string | YES | What the skill does -- method or technique. Supports block scalar (`\|`) |
### Recommended Fields
Parsed by hooks and used for skill discovery/routing. Skills should define these.
| Field | Type | Parsed by Hooks | Description |
|-------|------|-----------------|-------------|
| `trigger` | string | YES | WHEN to use this skill -- primary decision field. Replaces deprecated `when_to_use` |
| `skip_when` | string | YES | WHEN NOT to use -- differentiates from similar skills |
| `NOT_skip_when` | string | YES | Override for `skip_when` -- cases where the skill MUST still be used despite skip signals |
| `prerequisites` | string/list | YES | What must be true before using this skill (e.g., test framework installed) |
| `verification` | string | YES | How to verify the skill's gate passed (e.g., coverage thresholds, build success) |
### Optional Fields (Parsed by Hooks)
| Field | Type | Parsed by Hooks | Description |
|-------|------|-----------------|-------------|
| `when_to_use` | string | YES | **DEPRECATED** -- use `trigger` instead. Kept for backward compatibility; hook falls back to this if `trigger` is absent |
| `sequence.after` | list | YES | Skills that should come before this one (e.g., `[ring:dev-implementation]`) |
| `sequence.before` | list | YES | Skills that typically follow this one (e.g., `[ring:writing-plans]`) |
| `related.similar` | list | YES | Skills that seem similar but differ (helps differentiation) |
| `related.complementary` | list | YES | Skills that pair well with this one |
### Optional Fields (Not Parsed by Hooks)
These are defined in skill frontmatter but not read by `generate-skills-ref.py`. They serve as structured metadata for agents and validation tooling.
| Field | Type | Description |
|-------|------|-------------|
| `compliance_rules` | list of objects | Validation rules with `id`, `description`, `check_type`, `pattern`, `severity`, `failure_message` |
| `composition` | object | How the skill works with others: `works_well_with`, `conflicts_with`, `typical_workflow` |
| `input_schema` | object | Expected input context: `required` and `optional` fields with `name`, `type`, `description` |
| `output_schema` | object | Expected output format: `format` (always `"markdown"`), `required_sections` with `name`, `pattern`, `required` |
### Explicitly NOT Valid for Skills
| Field | Reason |
|-------|--------|
| `version` | Use git history for versioning |
| `allowed-tools` | Define tool access in the skill body, not frontmatter |
| `examples` | Include examples in the skill body |
| `category` | Not part of the schema -- categories are derived by hooks from directory name patterns |
| `tier` | Not part of the schema |
| `slug` | Not part of the schema |
| `user_invocable` | Not part of the schema -- invocability is implicit from skill structure |
| `title` | Not part of the schema -- use `name` |
| `type` | Not part of the schema for skills -- `type` is an agent-only field |
| `role` | Not part of the schema -- define role context in the skill body |
| `dependencies` | Not part of the schema -- use `prerequisites` for preconditions |
| `author` | Not part of the schema -- use git history |
| `license` | Not part of the schema -- repo-level license applies |
| `compatibility` | Not part of the schema |
| `metadata` | Not part of the schema -- use specific top-level fields instead |
| `agent_selection` | Not part of the schema -- define agent routing in the skill body |
| `tdd_policy` | Not part of the schema -- TDD is enforced by workflow, not frontmatter |
| `research_modes` | Not part of the schema -- define modes in the skill body |
| `trigger_when` | Not part of the schema -- use `trigger` |
---
## Commands (`*.md` in `commands/`)
Commands live in `{plugin}/commands/{name}.md`.
### Required Fields
| Field | Type | Description |
|-------|------|-------------|
| `name` | string | Command identifier. MUST use `ring:` prefix (e.g., `ring:commit`) |
| `description` | string | What the command does -- single line |
### Recommended Fields
| Field | Type | Description |
|-------|------|-------------|
| `argument-hint` | string | Argument syntax hint shown in command listings (e.g., `"[topic]"`, `"[message]"`) |
### Explicitly NOT Valid for Commands
| Field | Reason |
|-------|--------|
| `arguments` | Use `argument-hint` for syntax hints; document arguments in the command body |
| `args` | Use `argument-hint` |
| `version` | Use git history |
---
## Agents (`*.md` in `agents/`)
Agents live in `{plugin}/agents/{name}.md`.
### Required Fields
| Field | Type | Description |
|-------|------|-------------|
| `name` | string | Agent identifier. MUST use `ring:` prefix (e.g., `ring:code-reviewer`) |
| `description` | string | What the agent does -- role and scope |
| `type` | enum | Agent classification. Values in use: `specialist`, `reviewer`, `orchestrator`, `planning`, `exploration`, `analyst`, `calculator` |
| `output_schema` | object | Defines required output sections (see sub-fields below) |
**`output_schema` sub-fields:**
| Sub-field | Type | Required | Description |
|-----------|------|----------|-------------|
| `output_schema.format` | string | YES | Always `"markdown"` |
| `output_schema.required_sections` | list | YES | List of section definitions |
| `output_schema.required_sections[].name` | string | YES | Section display name |
| `output_schema.required_sections[].pattern` | string | YES | Regex pattern to match the section heading |
| `output_schema.required_sections[].required` | boolean | YES | Whether the section is mandatory |
| `output_schema.required_sections[].description` | string | no | When/why this section is needed |
| `output_schema.required_sections[].required_when` | object | no | Conditional requirement (e.g., `invocation_context`, `prompt_contains`) |
| `output_schema.verdict_values` | list | no | Valid verdict values for reviewer agents (e.g., `["PASS", "FAIL", "NEEDS_DISCUSSION"]`) |
### Optional Fields
| Field | Type | Description |
|-------|------|-------------|
| `input_schema` | object | Expected input context with `required` and `optional` sub-fields, each a list of `{name, type, description}` |
### Explicitly NOT Valid for Agents
| Field | Reason |
|-------|--------|
| `version` | Use git history |
| `color` | Not part of the schema |
| `project_rules_integration` | Not part of the schema |
| `allowed-tools` | Define tool access in the agent body, not frontmatter |
| `tools` | Not part of the schema -- define tool access in the agent body, not frontmatter |
---
## Deprecated Fields
| Deprecated Field | Replaced By | Migration |
|------------------|-------------|-----------|
| `when_to_use` | `trigger` | Rename field. Hook falls back to `when_to_use` if `trigger` is absent, but new skills MUST use `trigger` |
| `prerequisite` (singular) | `prerequisites` (plural) | Rename to plural form |
| `arguments` | `argument-hint` | Use `argument-hint` for syntax hint; document full argument details in the command body |
---
## Validation
**Validator script:** `default/hooks/validate-frontmatter.py`
| Condition | Validator Behavior |
|-----------|--------------------|
| Missing required field (`name`, `description`) | Error |
| Unknown/unrecognized field | Warning |
| Deprecated field present | Warning with migration guidance |
| Skill missing `trigger` | Warning (recommended field) |
| Agent missing `type` or `output_schema` | Error |
**Parser script:** `default/hooks/generate-skills-ref.py`
- Tries `pyyaml` first, falls back to regex parser
- Extracts first meaningful line from block scalars for quick reference display
- Groups skills into categories based on directory name patterns
- Handles backward compatibility: `when_to_use` -> `trigger` -> `description` fallback chain
---
## Related Documents
- [CLAUDE.md](../CLAUDE.md) -- Main project instructions
- [AGENT_DESIGN.md](AGENT_DESIGN.md) -- Agent output schema archetypes and standards compliance
- [WORKFLOWS.md](WORKFLOWS.md) -- How to add skills, agents, and commands
- [PROMPT_ENGINEERING.md](PROMPT_ENGINEERING.md) -- Language patterns for agent prompts

View file

@ -1,9 +1,7 @@
---
name: ring:finops-analyzer
version: 1.2.0
description: Senior Regulatory Compliance Analyst specializing in Brazilian financial regulatory template analysis and field mapping validation (Gates 1-2). Expert in BACEN, RFB, and Open Banking compliance.
type: specialist
color: blue
output_schema:
format: "markdown"
required_sections:

View file

@ -1,9 +1,7 @@
---
name: ring:finops-automation
version: 1.2.0
description: Senior Template Implementation Engineer specializing in .tpl template creation for Brazilian regulatory compliance (Gate 3). Expert in Reporter platform with XML, HTML and TXT template formats.
type: specialist
color: green
output_schema:
format: "markdown"
required_sections:

View file

@ -1,6 +1,5 @@
---
name: ring:infrastructure-cost-estimator
version: 7.3.0
description: Infrastructure Cost Calculator with per-component sharing model, environment-specific calculations (Homolog vs Production), dynamic Helm chart data from LerianStudio/helm, TPS capacity analysis, networking architecture, and service-component dependency mapping. RECEIVES complete data (read at runtime from LerianStudio/helm) and CALCULATES detailed cost attribution, capacity planning, and profitability.
type: calculator
output_schema:

View file

@ -1,6 +1,5 @@
---
name: ring:infrastructure-cost-estimation
version: 6.0.0
description: |
Orchestrates infrastructure cost estimation with tier-based or custom TPS sizing.
Offers pre-configured tiers (Starter/Growth/Business/Enterprise) or custom TPS input.

View file

@ -5,11 +5,6 @@ description: |
batch approval by confidence level, and auto-saves dictionary after approval.
Supports both pre-defined templates (dictionary exists) and new templates (any spec).
dependencies:
- ring:finops-analyzer
role: regulatory-analyst
trigger: |
- regulatory-templates-setup completed
- Need to analyze regulatory specification and map fields

View file

@ -5,10 +5,6 @@ description: |
for the 5-stage regulatory workflow. Supports any regulatory template (pre-defined
or new) via official spec intake (URL/XSD/PDF).
dependencies: []
role: setup
trigger: |
- Called by regulatory-templates orchestrator at workflow start
- Need to select template type and initialize context

View file

@ -5,16 +5,6 @@ description: |
Gate 2 (validation), Gate 3 (generation), optional Test Gate, optional Contribution Gate.
Supports any regulatory template (BACEN, RFB, CVM, SUSEP, COAF, or other).
dependencies:
- ring:regulatory-templates-setup
- ring:regulatory-templates-gate1
- ring:regulatory-templates-gate2
- ring:regulatory-templates-gate3
- ring:finops-analyzer
- ring:finops-automation
role: orchestrator
trigger: |
- Creating BACEN CADOCs (4010, 4016, 4111, or any other)
- Mapping e-Financeira, DIMP, APIX templates

View file

@ -5,14 +5,6 @@ description: |
Open Banking), 1 for infrastructure cost estimation when onboarding customers.
Supports any regulatory template via open intake system.
dependencies:
- ring:finops-analyzer
- ring:finops-automation
- ring:infrastructure-cost-estimator
- ring:regulatory-templates
role: guide
trigger: |
- Brazilian regulatory reporting (BACEN, RFB)
- Financial compliance requirements

View file

@ -70,7 +70,11 @@ class CursorAdapter(PlatformAdapter):
normalized_name = normalize_cursor_name(name) or "untitled-skill"
parts: List[str] = []
parts.append(self.create_frontmatter({"name": normalized_name, "description": clean_desc_single}).rstrip())
parts.append(
self.create_frontmatter(
{"name": normalized_name, "description": clean_desc_single}
).rstrip()
)
parts.append("")
parts.append(f"# {self._to_title_case(name)}")
@ -137,13 +141,17 @@ class CursorAdapter(PlatformAdapter):
normalized_name = normalize_cursor_name(name) or "untitled-agent"
parts: List[str] = []
parts.append(self.create_frontmatter({"name": normalized_name, "description": clean_desc}).rstrip())
parts.append(
self.create_frontmatter({"name": normalized_name, "description": clean_desc}).rstrip()
)
parts.append("")
parts.append(self._transform_body_for_cursor(body))
return "\n".join(parts)
def transform_command(self, command_content: str, metadata: Optional[Dict[str, Any]] = None) -> str:
def transform_command(
self, command_content: str, metadata: Optional[Dict[str, Any]] = None
) -> str:
"""
Transform a Ring command to Cursor command format.
@ -180,28 +188,36 @@ class CursorAdapter(PlatformAdapter):
parts.append(f"/{cmd_name}")
parts.append("")
raw_args = frontmatter.get("args", [])
if isinstance(raw_args, dict):
args: List[Any] = [raw_args]
elif isinstance(raw_args, list):
args = raw_args
else:
args = []
if args:
# Handle argument-hint (new schema) or args (legacy)
arg_hint = self._as_text(frontmatter.get("argument-hint", ""), "")
if arg_hint:
parts.append("## Parameters")
parts.append("")
for arg in args:
if not isinstance(arg, dict):
continue
arg_name = self._as_text(arg.get("name", ""), "")
arg_desc = self._as_text(arg.get("description", ""), "")
required = "required" if arg.get("required", False) else "optional"
param_line = f"- **{arg_name}** ({required})"
if arg_desc:
param_line += f": {arg_desc}"
parts.append(param_line)
parts.append(f"Usage: `/{cmd_name} {arg_hint}`")
parts.append("")
else:
raw_args = frontmatter.get("args", [])
if isinstance(raw_args, dict):
args: List[Any] = [raw_args]
elif isinstance(raw_args, list):
args = raw_args
else:
args = []
if args:
parts.append("## Parameters")
parts.append("")
for arg in args:
if not isinstance(arg, dict):
continue
arg_name = self._as_text(arg.get("name", ""), "")
arg_desc = self._as_text(arg.get("description", ""), "")
required = "required" if arg.get("required", False) else "optional"
param_line = f"- **{arg_name}** ({required})"
if arg_desc:
param_line += f": {arg_desc}"
parts.append(param_line)
parts.append("")
parts.append("## Steps")
parts.append("")
@ -229,18 +245,9 @@ class CursorAdapter(PlatformAdapter):
Mapping of Ring components to Cursor directories
"""
return {
"agents": {
"target_dir": "agents",
"extension": ".md"
},
"commands": {
"target_dir": "commands",
"extension": ".md"
},
"skills": {
"target_dir": "skills",
"extension": ".md"
}
"agents": {"target_dir": "agents", "extension": ".md"},
"commands": {"target_dir": "commands", "extension": ".md"},
"skills": {"target_dir": "skills", "extension": ".md"},
}
def get_terminology(self) -> Dict[str, str]:
@ -250,12 +257,7 @@ class CursorAdapter(PlatformAdapter):
Returns:
Mapping of Ring terms to Cursor terms
"""
return {
"agent": "agent",
"skill": "skill",
"command": "command",
"hook": "automation"
}
return {"agent": "agent", "skill": "skill", "command": "command", "hook": "automation"}
def is_native_format(self) -> bool:
"""
@ -292,7 +294,7 @@ class CursorAdapter(PlatformAdapter):
Cleaned string
"""
# Remove | and > markers
text = re.sub(r'^[|>]\s*', '', text)
text = re.sub(r"^[|>]\s*", "", text)
# Clean up extra whitespace
return text.strip()
@ -311,7 +313,9 @@ class CursorAdapter(PlatformAdapter):
result = result.replace(old, new)
# Remove Ring-specific tool references that don't apply
result = re.sub(r'`ring:[^`]+`', lambda m: self._transform_ring_reference(m.group(0)), result)
result = re.sub(
r"`ring:[^`]+`", lambda m: self._transform_ring_reference(m.group(0)), result
)
# Normalize /ring: command references for all component types
result = result.replace("/ring:", "/")
@ -329,7 +333,7 @@ class CursorAdapter(PlatformAdapter):
Cursor-friendly reference
"""
# Extract the name from the reference
match = re.match(r'`ring:([^`]+)`', ref)
match = re.match(r"`ring:([^`]+)`", ref)
if match:
name = match.group(1)
# Convert to readable format

View file

@ -129,9 +129,7 @@ class FactoryAdapter(PlatformAdapter):
return body
def _qualify_droid_name(
self,
frontmatter: Dict[str, Any],
metadata: Optional[Dict[str, Any]]
self, frontmatter: Dict[str, Any], metadata: Optional[Dict[str, Any]]
) -> Dict[str, Any]:
"""Qualify droid name with plugin namespace.
@ -172,7 +170,9 @@ class FactoryAdapter(PlatformAdapter):
result["name"] = f"{plugin_id}-{name}"
return result
def transform_command(self, command_content: str, metadata: Optional[Dict[str, Any]] = None) -> str:
def transform_command(
self, command_content: str, metadata: Optional[Dict[str, Any]] = None
) -> str:
"""
Transform a Ring command for Factory AI.
@ -213,14 +213,8 @@ class FactoryAdapter(PlatformAdapter):
"""
result = dict(frontmatter)
# Map 'args' or 'arguments' to 'argument-hint'
if "args" in result and "argument-hint" not in result:
result["argument-hint"] = result.pop("args")
elif "arguments" in result and "argument-hint" not in result:
result["argument-hint"] = result.pop("arguments")
# Remove fields Factory doesn't use for commands
for field in ["name", "version", "type", "tags"]:
for field in ["name", "version", "type", "tags", "args", "arguments"]:
result.pop(field, None)
# Transform any agent terminology in string values
@ -230,7 +224,9 @@ class FactoryAdapter(PlatformAdapter):
return result
def transform_hook(self, hook_content: str, metadata: Optional[Dict[str, Any]] = None) -> Optional[str]:
def transform_hook(
self, hook_content: str, metadata: Optional[Dict[str, Any]] = None
) -> Optional[str]:
"""
Transform a Ring hook for Factory AI.
@ -252,7 +248,7 @@ class FactoryAdapter(PlatformAdapter):
# Also handle any remaining references
result = result.replace("${CLAUDE_PLUGIN_ROOT}", "~/.factory")
result = result.replace("$CLAUDE_PLUGIN_ROOT", "~/.factory")
return result
def get_install_path(self) -> Path:
@ -273,6 +269,7 @@ class FactoryAdapter(PlatformAdapter):
env_path = candidate
except ValueError:
import logging
logging.getLogger(__name__).warning(
"FACTORY_CONFIG_PATH=%s ignored: path must be under home", override
)
@ -287,22 +284,13 @@ class FactoryAdapter(PlatformAdapter):
Mapping of Ring components to Factory AI directories
"""
return {
"agents": {
"target_dir": "droids",
"extension": ".md"
},
"commands": {
"target_dir": "commands",
"extension": ".md"
},
"skills": {
"target_dir": "skills",
"extension": ".md"
},
"agents": {"target_dir": "droids", "extension": ".md"},
"commands": {"target_dir": "commands", "extension": ".md"},
"skills": {"target_dir": "skills", "extension": ".md"},
"hooks": {
"target_dir": "hooks",
"extension": "" # Multiple extensions supported
}
"extension": "", # Multiple extensions supported
},
}
def requires_hooks_in_settings(self) -> bool:
@ -332,7 +320,7 @@ class FactoryAdapter(PlatformAdapter):
self,
hooks_config: Dict[str, Any],
dry_run: bool = False,
install_path: Optional[Path] = None
install_path: Optional[Path] = None,
) -> bool:
"""
Merge hooks configuration into Factory's settings.json.
@ -351,7 +339,7 @@ class FactoryAdapter(PlatformAdapter):
"""
import json
import logging
logger = logging.getLogger(__name__)
base_path = install_path or self.get_install_path()
settings_path = base_path / "settings.json"
@ -411,19 +399,14 @@ class FactoryAdapter(PlatformAdapter):
try:
# Write settings back
settings_path.write_text(
json.dumps(existing_settings, indent=2),
encoding="utf-8"
)
settings_path.write_text(json.dumps(existing_settings, indent=2), encoding="utf-8")
return True
except Exception as e:
logger.error(f"Failed to write settings.json: {e}")
return False
def _transform_hook_entry(
self,
hook_entry: Dict[str, Any],
install_path: Path
self, hook_entry: Dict[str, Any], install_path: Path
) -> Dict[str, Any]:
"""
Transform a hook entry's commands for Factory compatibility.
@ -454,14 +437,8 @@ class FactoryAdapter(PlatformAdapter):
cmd = transformed_cmd["command"]
# Transform Claude plugin paths to Factory absolute paths
# Use absolute path with ~ expansion for portability
cmd = cmd.replace(
"${CLAUDE_PLUGIN_ROOT}/hooks/",
f"{hooks_path}/"
)
cmd = cmd.replace(
"$CLAUDE_PLUGIN_ROOT/hooks/",
f"{hooks_path}/"
)
cmd = cmd.replace("${CLAUDE_PLUGIN_ROOT}/hooks/", f"{hooks_path}/")
cmd = cmd.replace("$CLAUDE_PLUGIN_ROOT/hooks/", f"{hooks_path}/")
# Handle any remaining plugin root references
cmd = cmd.replace("${CLAUDE_PLUGIN_ROOT}", str(install_path))
cmd = cmd.replace("$CLAUDE_PLUGIN_ROOT", str(install_path))
@ -494,12 +471,7 @@ class FactoryAdapter(PlatformAdapter):
Returns:
Mapping of Ring terms to Factory AI terms
"""
return {
"agent": "droid",
"skill": "skill",
"command": "command",
"hook": "trigger"
}
return {"agent": "droid", "skill": "skill", "command": "command", "hook": "trigger"}
def is_native_format(self) -> bool:
"""
@ -541,8 +513,7 @@ class FactoryAdapter(PlatformAdapter):
result[key] = self._replace_agent_references(value)
elif isinstance(value, list):
result[key] = [
self._replace_agent_references(v) if isinstance(v, str) else v
for v in value
self._replace_agent_references(v) if isinstance(v, str) else v for v in value
]
return result
@ -713,7 +684,10 @@ class FactoryAdapter(PlatformAdapter):
# NOTE: Factory droid names use hyphens, not colons (colons reserved for custom: prefix)
ring_contexts = [
# Task tool subagent_type references: ring-plugin:name -> ring-plugin-name
(r'subagent_type["\s]*[:=]["\s]*["\']?ring-([^:]+):([^"\'>\s]+)', r'subagent_type="\1-\2'),
(
r'subagent_type["\s]*[:=]["\s]*["\']?ring-([^:]+):([^"\'>\s]+)',
r'subagent_type="\1-\2',
),
(r'"ring-([^:]+):([^"]+)"', r'"ring-\1-\2"'),
(r"'ring-([^:]+):([^']+)'", r"'ring-\1-\2'"),
# Tool references with -agent suffix
@ -721,8 +695,8 @@ class FactoryAdapter(PlatformAdapter):
(r"'ring:([^']*)-agent'", r"'ring-\1-droid'"),
# Don't rename subagent_type field name - Factory Task tool uses it
# Only transform subagent -> subdroid in prose
(r'\bsubagent\b(?!_type)', 'subdroid'),
(r'\bSubagent\b(?!_type)', 'Subdroid'),
(r"\bsubagent\b(?!_type)", "subdroid"),
(r"\bSubagent\b(?!_type)", "Subdroid"),
]
result = masked
@ -732,12 +706,12 @@ class FactoryAdapter(PlatformAdapter):
# General agent terminology (with exclusions)
general_replacements = [
# Skip "user agent" and similar patterns
(r'\b(?<!user\s)(?<!User\s)(?<!USER\s)agent\b(?!\s+string)(?!\s+header)', 'droid'),
(r'\b(?<!user\s)(?<!User\s)(?<!USER\s)Agent\b(?!\s+string)(?!\s+header)', 'Droid'),
(r'\bAGENT\b(?!\s+STRING)(?!\s+HEADER)', 'DROID'),
(r'\b(?<!user\s)(?<!User\s)(?<!USER\s)agents\b(?!\s+strings)(?!\s+headers)', 'droids'),
(r'\b(?<!user\s)(?<!User\s)(?<!USER\s)Agents\b(?!\s+strings)(?!\s+headers)', 'Droids'),
(r'\bAGENTS\b(?!\s+STRINGS)(?!\s+HEADERS)', 'DROIDS'),
(r"\b(?<!user\s)(?<!User\s)(?<!USER\s)agent\b(?!\s+string)(?!\s+header)", "droid"),
(r"\b(?<!user\s)(?<!User\s)(?<!USER\s)Agent\b(?!\s+string)(?!\s+header)", "Droid"),
(r"\bAGENT\b(?!\s+STRING)(?!\s+HEADER)", "DROID"),
(r"\b(?<!user\s)(?<!User\s)(?<!USER\s)agents\b(?!\s+strings)(?!\s+headers)", "droids"),
(r"\b(?<!user\s)(?<!User\s)(?<!USER\s)Agents\b(?!\s+strings)(?!\s+headers)", "Droids"),
(r"\bAGENTS\b(?!\s+STRINGS)(?!\s+HEADERS)", "DROIDS"),
]
for pattern, replacement in general_replacements:
@ -764,8 +738,8 @@ class FactoryAdapter(PlatformAdapter):
# Remove -agent suffix (Factory uses the name field, not filename suffix)
if component_type == "agent":
filename = re.sub(r'-agent\.md$', '.md', filename)
filename = re.sub(r'_agent\.md$', '.md', filename)
filename = re.sub(r"-agent\.md$", ".md", filename)
filename = re.sub(r"_agent\.md$", ".md", filename)
return filename
@ -776,7 +750,7 @@ class FactoryAdapter(PlatformAdapter):
Factory/Droid only scans top-level .md files in:
- ~/.factory/droids/ (agents)
- ~/.factory/commands/ (commands)
Skills use ~/.factory/skills/<name>/SKILL.md structure.
Args:
@ -815,8 +789,8 @@ class FactoryAdapter(PlatformAdapter):
# For agents/droids, remove -agent suffix and add prefix (no -droid suffix needed)
# Factory expects filename to match the name field exactly
if component_type == "agent":
stem = re.sub(r'-agent$', '', stem)
stem = re.sub(r'_agent$', '', stem)
stem = re.sub(r"-agent$", "", stem)
stem = re.sub(r"_agent$", "", stem)
return f"ring-{plugin_name}-{stem}.md"
# For other component types, just add prefix

View file

@ -87,9 +87,6 @@ class OpenCodeAdapter(PlatformAdapter):
_OPENCODE_SKILL_ALLOWED_FIELDS: List[str] = [
"name", # Required: skill identifier
"description", # Optional: displayed in skill list
"license", # Optional: license identifier (e.g., "MIT")
"compatibility", # Optional: version constraints
"metadata", # Optional: arbitrary key-value metadata
]
# OpenCode agent allowed frontmatter fields

View file

@ -22,6 +22,7 @@ if str(INSTALLER_ROOT) not in sys.path:
# Path Fixtures
# ==============================================================================
@pytest.fixture
def fixtures_path() -> Path:
"""
@ -81,15 +82,15 @@ def tmp_ring_root(tmp_path: Path, fixtures_path: Path) -> Path:
"name": "ring-default",
"description": "Core Ring plugin",
"version": "1.0.0",
"source": "./default"
"source": "./default",
},
{
"name": "ring-test",
"description": "Test plugin",
"version": "0.1.0",
"source": "./test-plugin"
}
]
"source": "./test-plugin",
},
],
}
with open(marketplace_dir / "marketplace.json", "w") as f:
@ -137,6 +138,7 @@ def tmp_install_dir(tmp_path: Path) -> Path:
# Content Fixtures
# ==============================================================================
@pytest.fixture
def sample_skill_content(fixtures_path: Path) -> str:
"""
@ -202,6 +204,7 @@ def sample_hooks_dict(fixtures_path: Path) -> Dict[str, Any]:
# Minimal Content Fixtures (for unit tests)
# ==============================================================================
@pytest.fixture
def minimal_skill_content() -> str:
"""
@ -252,9 +255,7 @@ def minimal_command_content() -> str:
return """---
name: minimal-command
description: A minimal command for testing.
args:
- name: target
required: true
argument-hint: "[target]"
---
# Minimal Command
@ -304,6 +305,7 @@ The frontmatter YAML is malformed.
# Adapter Fixtures
# ==============================================================================
@pytest.fixture
def mock_platform_adapter():
"""
@ -350,10 +352,7 @@ def claude_adapter_config() -> Dict[str, Any]:
Returns:
Claude adapter configuration dictionary.
"""
return {
"install_path": "~/.claude",
"native": True
}
return {"install_path": "~/.claude", "native": True}
@pytest.fixture
@ -364,10 +363,7 @@ def factory_adapter_config() -> Dict[str, Any]:
Returns:
Factory adapter configuration dictionary.
"""
return {
"install_path": "~/.factory",
"native": False
}
return {"install_path": "~/.factory", "native": False}
@pytest.fixture
@ -378,10 +374,7 @@ def codex_adapter_config() -> Dict[str, Any]:
Returns:
Codex adapter configuration dictionary.
"""
return {
"install_path": "~/.codex",
"native": True
}
return {"install_path": "~/.codex", "native": True}
@pytest.fixture
@ -392,10 +385,7 @@ def cursor_adapter_config() -> Dict[str, Any]:
Returns:
Cursor adapter configuration dictionary.
"""
return {
"install_path": "~/.cursor",
"native": False
}
return {"install_path": "~/.cursor", "native": False}
@pytest.fixture
@ -406,10 +396,7 @@ def cline_adapter_config() -> Dict[str, Any]:
Returns:
Cline adapter configuration dictionary.
"""
return {
"install_path": "~/.cline",
"native": False
}
return {"install_path": "~/.cline", "native": False}
@pytest.fixture
@ -420,16 +407,14 @@ def opencode_adapter_config() -> Dict[str, Any]:
Returns:
OpenCode adapter configuration dictionary.
"""
return {
"install_path": "~/.config/opencode",
"native": True
}
return {"install_path": "~/.config/opencode", "native": True}
# ==============================================================================
# Transformer Fixtures
# ==============================================================================
@pytest.fixture
def transform_context():
"""
@ -445,14 +430,14 @@ def transform_context():
component_type: str = "skill",
source_path: str = "",
metadata: Dict[str, Any] = None,
options: Dict[str, Any] = None
options: Dict[str, Any] = None,
) -> TransformContext:
return TransformContext(
platform=platform,
component_type=component_type,
source_path=source_path,
metadata=metadata or {},
options=options or {}
options=options or {},
)
return _create_context
@ -462,6 +447,7 @@ def transform_context():
# Platform Detection Fixtures
# ==============================================================================
@pytest.fixture
def mock_platform_detection():
"""
@ -470,45 +456,33 @@ def mock_platform_detection():
Yields:
Dictionary of mocked detection functions.
"""
with patch("ring_installer.utils.platform_detect._detect_claude") as mock_claude, \
patch("ring_installer.utils.platform_detect._detect_codex") as mock_codex, \
patch("ring_installer.utils.platform_detect._detect_factory") as mock_factory, \
patch("ring_installer.utils.platform_detect._detect_cursor") as mock_cursor, \
patch("ring_installer.utils.platform_detect._detect_cline") as mock_cline, \
patch("ring_installer.utils.platform_detect._detect_opencode") as mock_opencode:
with patch("ring_installer.utils.platform_detect._detect_claude") as mock_claude, patch(
"ring_installer.utils.platform_detect._detect_codex"
) as mock_codex, patch(
"ring_installer.utils.platform_detect._detect_factory"
) as mock_factory, patch(
"ring_installer.utils.platform_detect._detect_cursor"
) as mock_cursor, patch(
"ring_installer.utils.platform_detect._detect_cline"
) as mock_cline, patch(
"ring_installer.utils.platform_detect._detect_opencode"
) as mock_opencode:
from ring_installer.utils.platform_detect import PlatformInfo
# Default: no platforms installed
mock_claude.return_value = PlatformInfo(
platform_id="claude",
name="Claude Code",
installed=False
)
mock_codex.return_value = PlatformInfo(
platform_id="codex",
name="Codex",
installed=False
platform_id="claude", name="Claude Code", installed=False
)
mock_codex.return_value = PlatformInfo(platform_id="codex", name="Codex", installed=False)
mock_factory.return_value = PlatformInfo(
platform_id="factory",
name="Factory AI",
installed=False
platform_id="factory", name="Factory AI", installed=False
)
mock_cursor.return_value = PlatformInfo(
platform_id="cursor",
name="Cursor",
installed=False
)
mock_cline.return_value = PlatformInfo(
platform_id="cline",
name="Cline",
installed=False
platform_id="cursor", name="Cursor", installed=False
)
mock_cline.return_value = PlatformInfo(platform_id="cline", name="Cline", installed=False)
mock_opencode.return_value = PlatformInfo(
platform_id="opencode",
name="OpenCode",
installed=False
platform_id="opencode", name="OpenCode", installed=False
)
yield {
@ -517,7 +491,7 @@ def mock_platform_detection():
"factory": mock_factory,
"cursor": mock_cursor,
"cline": mock_cline,
"opencode": mock_opencode
"opencode": mock_opencode,
}
@ -525,6 +499,7 @@ def mock_platform_detection():
# Install Manifest Fixtures
# ==============================================================================
@pytest.fixture
def sample_install_manifest() -> Dict[str, Any]:
"""
@ -542,11 +517,9 @@ def sample_install_manifest() -> Dict[str, Any]:
"files": {
"agents/sample-agent.md": "abc123hash",
"commands/sample-command.md": "def456hash",
"skills/sample-skill/SKILL.md": "ghi789hash"
"skills/sample-skill/SKILL.md": "ghi789hash",
},
"metadata": {
"installer_version": "0.1.0"
}
"metadata": {"installer_version": "0.1.0"},
}
@ -558,6 +531,7 @@ def create_manifest_file(tmp_path: Path):
Returns:
Function to create manifest files in tmp_path.
"""
def _create(manifest_data: Dict[str, Any], filename: str = ".ring-manifest.json") -> Path:
manifest_path = tmp_path / filename
with open(manifest_path, "w") as f:
@ -571,6 +545,7 @@ def create_manifest_file(tmp_path: Path):
# Version Fixtures
# ==============================================================================
@pytest.fixture
def version_test_cases() -> list:
"""
@ -589,14 +564,12 @@ def version_test_cases() -> list:
("1.1.0", "1.0.0", 1),
("1.0.0", "2.0.0", -1),
("2.0.0", "1.0.0", 1),
# Prerelease versions
("1.0.0-alpha", "1.0.0", -1),
("1.0.0", "1.0.0-alpha", 1),
("1.0.0-alpha", "1.0.0-beta", -1),
("1.0.0-beta", "1.0.0-alpha", 1),
("1.0.0-alpha.1", "1.0.0-alpha.2", -1),
# With v prefix
("v1.0.0", "1.0.0", 0),
("v1.0.0", "v1.0.1", -1),
@ -607,6 +580,7 @@ def version_test_cases() -> list:
# Cleanup Fixtures
# ==============================================================================
@pytest.fixture(autouse=True)
def cleanup_temp_files(tmp_path: Path):
"""
@ -622,6 +596,7 @@ def cleanup_temp_files(tmp_path: Path):
# Helper Functions (not fixtures, but available in tests)
# ==============================================================================
def assert_frontmatter_contains(content: str, expected_keys: list) -> None:
"""
Assert that content has frontmatter containing expected keys.

View file

@ -3,18 +3,7 @@ name: sample-command
description: |
A sample slash command for testing platform transformations.
args:
- name: target
description: The target file or directory to process
required: true
- name: format
description: Output format (json, yaml, markdown)
required: false
default: markdown
- name: verbose
description: Enable verbose output
required: false
default: false
argument-hint: "[target] [--format=markdown] [--verbose]"
---
# Sample Command

View file

@ -4,12 +4,7 @@ description: |
External research specialist for pre-dev planning. Searches web and documentation
for industry best practices, open source examples, and authoritative guidance.
Primary agent for greenfield features where codebase patterns don't exist.
tools:
- WebSearch
- WebFetch
- mcp__context7__resolve-library-id
- mcp__context7__get-library-docs
type: specialist
output_schema:
format: "markdown"
@ -30,7 +25,6 @@ output_schema:
pattern: "^## EXTERNAL REFERENCES$"
required: true
version: 1.2.0
---
# Best Practices Researcher

View file

@ -4,14 +4,7 @@ description: |
Tech stack analysis specialist for pre-dev planning. Detects project tech stack
from manifest files and fetches relevant framework/library documentation.
Identifies version constraints and implementation patterns from official docs.
tools:
- Glob
- Grep
- Read
- mcp__context7__resolve-library-id
- mcp__context7__get-library-docs
- WebFetch
type: specialist
output_schema:
format: "markdown"
@ -32,7 +25,6 @@ output_schema:
pattern: "^## VERSION CONSIDERATIONS$"
required: true
version: 1.2.0
---
# Framework Docs Researcher

View file

@ -4,12 +4,7 @@ description: |
Product Designer agent for UX research, user validation, and design specifications.
Accepts feature context and research findings. Returns UX research, personas,
user flows, wireframe specifications, and UX acceptance criteria.
tools:
- WebSearch
- WebFetch
- Read
- Glob
- Grep
type: specialist
output_schema:
format: "markdown"
@ -42,7 +37,6 @@ output_schema:
pattern: "^## RECOMMENDATIONS$"
required: true
version: 1.0.0
---
# Product Designer

View file

@ -4,12 +4,7 @@ description: |
Codebase research specialist for pre-dev planning. Searches target repository
for existing patterns, conventions, and prior solutions. Returns findings with
exact file:line references for use in PRD/TRD creation.
tools:
- Glob
- Grep
- Read
- Task
type: analyst
output_schema:
format: "markdown"
@ -30,7 +25,6 @@ output_schema:
pattern: "^## RECOMMENDATIONS$"
required: true
version: 1.2.0
---
# Repo Research Analyst

View file

@ -30,18 +30,6 @@ related:
- ring:pre-dev-trd-creation
- ring:pre-dev-task-breakdown
- ring:pre-dev-delivery-planning
user_invocable: false
allowed-tools:
- Skill
- Read
- Write
- Glob
- Grep
- Bash
- Agent
- AskUserQuestion
---
# Small Track Pre-Dev Workflow (5 Gates)

View file

@ -35,18 +35,6 @@ related:
- ring:pre-dev-task-breakdown
- ring:pre-dev-subtask-creation
- ring:pre-dev-delivery-planning
user_invocable: false
allowed-tools:
- Skill
- Read
- Write
- Glob
- Grep
- Bash
- Agent
- AskUserQuestion
---
# Full Track Pre-Dev Workflow (10 Gates)

View file

@ -21,23 +21,6 @@ sequence:
related:
complementary: [ring:pre-dev-prd-creation, ring:pre-dev-trd-creation]
research_modes:
greenfield:
description: "New feature with no existing patterns to follow"
primary_agents: [ring:best-practices-researcher, ring:framework-docs-researcher, ring:product-designer]
focus: "External best practices, framework patterns, and user problem validation"
modification:
description: "Changing or extending existing functionality"
primary_agents: [ring:repo-research-analyst]
secondary_agents: [ring:product-designer]
focus: "Existing codebase patterns and conventions, UX impact assessment"
integration:
description: "Connecting systems or adding external dependencies"
primary_agents: [ring:framework-docs-researcher, ring:best-practices-researcher, ring:repo-research-analyst]
secondary_agents: [ring:product-designer]
focus: "API documentation, integration patterns, and user experience considerations"
---
# Pre-Dev Research Skill (Gate 0)

View file

@ -1,13 +1,8 @@
---
title: AI Agent Baseline Definition
description: |
Canonical source for AI-agent-hours definition across pm-team skills.
Defines baseline execution model, capacity utilization, and usage patterns.
category: shared-pattern
tags: [ai-estimation, baseline, capacity, calibration]
referenced_by:
- ring:pre-dev-task-breakdown (Gate 7)
- ring:pre-dev-delivery-planning (Gate 9)
Referenced by: ring:pre-dev-task-breakdown (Gate 7), ring:pre-dev-delivery-planning (Gate 9).
---
# AI Agent Baseline Definition

View file

@ -1,6 +1,5 @@
---
name: delivery-reporter
version: 1.1.0
name: ring:delivery-reporter
description: Delivery Reporting Specialist for analyzing Git repositories and creating visual executive presentations of squad deliveries. Extracts business value from technical changes (releases, PRs, commits) and generates HTML slide presentations with customizable visual identity.
type: specialist
output_schema:

View file

@ -1,6 +1,5 @@
---
name: ring:executive-reporter
version: 1.3.0
description: Executive Reporting Specialist for creating dashboards, status summaries, board packages, and stakeholder communications. Focuses on actionable insights for leadership.
type: specialist
output_schema:

View file

@ -1,6 +1,5 @@
---
name: ring:governance-specialist
version: 1.2.0
description: Project Governance Specialist for gate reviews, process compliance, audit readiness, and governance framework implementation across portfolio projects.
type: specialist
output_schema:

View file

@ -1,6 +1,5 @@
---
name: ring:portfolio-manager
version: 1.2.0
description: Senior Portfolio Manager specialized in multi-project coordination, strategic alignment assessment, and portfolio optimization. Handles portfolio-level planning, prioritization, and health monitoring.
type: specialist
output_schema:

View file

@ -1,6 +1,5 @@
---
name: ring:resource-planner
version: 1.1.0
description: Resource Planning Specialist for capacity planning, allocation optimization, skills management, and conflict resolution across portfolio projects.
type: specialist
output_schema:

View file

@ -1,6 +1,5 @@
---
name: ring:risk-analyst
version: 1.1.0
description: Portfolio Risk Analyst specialized in risk identification, assessment, correlation analysis, and mitigation planning across portfolio projects. Manages RAID logs and portfolio risk exposure.
type: specialist
output_schema:

View file

@ -1,6 +1,5 @@
---
name: ring:api-writer
version: 0.3.0
description: Senior Technical Writer specialized in API reference documentation including endpoint descriptions, request/response schemas, and error documentation.
type: specialist
output_schema:

View file

@ -1,6 +1,5 @@
---
name: ring:docs-reviewer
version: 0.3.0
description: Documentation Quality Reviewer specialized in checking voice, tone, structure, completeness, and technical accuracy of documentation.
type: reviewer
output_schema:

View file

@ -1,6 +1,5 @@
---
name: ring:functional-writer
version: 0.3.0
description: Senior Technical Writer specialized in functional documentation including guides, conceptual explanations, tutorials, and best practices.
type: specialist
output_schema:

View file

@ -2,10 +2,6 @@
name: ring:review-docs
description: Review existing documentation for quality, voice, tone, and completeness
argument-hint: "[file]"
arguments:
- name: file
description: Path to the documentation file to review
required: false
---
# Review Documentation Command

View file

@ -2,10 +2,6 @@
name: ring:write-api
description: Start writing API reference documentation for an endpoint
argument-hint: "[endpoint]"
arguments:
- name: endpoint
description: The API endpoint to document (e.g., POST /accounts)
required: true
---
# Write API Reference Command

View file

@ -2,10 +2,6 @@
name: ring:write-guide
description: Start writing a functional guide with voice, tone, and structure guidance
argument-hint: "[topic]"
arguments:
- name: topic
description: The topic or feature to document
required: true
---
# Write Guide Command