mirror of
https://github.com/LerianStudio/ring
synced 2026-04-21 13:37:27 +00:00
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:
parent
102575dc7c
commit
fa6c4c87e8
100 changed files with 1810 additions and 1033 deletions
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
0
default/hooks/tests/__init__.py
Normal file
0
default/hooks/tests/__init__.py
Normal file
336
default/hooks/tests/test_generate_skills_ref.py
Normal file
336
default/hooks/tests/test_generate_skills_ref.py
Normal 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
|
||||
245
default/hooks/tests/test_validate_frontmatter.py
Normal file
245
default/hooks/tests/test_validate_frontmatter.py
Normal 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
|
||||
483
default/hooks/validate-frontmatter.py
Executable file
483
default/hooks/validate-frontmatter.py
Executable 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())
|
||||
|
|
@ -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.
|
||||
---
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
186
docs/FRONTMATTER_SCHEMA.md
Normal 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
|
||||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in a new issue