@iamnishankhan/opencode-kit-grok 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (198) hide show
  1. package/bin/cli.js +46 -0
  2. package/kit/.opencode/AGENTS.md +392 -0
  3. package/kit/.opencode/README.md +71 -0
  4. package/kit/.opencode/USAGE.md +405 -0
  5. package/kit/.opencode/agents/backend-specialist.md +278 -0
  6. package/kit/.opencode/agents/code-archaeologist.md +114 -0
  7. package/kit/.opencode/agents/database-architect.md +233 -0
  8. package/kit/.opencode/agents/debugger.md +234 -0
  9. package/kit/.opencode/agents/devops-engineer.md +252 -0
  10. package/kit/.opencode/agents/documentation-writer.md +111 -0
  11. package/kit/.opencode/agents/explorer-agent.md +83 -0
  12. package/kit/.opencode/agents/frontend-specialist.md +606 -0
  13. package/kit/.opencode/agents/game-developer.md +169 -0
  14. package/kit/.opencode/agents/mobile-developer.md +385 -0
  15. package/kit/.opencode/agents/orchestrator.md +213 -0
  16. package/kit/.opencode/agents/penetration-tester.md +197 -0
  17. package/kit/.opencode/agents/performance-optimizer.md +194 -0
  18. package/kit/.opencode/agents/product-manager.md +120 -0
  19. package/kit/.opencode/agents/product-owner.md +103 -0
  20. package/kit/.opencode/agents/project-planner.md +418 -0
  21. package/kit/.opencode/agents/qa-automation-engineer.md +113 -0
  22. package/kit/.opencode/agents/security-auditor.md +179 -0
  23. package/kit/.opencode/agents/seo-specialist.md +119 -0
  24. package/kit/.opencode/agents/test-engineer.md +169 -0
  25. package/kit/.opencode/commands/brainstorm.md +113 -0
  26. package/kit/.opencode/commands/coordinate.md +72 -0
  27. package/kit/.opencode/commands/create.md +63 -0
  28. package/kit/.opencode/commands/debug.md +102 -0
  29. package/kit/.opencode/commands/deploy.md +175 -0
  30. package/kit/.opencode/commands/enhance.md +62 -0
  31. package/kit/.opencode/commands/orchestrate.md +241 -0
  32. package/kit/.opencode/commands/plan.md +89 -0
  33. package/kit/.opencode/commands/preview.md +80 -0
  34. package/kit/.opencode/commands/remember.md +67 -0
  35. package/kit/.opencode/commands/status.md +86 -0
  36. package/kit/.opencode/commands/test.md +143 -0
  37. package/kit/.opencode/commands/verify.md +71 -0
  38. package/kit/.opencode/memory/MEMORY.md +6 -0
  39. package/kit/.opencode/memory/feedback-history.md +9 -0
  40. package/kit/.opencode/memory/project-conventions.md +16 -0
  41. package/kit/.opencode/memory/tech-decisions.md +10 -0
  42. package/kit/.opencode/memory/user-preferences.md +9 -0
  43. package/kit/.opencode/opencode.json +15 -0
  44. package/kit/.opencode/scripts/README.md +95 -0
  45. package/kit/.opencode/scripts/auto_preview.py +149 -0
  46. package/kit/.opencode/scripts/checklist.py +75 -0
  47. package/kit/.opencode/scripts/component_registry.py +238 -0
  48. package/kit/.opencode/scripts/dependency_graph.py +73 -0
  49. package/kit/.opencode/scripts/generate_manifest.py +44 -0
  50. package/kit/.opencode/scripts/session_manager.py +120 -0
  51. package/kit/.opencode/scripts/tests/test_toolkit.py +238 -0
  52. package/kit/.opencode/scripts/validate_kit.py +397 -0
  53. package/kit/.opencode/scripts/validation_runner.py +216 -0
  54. package/kit/.opencode/scripts/verify_all.py +85 -0
  55. package/kit/.opencode/skills/api-patterns/SKILL.md +83 -0
  56. package/kit/.opencode/skills/api-patterns/api-style.md +42 -0
  57. package/kit/.opencode/skills/api-patterns/auth.md +24 -0
  58. package/kit/.opencode/skills/api-patterns/documentation.md +26 -0
  59. package/kit/.opencode/skills/api-patterns/graphql.md +41 -0
  60. package/kit/.opencode/skills/api-patterns/rate-limiting.md +31 -0
  61. package/kit/.opencode/skills/api-patterns/response.md +37 -0
  62. package/kit/.opencode/skills/api-patterns/rest.md +40 -0
  63. package/kit/.opencode/skills/api-patterns/scripts/api_validator.py +211 -0
  64. package/kit/.opencode/skills/api-patterns/security-testing.md +122 -0
  65. package/kit/.opencode/skills/api-patterns/trpc.md +41 -0
  66. package/kit/.opencode/skills/api-patterns/versioning.md +22 -0
  67. package/kit/.opencode/skills/app-builder/SKILL.md +78 -0
  68. package/kit/.opencode/skills/app-builder/agent-coordination.md +71 -0
  69. package/kit/.opencode/skills/app-builder/feature-building.md +53 -0
  70. package/kit/.opencode/skills/app-builder/project-detection.md +45 -0
  71. package/kit/.opencode/skills/app-builder/scaffolding.md +110 -0
  72. package/kit/.opencode/skills/app-builder/tech-stack.md +41 -0
  73. package/kit/.opencode/skills/app-builder/templates/SKILL.md +39 -0
  74. package/kit/.opencode/skills/app-builder/templates/astro-static/TEMPLATE.md +78 -0
  75. package/kit/.opencode/skills/app-builder/templates/chrome-extension/TEMPLATE.md +96 -0
  76. package/kit/.opencode/skills/app-builder/templates/cli-tool/TEMPLATE.md +88 -0
  77. package/kit/.opencode/skills/app-builder/templates/electron-desktop/TEMPLATE.md +97 -0
  78. package/kit/.opencode/skills/app-builder/templates/express-api/TEMPLATE.md +89 -0
  79. package/kit/.opencode/skills/app-builder/templates/flutter-app/TEMPLATE.md +93 -0
  80. package/kit/.opencode/skills/app-builder/templates/monorepo-turborepo/TEMPLATE.md +97 -0
  81. package/kit/.opencode/skills/app-builder/templates/nextjs-fullstack/TEMPLATE.md +126 -0
  82. package/kit/.opencode/skills/app-builder/templates/nextjs-saas/TEMPLATE.md +125 -0
  83. package/kit/.opencode/skills/app-builder/templates/nextjs-static/TEMPLATE.md +174 -0
  84. package/kit/.opencode/skills/app-builder/templates/nuxt-app/TEMPLATE.md +127 -0
  85. package/kit/.opencode/skills/app-builder/templates/python-fastapi/TEMPLATE.md +94 -0
  86. package/kit/.opencode/skills/app-builder/templates/react-native-app/TEMPLATE.md +121 -0
  87. package/kit/.opencode/skills/architecture/SKILL.md +57 -0
  88. package/kit/.opencode/skills/architecture/context-discovery.md +43 -0
  89. package/kit/.opencode/skills/architecture/examples.md +94 -0
  90. package/kit/.opencode/skills/architecture/pattern-selection.md +68 -0
  91. package/kit/.opencode/skills/architecture/patterns-reference.md +50 -0
  92. package/kit/.opencode/skills/architecture/trade-off-analysis.md +77 -0
  93. package/kit/.opencode/skills/bash-linux/SKILL.md +201 -0
  94. package/kit/.opencode/skills/batch-operations/SKILL.md +105 -0
  95. package/kit/.opencode/skills/behavioral-modes/SKILL.md +253 -0
  96. package/kit/.opencode/skills/brainstorming/SKILL.md +178 -0
  97. package/kit/.opencode/skills/brainstorming/dynamic-questioning.md +350 -0
  98. package/kit/.opencode/skills/clean-code/SKILL.md +201 -0
  99. package/kit/.opencode/skills/code-review-checklist/SKILL.md +111 -0
  100. package/kit/.opencode/skills/code-review-graph/SKILL.md +305 -0
  101. package/kit/.opencode/skills/context-compression/SKILL.md +145 -0
  102. package/kit/.opencode/skills/coordinator-mode/SKILL.md +208 -0
  103. package/kit/.opencode/skills/database-design/SKILL.md +54 -0
  104. package/kit/.opencode/skills/database-design/database-selection.md +43 -0
  105. package/kit/.opencode/skills/database-design/indexing.md +39 -0
  106. package/kit/.opencode/skills/database-design/migrations.md +48 -0
  107. package/kit/.opencode/skills/database-design/optimization.md +36 -0
  108. package/kit/.opencode/skills/database-design/orm-selection.md +30 -0
  109. package/kit/.opencode/skills/database-design/schema-design.md +56 -0
  110. package/kit/.opencode/skills/database-design/scripts/schema_validator.py +172 -0
  111. package/kit/.opencode/skills/deployment-procedures/SKILL.md +243 -0
  112. package/kit/.opencode/skills/design-spec/SKILL.md +185 -0
  113. package/kit/.opencode/skills/design-spec/collection.md +106 -0
  114. package/kit/.opencode/skills/documentation-templates/SKILL.md +196 -0
  115. package/kit/.opencode/skills/frontend-architecture/SKILL.md +245 -0
  116. package/kit/.opencode/skills/frontend-design/SKILL.md +1222 -0
  117. package/kit/.opencode/skills/frontend-design/redesign.md +178 -0
  118. package/kit/.opencode/skills/frontend-design/scripts/accessibility_checker.py +145 -0
  119. package/kit/.opencode/skills/frontend-design/scripts/ux_audit.py +722 -0
  120. package/kit/.opencode/skills/frontend-design/style-brutalist.md +92 -0
  121. package/kit/.opencode/skills/frontend-design/style-minimalist.md +85 -0
  122. package/kit/.opencode/skills/game-development/2d-games/SKILL.md +118 -0
  123. package/kit/.opencode/skills/game-development/3d-games/SKILL.md +134 -0
  124. package/kit/.opencode/skills/game-development/SKILL.md +169 -0
  125. package/kit/.opencode/skills/game-development/game-art/SKILL.md +184 -0
  126. package/kit/.opencode/skills/game-development/game-audio/SKILL.md +189 -0
  127. package/kit/.opencode/skills/game-development/game-design/SKILL.md +128 -0
  128. package/kit/.opencode/skills/game-development/mobile-games/SKILL.md +107 -0
  129. package/kit/.opencode/skills/game-development/multiplayer/SKILL.md +131 -0
  130. package/kit/.opencode/skills/game-development/pc-games/SKILL.md +143 -0
  131. package/kit/.opencode/skills/game-development/vr-ar/SKILL.md +122 -0
  132. package/kit/.opencode/skills/game-development/web-games/SKILL.md +149 -0
  133. package/kit/.opencode/skills/geo-fundamentals/SKILL.md +158 -0
  134. package/kit/.opencode/skills/geo-fundamentals/scripts/geo_checker.py +273 -0
  135. package/kit/.opencode/skills/i18n-localization/SKILL.md +156 -0
  136. package/kit/.opencode/skills/i18n-localization/scripts/i18n_checker.py +223 -0
  137. package/kit/.opencode/skills/intelligent-routing/SKILL.md +337 -0
  138. package/kit/.opencode/skills/lint-and-validate/SKILL.md +47 -0
  139. package/kit/.opencode/skills/lint-and-validate/scripts/lint_runner.py +184 -0
  140. package/kit/.opencode/skills/lint-and-validate/scripts/type_coverage.py +245 -0
  141. package/kit/.opencode/skills/mcp-builder/SKILL.md +205 -0
  142. package/kit/.opencode/skills/memory-system/SKILL.md +181 -0
  143. package/kit/.opencode/skills/mobile-design/SKILL.md +396 -0
  144. package/kit/.opencode/skills/mobile-design/decision-trees.md +516 -0
  145. package/kit/.opencode/skills/mobile-design/mobile-backend.md +491 -0
  146. package/kit/.opencode/skills/mobile-design/mobile-color-system.md +420 -0
  147. package/kit/.opencode/skills/mobile-design/mobile-debugging.md +122 -0
  148. package/kit/.opencode/skills/mobile-design/mobile-design-thinking.md +357 -0
  149. package/kit/.opencode/skills/mobile-design/mobile-navigation.md +458 -0
  150. package/kit/.opencode/skills/mobile-design/mobile-performance.md +767 -0
  151. package/kit/.opencode/skills/mobile-design/mobile-testing.md +356 -0
  152. package/kit/.opencode/skills/mobile-design/mobile-typography.md +433 -0
  153. package/kit/.opencode/skills/mobile-design/platform-android.md +666 -0
  154. package/kit/.opencode/skills/mobile-design/platform-ios.md +561 -0
  155. package/kit/.opencode/skills/mobile-design/scripts/mobile_audit.py +670 -0
  156. package/kit/.opencode/skills/mobile-design/touch-psychology.md +537 -0
  157. package/kit/.opencode/skills/nextjs-react-expert/1-async-eliminating-waterfalls.md +351 -0
  158. package/kit/.opencode/skills/nextjs-react-expert/2-bundle-bundle-size-optimization.md +240 -0
  159. package/kit/.opencode/skills/nextjs-react-expert/3-server-server-side-performance.md +490 -0
  160. package/kit/.opencode/skills/nextjs-react-expert/4-client-client-side-data-fetching.md +264 -0
  161. package/kit/.opencode/skills/nextjs-react-expert/5-rerender-re-render-optimization.md +581 -0
  162. package/kit/.opencode/skills/nextjs-react-expert/6-rendering-rendering-performance.md +432 -0
  163. package/kit/.opencode/skills/nextjs-react-expert/7-js-javascript-performance.md +684 -0
  164. package/kit/.opencode/skills/nextjs-react-expert/8-advanced-advanced-patterns.md +150 -0
  165. package/kit/.opencode/skills/nextjs-react-expert/9-cache-components.md +103 -0
  166. package/kit/.opencode/skills/nextjs-react-expert/SKILL.md +293 -0
  167. package/kit/.opencode/skills/nextjs-react-expert/scripts/convert_rules.py +222 -0
  168. package/kit/.opencode/skills/nextjs-react-expert/scripts/react_performance_checker.py +281 -0
  169. package/kit/.opencode/skills/nodejs-best-practices/SKILL.md +336 -0
  170. package/kit/.opencode/skills/parallel-agents/SKILL.md +192 -0
  171. package/kit/.opencode/skills/performance-profiling/SKILL.md +145 -0
  172. package/kit/.opencode/skills/performance-profiling/scripts/bundle_analyzer.py +120 -0
  173. package/kit/.opencode/skills/performance-profiling/scripts/lighthouse_audit.py +108 -0
  174. package/kit/.opencode/skills/plan-writing/SKILL.md +153 -0
  175. package/kit/.opencode/skills/powershell-windows/SKILL.md +169 -0
  176. package/kit/.opencode/skills/python-patterns/SKILL.md +443 -0
  177. package/kit/.opencode/skills/red-team-tactics/SKILL.md +201 -0
  178. package/kit/.opencode/skills/rust-pro/SKILL.md +177 -0
  179. package/kit/.opencode/skills/seo-fundamentals/SKILL.md +131 -0
  180. package/kit/.opencode/skills/seo-fundamentals/scripts/seo_checker.py +219 -0
  181. package/kit/.opencode/skills/server-management/SKILL.md +163 -0
  182. package/kit/.opencode/skills/simplify-code/SKILL.md +131 -0
  183. package/kit/.opencode/skills/skillify/SKILL.md +115 -0
  184. package/kit/.opencode/skills/systematic-debugging/SKILL.md +111 -0
  185. package/kit/.opencode/skills/tailwind-patterns/SKILL.md +271 -0
  186. package/kit/.opencode/skills/tdd-workflow/SKILL.md +151 -0
  187. package/kit/.opencode/skills/testing-patterns/SKILL.md +180 -0
  188. package/kit/.opencode/skills/testing-patterns/scripts/test_runner.py +219 -0
  189. package/kit/.opencode/skills/verify-changes/SKILL.md +127 -0
  190. package/kit/.opencode/skills/vulnerability-scanner/SKILL.md +278 -0
  191. package/kit/.opencode/skills/vulnerability-scanner/checklists.md +124 -0
  192. package/kit/.opencode/skills/vulnerability-scanner/scripts/dependency_analyzer.py +144 -0
  193. package/kit/.opencode/skills/vulnerability-scanner/scripts/security_scan.py +410 -0
  194. package/kit/.opencode/skills/web-design-guidelines/SKILL.md +56 -0
  195. package/kit/.opencode/skills/webapp-testing/SKILL.md +189 -0
  196. package/kit/.opencode/skills/webapp-testing/scripts/playwright_runner.py +123 -0
  197. package/lib/init.js +61 -0
  198. package/package.json +21 -0
@@ -0,0 +1,397 @@
1
+ #!/usr/bin/env python3
2
+ """Self-validate an OpenCode kit installation.
3
+
4
+ Checks machine-readable configuration, versioned frontmatter contracts,
5
+ cross references, generated registries, local Markdown links, memory schema,
6
+ Python syntax, and architecture inventory counts.
7
+ """
8
+ from __future__ import annotations
9
+
10
+ import argparse
11
+ import ast
12
+ import json
13
+ import re
14
+ import sys
15
+ from dataclasses import dataclass
16
+ from pathlib import Path
17
+ from urllib.parse import unquote
18
+
19
+ try:
20
+ import yaml # type: ignore
21
+ except ImportError: # pragma: no cover - optional enhancement
22
+ yaml = None
23
+
24
+ from component_registry import (
25
+ build_lock,
26
+ build_manifest,
27
+ canonical_json,
28
+ is_semver,
29
+ normalize_list,
30
+ version_satisfies,
31
+ )
32
+ from dependency_graph import render as render_dependency_graph
33
+
34
+
35
+ @dataclass
36
+ class Finding:
37
+ severity: str
38
+ code: str
39
+ file: str
40
+ line: int
41
+ message: str
42
+
43
+
44
+ REQUIRED_FIELDS = {
45
+ "agent": {"name", "description", "tools", "model", "skills", "version"},
46
+ "skill": {"name", "description", "when_to_use", "allowed-tools", "version"},
47
+ "workflow": {
48
+ "name",
49
+ "description",
50
+ "version",
51
+ "requires_agents",
52
+ "requires_skills",
53
+ "artifact_outputs",
54
+ },
55
+ "rule": {"name", "trigger", "version", "priority"},
56
+ }
57
+
58
+
59
+ def add(findings: list[Finding], severity: str, code: str, path: Path, message: str, line: int = 1) -> None:
60
+ findings.append(Finding(severity, code, path.as_posix(), line, message))
61
+
62
+
63
+ def extract_frontmatter(path: Path) -> tuple[str | None, int]:
64
+ text = path.read_text("utf-8", errors="replace")
65
+ if not text.startswith("---\n"):
66
+ return None, 1
67
+ end = text.find("\n---\n", 4)
68
+ if end < 0:
69
+ return None, 1
70
+ return text[4:end], 1
71
+
72
+
73
+ def fallback_frontmatter(raw: str) -> dict[str, object]:
74
+ data: dict[str, object] = {}
75
+ for line in raw.splitlines():
76
+ if not line or line[0].isspace() or line.lstrip().startswith("#"):
77
+ continue
78
+ match = re.match(r"^([A-Za-z0-9_-]+):\s*(.*)$", line)
79
+ if not match:
80
+ continue
81
+ key, value = match.groups()
82
+ data[key] = value.strip().strip('"\'')
83
+ return data
84
+
85
+
86
+ def parse_frontmatter(path: Path, findings: list[Finding]) -> dict[str, object] | None:
87
+ raw, _ = extract_frontmatter(path)
88
+ if raw is None:
89
+ add(findings, "error", "frontmatter.missing", path, "Missing or unterminated YAML frontmatter")
90
+ return None
91
+ if yaml is None:
92
+ return fallback_frontmatter(raw)
93
+ try:
94
+ data = yaml.safe_load(raw)
95
+ except Exception as exc:
96
+ line = int(getattr(getattr(exc, "problem_mark", None), "line", 0)) + 2
97
+ add(findings, "error", "frontmatter.invalid_yaml", path, str(exc), line)
98
+ return None
99
+ if not isinstance(data, dict):
100
+ add(findings, "error", "frontmatter.not_mapping", path, "Frontmatter must be a YAML mapping")
101
+ return None
102
+ return data
103
+
104
+
105
+ def validate_json(root: Path, findings: list[Finding]) -> None:
106
+ for path in root.rglob("*.json"):
107
+ if "__pycache__" in path.parts:
108
+ continue
109
+ try:
110
+ json.loads(path.read_text("utf-8"))
111
+ except (OSError, json.JSONDecodeError) as exc:
112
+ line = int(getattr(exc, "lineno", 1))
113
+ add(findings, "error", "json.invalid", path.relative_to(root), str(exc), line)
114
+
115
+
116
+ def validate_kit_version(root: Path, findings: list[Finding]) -> None:
117
+ version_path = root / "VERSION"
118
+ if not version_path.is_file():
119
+ add(findings, "error", "version.missing", Path("VERSION"), "VERSION is missing")
120
+ return
121
+ value = version_path.read_text("utf-8").strip()
122
+ if not re.fullmatch(r"\d{4}\.\d{1,2}\.\d{1,2}", value):
123
+ add(findings, "error", "version.invalid_calver", Path("VERSION"), f"Expected YYYY.M.D CalVer, got {value!r}")
124
+
125
+
126
+ def validate_frontmatter(
127
+ root: Path, findings: list[Finding]
128
+ ) -> tuple[
129
+ dict[str, dict[str, object]],
130
+ dict[str, dict[str, object]],
131
+ dict[str, dict[str, object]],
132
+ dict[str, dict[str, object]],
133
+ ]:
134
+ agents: dict[str, dict[str, object]] = {}
135
+ skills: dict[str, dict[str, object]] = {}
136
+ workflows: dict[str, dict[str, object]] = {}
137
+ rules: dict[str, dict[str, object]] = {}
138
+ groups = (
139
+ ("agent", sorted((root / "agent").glob("*.md"))),
140
+ ("skill", sorted((root / "skills").glob("*/SKILL.md"))),
141
+ ("workflow", sorted((root / "workflows").glob("*.md"))),
142
+ ("rule", sorted((root / "rules").glob("*.md"))),
143
+ )
144
+ registries = {"agent": agents, "skill": skills, "workflow": workflows, "rule": rules}
145
+ for kind, paths in groups:
146
+ names_seen: set[str] = set()
147
+ for path in paths:
148
+ rel = path.relative_to(root)
149
+ data = parse_frontmatter(path, findings)
150
+ if data is None:
151
+ continue
152
+ missing = REQUIRED_FIELDS[kind] - set(data)
153
+ for field in sorted(missing):
154
+ add(findings, "error", "frontmatter.required_field", rel, f"Missing required field: {field}")
155
+ name = str(data.get("name", ""))
156
+ expected = path.parent.name if kind == "skill" else path.stem
157
+ if name != expected:
158
+ add(findings, "error", "frontmatter.name_mismatch", rel, f"name={name!r}, expected {expected!r}")
159
+ if name:
160
+ if name in names_seen:
161
+ add(findings, "error", "frontmatter.duplicate_name", rel, f"Duplicate {kind} name: {name}")
162
+ names_seen.add(name)
163
+ version = data.get("version")
164
+ if version is not None and not is_semver(str(version)):
165
+ add(findings, "error", "frontmatter.invalid_semver", rel, f"Invalid SemVer: {version!r}")
166
+ registries[kind][expected] = data
167
+ return agents, skills, workflows, rules
168
+
169
+
170
+ def validate_references(
171
+ root: Path,
172
+ agents: dict[str, dict[str, object]],
173
+ skills: dict[str, dict[str, object]],
174
+ workflows: dict[str, dict[str, object]],
175
+ findings: list[Finding],
176
+ ) -> None:
177
+ for agent_name, data in agents.items():
178
+ path = Path("agent") / f"{agent_name}.md"
179
+ for skill in normalize_list(data.get("skills")):
180
+ if skill not in skills:
181
+ add(findings, "error", "reference.unknown_skill", path, f"Agent references missing skill: {skill}")
182
+
183
+ for workflow_name, data in workflows.items():
184
+ path = Path("workflows") / f"{workflow_name}.md"
185
+ for agent in normalize_list(data.get("requires_agents")):
186
+ if agent not in agents:
187
+ add(findings, "error", "reference.unknown_agent", path, f"Workflow references missing agent: {agent}")
188
+ for skill in normalize_list(data.get("requires_skills")):
189
+ if skill not in skills:
190
+ add(findings, "error", "reference.unknown_skill", path, f"Workflow references missing skill: {skill}")
191
+ if not normalize_list(data.get("artifact_outputs")):
192
+ add(findings, "warning", "workflow.no_outputs", path, "Workflow declares no artifact outputs")
193
+
194
+ script_pattern = re.compile(r'["\']((?:skills|scripts)/[^"\']+?\.py)["\']')
195
+ for path in (root / "scripts").glob("*.py"):
196
+ text = path.read_text("utf-8", errors="replace")
197
+ for match in script_pattern.finditer(text):
198
+ if any(char in match.group(1) for char in "*?["):
199
+ continue
200
+ target = root / match.group(1)
201
+ if not target.is_file():
202
+ line = text.count("\n", 0, match.start()) + 1
203
+ add(findings, "error", "reference.missing_script", path.relative_to(root), f"Referenced script does not exist: {match.group(1)}", line)
204
+
205
+
206
+ def validate_manifest(root: Path, findings: list[Finding]) -> None:
207
+ manifest_path = root / "manifest.json"
208
+ lock_path = root / "manifest.lock.json"
209
+ if not manifest_path.is_file():
210
+ add(findings, "error", "manifest.missing", Path("manifest.json"), "Run .opencode/scripts/generate_manifest.py")
211
+ return
212
+ if not lock_path.is_file():
213
+ add(findings, "error", "manifest.lock_missing", Path("manifest.lock.json"), "Run .opencode/scripts/generate_manifest.py")
214
+ return
215
+ try:
216
+ actual_manifest = json.loads(manifest_path.read_text("utf-8"))
217
+ actual_lock = json.loads(lock_path.read_text("utf-8"))
218
+ except json.JSONDecodeError:
219
+ return
220
+ try:
221
+ expected_manifest = build_manifest(root)
222
+ expected_lock = build_lock(root, expected_manifest)
223
+ except Exception as exc:
224
+ add(findings, "error", "manifest.generation_failed", Path("manifest.json"), str(exc))
225
+ return
226
+ if actual_manifest != expected_manifest:
227
+ add(findings, "error", "manifest.stale", Path("manifest.json"), "Registry differs from component frontmatter; regenerate it")
228
+ if actual_lock != expected_lock:
229
+ add(findings, "error", "manifest.lock_stale", Path("manifest.lock.json"), "Lock differs from current component files; regenerate it")
230
+
231
+ skill_versions = {name: data["version"] for name, data in expected_manifest["skills"].items()}
232
+ for agent_name, agent in expected_manifest["agents"].items():
233
+ for skill_name, constraint in agent["requires"]["skills"].items():
234
+ version = skill_versions.get(skill_name)
235
+ if version and not version_satisfies(version, constraint):
236
+ add(
237
+ findings,
238
+ "error",
239
+ "manifest.incompatible_dependency",
240
+ Path(agent["path"]),
241
+ f"{skill_name} {version} does not satisfy {constraint}",
242
+ )
243
+
244
+
245
+ def validate_generated_docs(root: Path, findings: list[Finding]) -> None:
246
+ path = root / "DEPENDENCY_GRAPH.md"
247
+ if not path.is_file():
248
+ add(findings, "error", "graph.missing", Path("DEPENDENCY_GRAPH.md"), "Run .opencode/scripts/dependency_graph.py")
249
+ return
250
+ expected = render_dependency_graph(root)
251
+ if path.read_text("utf-8") != expected:
252
+ add(findings, "error", "graph.stale", Path("DEPENDENCY_GRAPH.md"), "Dependency graph is stale")
253
+
254
+
255
+ def validate_markdown_links(root: Path, findings: list[Finding]) -> None:
256
+ pattern = re.compile(r"!?\[[^\]]*\]\(([^)]+)\)")
257
+ for path in root.rglob("*.md"):
258
+ text = path.read_text("utf-8", errors="replace")
259
+ for match in pattern.finditer(text):
260
+ raw = match.group(1).strip()
261
+ if not raw:
262
+ continue
263
+ target_text = raw.split()[0].strip("<>")
264
+ if target_text.startswith(("#", "http://", "https://", "mailto:", "tel:", "data:")):
265
+ continue
266
+ target_text = unquote(target_text.split("#", 1)[0])
267
+ if not target_text:
268
+ continue
269
+ target = (path.parent / target_text).resolve()
270
+ try:
271
+ target.relative_to(root.resolve())
272
+ except ValueError:
273
+ continue
274
+ if path.relative_to(root).as_posix() == "skills/documentation-templates/SKILL.md" and target_text.startswith("./docs/"):
275
+ continue
276
+ if not target.exists():
277
+ line = text.count("\n", 0, match.start()) + 1
278
+ add(findings, "error", "markdown.missing_link", path.relative_to(root), f"Missing local target: {target_text}", line)
279
+
280
+
281
+ def validate_memory(root: Path, findings: list[Finding]) -> None:
282
+ memory_root = root / "memory"
283
+ index = memory_root / "MEMORY.md"
284
+ required_topics = {"project-conventions.md", "user-preferences.md", "tech-decisions.md", "feedback-history.md"}
285
+ if not index.is_file():
286
+ add(findings, "error", "memory.index_missing", Path(".opencode/memory/MEMORY.md"), "Memory index is missing")
287
+ return
288
+ lines = index.read_text("utf-8").splitlines()
289
+ if len(lines) > 200:
290
+ add(findings, "error", "memory.index_too_large", Path(".opencode/memory/MEMORY.md"), f"Memory index has {len(lines)} lines; maximum is 200")
291
+ entry_pattern = re.compile(r"^- \[(user|feedback|project|reference)\] .+ → ([A-Za-z0-9._-]+\.md)$")
292
+ for line_no, line in enumerate(lines, 1):
293
+ if not line.startswith("- ["):
294
+ continue
295
+ match = entry_pattern.fullmatch(line)
296
+ if not match:
297
+ add(findings, "error", "memory.invalid_entry", Path(".opencode/memory/MEMORY.md"), "Invalid memory index entry", line_no)
298
+ continue
299
+ target = memory_root / match.group(2)
300
+ if not target.is_file():
301
+ add(findings, "error", "memory.missing_topic", Path(".opencode/memory/MEMORY.md"), f"Missing topic file: {match.group(2)}", line_no)
302
+ for topic in sorted(required_topics):
303
+ path = memory_root / topic
304
+ if not path.is_file():
305
+ add(findings, "error", "memory.required_topic", Path("memory") / topic, "Required memory topic is missing")
306
+ continue
307
+ data = parse_frontmatter(path, findings)
308
+ if data is None:
309
+ continue
310
+ missing = {"type", "created", "updated"} - set(data)
311
+ for field in sorted(missing):
312
+ add(findings, "error", "memory.required_field", path.relative_to(root), f"Missing required field: {field}")
313
+ if str(data.get("type", "")) not in {"user", "feedback", "project", "reference"}:
314
+ add(findings, "error", "memory.invalid_type", path.relative_to(root), f"Invalid memory type: {data.get('type')!r}")
315
+
316
+
317
+ def validate_python(root: Path, findings: list[Finding]) -> None:
318
+ for path in root.rglob("*.py"):
319
+ if "__pycache__" in path.parts:
320
+ continue
321
+ try:
322
+ ast.parse(path.read_text("utf-8"), filename=str(path))
323
+ except (OSError, SyntaxError) as exc:
324
+ add(findings, "error", "python.syntax", path.relative_to(root), str(exc), int(getattr(exc, "lineno", 1) or 1))
325
+
326
+
327
+ def validate_architecture_counts(root: Path, findings: list[Finding]) -> None:
328
+ path = root / "ARCHITECTURE.md"
329
+ if not path.is_file():
330
+ add(findings, "error", "architecture.missing", Path("ARCHITECTURE.md"), "ARCHITECTURE.md is missing")
331
+ return
332
+ text = path.read_text("utf-8", errors="replace")
333
+ actual = {
334
+ "agents": len(list((root / "agent").glob("*.md"))),
335
+ "skills": len(list((root / "skills").glob("*/SKILL.md"))),
336
+ "workflows": len(list((root / "workflows").glob("*.md"))),
337
+ "skill_scripts": len(list((root / "skills").glob("*/scripts/*.py"))),
338
+ }
339
+ patterns = {
340
+ "agents": r"\*\*Total Agents\*\*\s*\|\s*(\d+)",
341
+ "skills": r"\*\*Total Skills\*\*\s*\|\s*(\d+)",
342
+ "workflows": r"\*\*Total Workflows\*\*\s*\|\s*(\d+)",
343
+ "skill_scripts": r"\*\*Total Skill Scripts\*\*\s*\|\s*(\d+)",
344
+ }
345
+ for key, pattern in patterns.items():
346
+ match = re.search(pattern, text)
347
+ if not match:
348
+ add(findings, "error", "architecture.count_missing", Path("ARCHITECTURE.md"), f"Missing inventory field for {key}")
349
+ elif int(match.group(1)) != actual[key]:
350
+ add(findings, "error", "architecture.count_mismatch", Path("ARCHITECTURE.md"), f"{key}: documented {match.group(1)}, actual {actual[key]}")
351
+
352
+
353
+ def validate(root: Path) -> list[Finding]:
354
+ findings: list[Finding] = []
355
+ validate_json(root, findings)
356
+ validate_kit_version(root, findings)
357
+ agents, skills, workflows, _rules = validate_frontmatter(root, findings)
358
+ validate_references(root, agents, skills, workflows, findings)
359
+ validate_manifest(root, findings)
360
+ validate_generated_docs(root, findings)
361
+ validate_markdown_links(root, findings)
362
+ validate_memory(root, findings)
363
+ validate_python(root, findings)
364
+ validate_architecture_counts(root, findings)
365
+ return findings
366
+
367
+
368
+ def main() -> int:
369
+ parser = argparse.ArgumentParser(description="Validate OpenCode kit structure and cross references")
370
+ parser.add_argument("path", nargs="?", default=None, help="Path to .agents (defaults to this toolkit)")
371
+ parser.add_argument("--json", action="store_true", dest="as_json")
372
+ args = parser.parse_args()
373
+ root = Path(args.path).resolve() if args.path else Path(__file__).resolve().parents[1]
374
+ if not root.is_dir():
375
+ parser.error(f"Toolkit directory does not exist: {root}")
376
+ findings = validate(root)
377
+ errors = [item for item in findings if item.severity == "error"]
378
+ warnings = [item for item in findings if item.severity == "warning"]
379
+ payload = {
380
+ "toolkit": str(root),
381
+ "passed": not errors,
382
+ "summary": {"errors": len(errors), "warnings": len(warnings)},
383
+ "findings": [item.__dict__ for item in findings],
384
+ }
385
+ if args.as_json:
386
+ print(json.dumps(payload, indent=2, ensure_ascii=False))
387
+ else:
388
+ print(f"OpenCode kit self-validation: {root}")
389
+ for item in findings:
390
+ print(f"[{item.severity.upper()}] {item.file}:{item.line} {item.code} - {item.message}")
391
+ print(f"Summary: {len(errors)} error(s), {len(warnings)} warning(s)")
392
+ print("[PASS] Toolkit is structurally valid." if not errors else "[FAIL] Toolkit validation failed.")
393
+ return 0 if not errors else 1
394
+
395
+
396
+ if __name__ == "__main__":
397
+ raise SystemExit(main())
@@ -0,0 +1,216 @@
1
+ #!/usr/bin/env python3
2
+ """Shared process runner for OpenCode kit validation entry points."""
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import os
7
+ import subprocess
8
+ import sys
9
+ from dataclasses import asdict, dataclass, field
10
+ from datetime import datetime, timezone
11
+ from pathlib import Path
12
+ from typing import Iterable, Literal
13
+
14
+ Target = Literal["project", "url", "none"]
15
+
16
+
17
+ @dataclass(frozen=True)
18
+ class CheckSpec:
19
+ name: str
20
+ script: str
21
+ category: str
22
+ target: Target = "project"
23
+ required: bool = False
24
+ args: tuple[str, ...] = ()
25
+ timeout: int = 300
26
+
27
+
28
+ @dataclass
29
+ class CheckResult:
30
+ name: str
31
+ category: str
32
+ status: Literal["passed", "failed", "skipped", "error"]
33
+ required: bool
34
+ duration_seconds: float = 0.0
35
+ command: list[str] = field(default_factory=list)
36
+ stdout: str = ""
37
+ stderr: str = ""
38
+ reason: str = ""
39
+
40
+ @property
41
+ def passed(self) -> bool:
42
+ return self.status in {"passed", "skipped"}
43
+
44
+
45
+ class Console:
46
+ def __init__(self) -> None:
47
+ enabled = sys.stdout.isatty() and "NO_COLOR" not in os.environ
48
+ self.bold = "\033[1m" if enabled else ""
49
+ self.cyan = "\033[96m" if enabled else ""
50
+ self.green = "\033[92m" if enabled else ""
51
+ self.yellow = "\033[93m" if enabled else ""
52
+ self.red = "\033[91m" if enabled else ""
53
+ self.end = "\033[0m" if enabled else ""
54
+
55
+ def header(self, text: str) -> None:
56
+ line = "=" * 70
57
+ print(f"\n{self.bold}{self.cyan}{line}\n{text.center(70)}\n{line}{self.end}\n")
58
+
59
+ def passed(self, text: str) -> None:
60
+ print(f"{self.green}[PASS] {text}{self.end}")
61
+
62
+ def skipped(self, text: str) -> None:
63
+ print(f"{self.yellow}[SKIP] {text}{self.end}")
64
+
65
+ def failed(self, text: str) -> None:
66
+ print(f"{self.red}[FAIL] {text}{self.end}")
67
+
68
+ def info(self, text: str) -> None:
69
+ print(text)
70
+
71
+
72
+ CONSOLE = Console()
73
+
74
+
75
+ def locate_toolkit_root(project: Path, caller_file: str) -> Path:
76
+ """Find the toolkit independently from the project being audited."""
77
+ for candidate in (project / ".opencode", project / ".agents", project / ".agent"):
78
+ if (candidate / "skills").is_dir() and (candidate / "scripts").is_dir():
79
+ return candidate
80
+ embedded = Path(caller_file).resolve().parents[1]
81
+ if (embedded / "skills").is_dir() and (embedded / "scripts").is_dir():
82
+ return embedded
83
+ raise FileNotFoundError(
84
+ "OpenCode kit root was not found. Expected <project>/.opencode, <project>/.agents, <project>/.agent, "
85
+ "or a runner located inside the toolkit."
86
+ )
87
+
88
+
89
+ def _command_for(spec: CheckSpec, script: Path, project: Path, url: str | None) -> list[str] | None:
90
+ command = [sys.executable, str(script)]
91
+ if spec.target == "project":
92
+ command.append(str(project))
93
+ elif spec.target == "url":
94
+ if not url:
95
+ return None
96
+ command.append(url)
97
+ command.extend(spec.args)
98
+ return command
99
+
100
+
101
+ def run_check(spec: CheckSpec, toolkit_root: Path, project: Path, url: str | None) -> CheckResult:
102
+ script = toolkit_root / spec.script
103
+ if not script.is_file():
104
+ status = "failed" if spec.required else "skipped"
105
+ reason = f"Script not found: {script}"
106
+ result = CheckResult(spec.name, spec.category, status, spec.required, reason=reason)
107
+ (CONSOLE.failed if status == "failed" else CONSOLE.skipped)(f"{spec.name}: {reason}")
108
+ return result
109
+
110
+ command = _command_for(spec, script, project, url)
111
+ if command is None:
112
+ reason = "URL not provided"
113
+ CONSOLE.skipped(f"{spec.name}: {reason}")
114
+ return CheckResult(spec.name, spec.category, "skipped", spec.required, reason=reason)
115
+
116
+ started = datetime.now(timezone.utc)
117
+ try:
118
+ proc = subprocess.run(
119
+ command,
120
+ cwd=project,
121
+ capture_output=True,
122
+ text=True,
123
+ timeout=spec.timeout,
124
+ check=False,
125
+ )
126
+ duration = (datetime.now(timezone.utc) - started).total_seconds()
127
+ status = "passed" if proc.returncode == 0 else "failed"
128
+ result = CheckResult(
129
+ spec.name, spec.category, status, spec.required, duration,
130
+ command, proc.stdout, proc.stderr,
131
+ reason="" if status == "passed" else f"Exit code {proc.returncode}",
132
+ )
133
+ if status == "passed":
134
+ CONSOLE.passed(f"{spec.name} ({duration:.1f}s)")
135
+ else:
136
+ CONSOLE.failed(f"{spec.name} ({duration:.1f}s, exit {proc.returncode})")
137
+ _print_failure_output(result)
138
+ return result
139
+ except subprocess.TimeoutExpired as exc:
140
+ duration = (datetime.now(timezone.utc) - started).total_seconds()
141
+ result = CheckResult(
142
+ spec.name, spec.category, "error", spec.required, duration, command,
143
+ _decode_timeout(exc.stdout), _decode_timeout(exc.stderr),
144
+ f"Timed out after {spec.timeout}s",
145
+ )
146
+ CONSOLE.failed(f"{spec.name}: {result.reason}")
147
+ return result
148
+ except OSError as exc:
149
+ duration = (datetime.now(timezone.utc) - started).total_seconds()
150
+ result = CheckResult(spec.name, spec.category, "error", spec.required, duration, command, reason=str(exc))
151
+ CONSOLE.failed(f"{spec.name}: {exc}")
152
+ return result
153
+
154
+
155
+ def _decode_timeout(value: str | bytes | None) -> str:
156
+ if value is None:
157
+ return ""
158
+ return value.decode(errors="replace") if isinstance(value, bytes) else value
159
+
160
+
161
+ def _print_failure_output(result: CheckResult, limit: int = 1600) -> None:
162
+ combined = "\n".join(part.strip() for part in (result.stdout, result.stderr) if part.strip())
163
+ if combined:
164
+ print(combined[-limit:])
165
+
166
+
167
+ def execute_suite(
168
+ specs: Iterable[CheckSpec],
169
+ toolkit_root: Path,
170
+ project: Path,
171
+ url: str | None,
172
+ stop_on_fail: bool = False,
173
+ ) -> list[CheckResult]:
174
+ results: list[CheckResult] = []
175
+ current_category = ""
176
+ for spec in specs:
177
+ if spec.category != current_category:
178
+ current_category = spec.category
179
+ CONSOLE.header(current_category)
180
+ result = run_check(spec, toolkit_root, project, url)
181
+ results.append(result)
182
+ if stop_on_fail and result.status in {"failed", "error"}:
183
+ break
184
+ return results
185
+
186
+
187
+ def suite_success(results: Iterable[CheckResult]) -> bool:
188
+ return all(result.status not in {"failed", "error"} for result in results)
189
+
190
+
191
+ def print_summary(title: str, results: list[CheckResult], started: datetime) -> bool:
192
+ CONSOLE.header(title)
193
+ counts = {status: sum(r.status == status for r in results) for status in ("passed", "failed", "error", "skipped")}
194
+ duration = (datetime.now(timezone.utc) - started).total_seconds()
195
+ print(f"Duration: {duration:.1f}s")
196
+ print(f"Checks: {len(results)} | Passed: {counts['passed']} | Failed: {counts['failed']} | Errors: {counts['error']} | Skipped: {counts['skipped']}")
197
+ for result in results:
198
+ marker = {"passed": "PASS", "failed": "FAIL", "error": "ERROR", "skipped": "SKIP"}[result.status]
199
+ detail = f" - {result.reason}" if result.reason else ""
200
+ print(f"[{marker}] {result.category} / {result.name}{detail}")
201
+ ok = suite_success(results)
202
+ (CONSOLE.passed if ok else CONSOLE.failed)("Validation completed successfully" if ok else "Validation found blocking failures")
203
+ return ok
204
+
205
+
206
+ def write_report(path: Path, project: Path, toolkit_root: Path, results: list[CheckResult], started: datetime) -> None:
207
+ payload = {
208
+ "project": str(project),
209
+ "toolkit_root": str(toolkit_root),
210
+ "started_at": started.isoformat(),
211
+ "finished_at": datetime.now(timezone.utc).isoformat(),
212
+ "success": suite_success(results),
213
+ "results": [asdict(result) for result in results],
214
+ }
215
+ path.parent.mkdir(parents=True, exist_ok=True)
216
+ path.write_text(json.dumps(payload, indent=2, ensure_ascii=False) + "\n", encoding="utf-8")