@laitszkin/apollo-toolkit 2.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 (204) hide show
  1. package/AGENTS.md +62 -0
  2. package/CHANGELOG.md +100 -0
  3. package/LICENSE +21 -0
  4. package/README.md +144 -0
  5. package/align-project-documents/SKILL.md +94 -0
  6. package/align-project-documents/agents/openai.yaml +4 -0
  7. package/analyse-app-logs/LICENSE +21 -0
  8. package/analyse-app-logs/README.md +126 -0
  9. package/analyse-app-logs/SKILL.md +121 -0
  10. package/analyse-app-logs/agents/openai.yaml +4 -0
  11. package/analyse-app-logs/references/investigation-checklist.md +58 -0
  12. package/analyse-app-logs/references/log-signal-patterns.md +52 -0
  13. package/answering-questions-with-research/SKILL.md +46 -0
  14. package/answering-questions-with-research/agents/openai.yaml +4 -0
  15. package/bin/apollo-toolkit.js +7 -0
  16. package/commit-and-push/LICENSE +21 -0
  17. package/commit-and-push/README.md +26 -0
  18. package/commit-and-push/SKILL.md +70 -0
  19. package/commit-and-push/agents/openai.yaml +4 -0
  20. package/commit-and-push/references/branch-naming.md +15 -0
  21. package/commit-and-push/references/commit-messages.md +19 -0
  22. package/deep-research-topics/LICENSE +21 -0
  23. package/deep-research-topics/README.md +43 -0
  24. package/deep-research-topics/SKILL.md +84 -0
  25. package/deep-research-topics/agents/openai.yaml +4 -0
  26. package/develop-new-features/LICENSE +21 -0
  27. package/develop-new-features/README.md +52 -0
  28. package/develop-new-features/SKILL.md +105 -0
  29. package/develop-new-features/agents/openai.yaml +4 -0
  30. package/develop-new-features/references/testing-e2e.md +35 -0
  31. package/develop-new-features/references/testing-integration.md +42 -0
  32. package/develop-new-features/references/testing-property-based.md +44 -0
  33. package/develop-new-features/references/testing-unit.md +37 -0
  34. package/discover-edge-cases/CHANGELOG.md +19 -0
  35. package/discover-edge-cases/LICENSE +21 -0
  36. package/discover-edge-cases/README.md +87 -0
  37. package/discover-edge-cases/SKILL.md +124 -0
  38. package/discover-edge-cases/agents/openai.yaml +4 -0
  39. package/discover-edge-cases/references/architecture-edge-cases.md +41 -0
  40. package/discover-edge-cases/references/code-edge-cases.md +46 -0
  41. package/docs-to-voice/.env.example +106 -0
  42. package/docs-to-voice/CHANGELOG.md +71 -0
  43. package/docs-to-voice/LICENSE +21 -0
  44. package/docs-to-voice/README.md +118 -0
  45. package/docs-to-voice/SKILL.md +107 -0
  46. package/docs-to-voice/agents/openai.yaml +4 -0
  47. package/docs-to-voice/scripts/docs_to_voice.py +1385 -0
  48. package/docs-to-voice/scripts/docs_to_voice.sh +11 -0
  49. package/docs-to-voice/tests/test_docs_to_voice_api_max_chars.py +210 -0
  50. package/docs-to-voice/tests/test_docs_to_voice_sentence_timeline.py +115 -0
  51. package/docs-to-voice/tests/test_docs_to_voice_settings.py +43 -0
  52. package/docs-to-voice/tests/test_docs_to_voice_speech_rate.py +57 -0
  53. package/enhance-existing-features/CHANGELOG.md +35 -0
  54. package/enhance-existing-features/LICENSE +21 -0
  55. package/enhance-existing-features/README.md +54 -0
  56. package/enhance-existing-features/SKILL.md +120 -0
  57. package/enhance-existing-features/agents/openai.yaml +4 -0
  58. package/enhance-existing-features/references/e2e-tests.md +25 -0
  59. package/enhance-existing-features/references/integration-tests.md +30 -0
  60. package/enhance-existing-features/references/property-based-tests.md +33 -0
  61. package/enhance-existing-features/references/unit-tests.md +29 -0
  62. package/feature-propose/LICENSE +21 -0
  63. package/feature-propose/README.md +23 -0
  64. package/feature-propose/SKILL.md +107 -0
  65. package/feature-propose/agents/openai.yaml +4 -0
  66. package/feature-propose/references/enhancement-features.md +25 -0
  67. package/feature-propose/references/important-features.md +25 -0
  68. package/feature-propose/references/mvp-features.md +25 -0
  69. package/feature-propose/references/performance-features.md +25 -0
  70. package/financial-research/SKILL.md +208 -0
  71. package/financial-research/agents/openai.yaml +4 -0
  72. package/financial-research/assets/weekly_market_report_template.md +45 -0
  73. package/fix-github-issues/SKILL.md +98 -0
  74. package/fix-github-issues/agents/openai.yaml +4 -0
  75. package/fix-github-issues/scripts/list_issues.py +148 -0
  76. package/fix-github-issues/tests/test_list_issues.py +127 -0
  77. package/generate-spec/LICENSE +21 -0
  78. package/generate-spec/README.md +61 -0
  79. package/generate-spec/SKILL.md +96 -0
  80. package/generate-spec/agents/openai.yaml +4 -0
  81. package/generate-spec/references/templates/checklist.md +78 -0
  82. package/generate-spec/references/templates/spec.md +55 -0
  83. package/generate-spec/references/templates/tasks.md +35 -0
  84. package/generate-spec/scripts/create-specs +123 -0
  85. package/harden-app-security/CHANGELOG.md +27 -0
  86. package/harden-app-security/LICENSE +21 -0
  87. package/harden-app-security/README.md +46 -0
  88. package/harden-app-security/SKILL.md +127 -0
  89. package/harden-app-security/agents/openai.yaml +4 -0
  90. package/harden-app-security/references/agent-attack-catalog.md +117 -0
  91. package/harden-app-security/references/common-software-attack-catalog.md +168 -0
  92. package/harden-app-security/references/red-team-extreme-scenarios.md +81 -0
  93. package/harden-app-security/references/risk-checklist.md +78 -0
  94. package/harden-app-security/references/security-test-patterns-agent.md +101 -0
  95. package/harden-app-security/references/security-test-patterns-finance.md +88 -0
  96. package/harden-app-security/references/test-snippets.md +73 -0
  97. package/improve-observability/SKILL.md +114 -0
  98. package/improve-observability/agents/openai.yaml +4 -0
  99. package/learn-skill-from-conversations/CHANGELOG.md +15 -0
  100. package/learn-skill-from-conversations/LICENSE +22 -0
  101. package/learn-skill-from-conversations/README.md +47 -0
  102. package/learn-skill-from-conversations/SKILL.md +85 -0
  103. package/learn-skill-from-conversations/agents/openai.yaml +4 -0
  104. package/learn-skill-from-conversations/scripts/extract_recent_conversations.py +369 -0
  105. package/learn-skill-from-conversations/tests/test_extract_recent_conversations.py +176 -0
  106. package/learning-error-book/SKILL.md +112 -0
  107. package/learning-error-book/agents/openai.yaml +4 -0
  108. package/learning-error-book/assets/error_book_template.md +66 -0
  109. package/learning-error-book/scripts/render_markdown_to_pdf.py +367 -0
  110. package/lib/cli.js +338 -0
  111. package/lib/installer.js +225 -0
  112. package/maintain-project-constraints/SKILL.md +109 -0
  113. package/maintain-project-constraints/agents/openai.yaml +4 -0
  114. package/maintain-skill-catalog/README.md +18 -0
  115. package/maintain-skill-catalog/SKILL.md +66 -0
  116. package/maintain-skill-catalog/agents/openai.yaml +4 -0
  117. package/novel-to-short-video/CHANGELOG.md +53 -0
  118. package/novel-to-short-video/LICENSE +21 -0
  119. package/novel-to-short-video/README.md +63 -0
  120. package/novel-to-short-video/SKILL.md +233 -0
  121. package/novel-to-short-video/agents/openai.yaml +4 -0
  122. package/novel-to-short-video/references/plan-template.md +71 -0
  123. package/novel-to-short-video/references/roles-json.md +41 -0
  124. package/open-github-issue/LICENSE +21 -0
  125. package/open-github-issue/README.md +97 -0
  126. package/open-github-issue/SKILL.md +119 -0
  127. package/open-github-issue/agents/openai.yaml +4 -0
  128. package/open-github-issue/scripts/open_github_issue.py +380 -0
  129. package/open-github-issue/tests/test_open_github_issue.py +159 -0
  130. package/open-source-pr-workflow/CHANGELOG.md +32 -0
  131. package/open-source-pr-workflow/LICENSE +21 -0
  132. package/open-source-pr-workflow/README.md +23 -0
  133. package/open-source-pr-workflow/SKILL.md +123 -0
  134. package/open-source-pr-workflow/agents/openai.yaml +4 -0
  135. package/openai-text-to-image-storyboard/.env.example +10 -0
  136. package/openai-text-to-image-storyboard/CHANGELOG.md +49 -0
  137. package/openai-text-to-image-storyboard/LICENSE +21 -0
  138. package/openai-text-to-image-storyboard/README.md +99 -0
  139. package/openai-text-to-image-storyboard/SKILL.md +107 -0
  140. package/openai-text-to-image-storyboard/agents/openai.yaml +4 -0
  141. package/openai-text-to-image-storyboard/scripts/generate_storyboard_images.py +763 -0
  142. package/package.json +36 -0
  143. package/record-spending/SKILL.md +113 -0
  144. package/record-spending/agents/openai.yaml +4 -0
  145. package/record-spending/references/account-format.md +33 -0
  146. package/record-spending/references/workbook-layout.md +84 -0
  147. package/resolve-review-comments/SKILL.md +122 -0
  148. package/resolve-review-comments/agents/openai.yaml +4 -0
  149. package/resolve-review-comments/references/adoption-criteria.md +23 -0
  150. package/resolve-review-comments/scripts/review_threads.py +425 -0
  151. package/resolve-review-comments/tests/test_review_threads.py +74 -0
  152. package/review-change-set/LICENSE +21 -0
  153. package/review-change-set/README.md +55 -0
  154. package/review-change-set/SKILL.md +103 -0
  155. package/review-change-set/agents/openai.yaml +4 -0
  156. package/review-codebases/LICENSE +21 -0
  157. package/review-codebases/README.md +67 -0
  158. package/review-codebases/SKILL.md +109 -0
  159. package/review-codebases/agents/openai.yaml +4 -0
  160. package/scripts/install_skills.ps1 +283 -0
  161. package/scripts/install_skills.sh +262 -0
  162. package/scripts/validate_openai_agent_config.py +194 -0
  163. package/scripts/validate_skill_frontmatter.py +110 -0
  164. package/specs-to-project-docs/LICENSE +21 -0
  165. package/specs-to-project-docs/README.md +57 -0
  166. package/specs-to-project-docs/SKILL.md +111 -0
  167. package/specs-to-project-docs/agents/openai.yaml +4 -0
  168. package/specs-to-project-docs/references/templates/architecture.md +29 -0
  169. package/specs-to-project-docs/references/templates/configuration.md +29 -0
  170. package/specs-to-project-docs/references/templates/developer-guide.md +33 -0
  171. package/specs-to-project-docs/references/templates/docs-index.md +39 -0
  172. package/specs-to-project-docs/references/templates/features.md +25 -0
  173. package/specs-to-project-docs/references/templates/getting-started.md +38 -0
  174. package/specs-to-project-docs/references/templates/readme.md +49 -0
  175. package/systematic-debug/LICENSE +21 -0
  176. package/systematic-debug/README.md +81 -0
  177. package/systematic-debug/SKILL.md +59 -0
  178. package/systematic-debug/agents/openai.yaml +4 -0
  179. package/text-to-short-video/.env.example +36 -0
  180. package/text-to-short-video/LICENSE +21 -0
  181. package/text-to-short-video/README.md +82 -0
  182. package/text-to-short-video/SKILL.md +221 -0
  183. package/text-to-short-video/agents/openai.yaml +4 -0
  184. package/text-to-short-video/scripts/enforce_video_aspect_ratio.py +350 -0
  185. package/version-release/CHANGELOG.md +53 -0
  186. package/version-release/LICENSE +21 -0
  187. package/version-release/README.md +28 -0
  188. package/version-release/SKILL.md +94 -0
  189. package/version-release/agents/openai.yaml +4 -0
  190. package/version-release/references/branch-naming.md +15 -0
  191. package/version-release/references/changelog-writing.md +8 -0
  192. package/version-release/references/commit-messages.md +19 -0
  193. package/version-release/references/readme-writing.md +12 -0
  194. package/version-release/references/semantic-versioning.md +12 -0
  195. package/video-production/CHANGELOG.md +104 -0
  196. package/video-production/LICENSE +18 -0
  197. package/video-production/README.md +68 -0
  198. package/video-production/SKILL.md +213 -0
  199. package/video-production/agents/openai.yaml +4 -0
  200. package/video-production/references/plan-template.md +54 -0
  201. package/video-production/references/roles-json.md +41 -0
  202. package/weekly-financial-event-report/SKILL.md +195 -0
  203. package/weekly-financial-event-report/agents/openai.yaml +4 -0
  204. package/weekly-financial-event-report/assets/financial_event_report_template.md +53 -0
@@ -0,0 +1,262 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ usage() {
5
+ cat <<"USAGE"
6
+ Usage:
7
+ ./scripts/install_skills.sh [codex|openclaw|trae|all]...
8
+
9
+ Modes:
10
+ codex Install symlinks into ~/.codex/skills
11
+ openclaw Install symlinks into ~/.openclaw/workspace*/skills
12
+ trae Install symlinks into ~/.trae/skills
13
+ all Install all supported targets
14
+
15
+ Optional environment overrides:
16
+ CODEX_SKILLS_DIR Override codex skills destination path
17
+ OPENCLAW_HOME Override openclaw home path
18
+ TRAE_SKILLS_DIR Override trae skills destination path
19
+ APOLLO_TOOLKIT_HOME Override local install path used in curl/pipe mode
20
+ APOLLO_TOOLKIT_REPO_URL Override git repository URL used in curl/pipe mode
21
+ USAGE
22
+ }
23
+
24
+ SCRIPT_SOURCE="${BASH_SOURCE[0]-}"
25
+ TOOLKIT_REPO_URL="${APOLLO_TOOLKIT_REPO_URL:-https://github.com/LaiTszKin/apollo-toolkit.git}"
26
+ TOOLKIT_HOME="${APOLLO_TOOLKIT_HOME:-$HOME/.apollo-toolkit}"
27
+
28
+ show_banner() {
29
+ cat <<'BANNER'
30
+ +------------------------------------------+
31
+ | Apollo Toolkit |
32
+ | npm installer and skill linker |
33
+ +------------------------------------------+
34
+ BANNER
35
+ }
36
+
37
+ bootstrap_repo_if_needed() {
38
+ if [[ -d "$TOOLKIT_HOME/.git" ]]; then
39
+ git -C "$TOOLKIT_HOME" pull --ff-only >/dev/null
40
+ else
41
+ rm -rf "$TOOLKIT_HOME"
42
+ git clone --depth 1 "$TOOLKIT_REPO_URL" "$TOOLKIT_HOME" >/dev/null
43
+ fi
44
+ }
45
+
46
+ if [[ -n "$SCRIPT_SOURCE" && -f "$SCRIPT_SOURCE" ]]; then
47
+ SCRIPT_DIR="$(cd "$(dirname "$SCRIPT_SOURCE")" && pwd)"
48
+ REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
49
+ else
50
+ # curl/pipe mode: use current directory only when it looks like this repo.
51
+ if find "$PWD" -mindepth 1 -maxdepth 1 -type d -exec test -f '{}/SKILL.md' ';' -print -quit | grep -q .; then
52
+ REPO_ROOT="$PWD"
53
+ else
54
+ bootstrap_repo_if_needed
55
+ REPO_ROOT="$TOOLKIT_HOME"
56
+ fi
57
+ SCRIPT_DIR="$REPO_ROOT/scripts"
58
+ fi
59
+ SELECTED_MODES=()
60
+ SKILL_PATHS=()
61
+
62
+ collect_skills() {
63
+ local dir
64
+ SKILL_PATHS=()
65
+ while IFS= read -r dir; do
66
+ if [[ -f "$dir/SKILL.md" ]]; then
67
+ SKILL_PATHS+=("$dir")
68
+ fi
69
+ done < <(find "$REPO_ROOT" -mindepth 1 -maxdepth 1 -type d | sort)
70
+
71
+ if [[ ${#SKILL_PATHS[@]} -eq 0 ]]; then
72
+ echo "No skill folders found in: $REPO_ROOT" >&2
73
+ exit 1
74
+ fi
75
+ }
76
+
77
+ replace_with_symlink() {
78
+ local src="$1"
79
+ local target_root="$2"
80
+ local name target
81
+
82
+ name="$(basename "$src")"
83
+ target="$target_root/$name"
84
+
85
+ mkdir -p "$target_root"
86
+ if [[ -e "$target" || -L "$target" ]]; then
87
+ rm -rf "$target"
88
+ fi
89
+ ln -s "$src" "$target"
90
+ echo "[linked] $target -> $src"
91
+ }
92
+
93
+ install_codex() {
94
+ local codex_skills_dir
95
+ codex_skills_dir="${CODEX_SKILLS_DIR:-$HOME/.codex/skills}"
96
+
97
+ echo "Installing to codex: $codex_skills_dir"
98
+ for src in "${SKILL_PATHS[@]}"; do
99
+ replace_with_symlink "$src" "$codex_skills_dir"
100
+ done
101
+ }
102
+
103
+ install_openclaw() {
104
+ local openclaw_home workspace skills_dir
105
+ local -a workspaces
106
+
107
+ openclaw_home="${OPENCLAW_HOME:-$HOME/.openclaw}"
108
+
109
+ workspaces=()
110
+ while IFS= read -r workspace; do
111
+ workspaces+=("$workspace")
112
+ done < <(find "$openclaw_home" -mindepth 1 -maxdepth 1 -type d -name 'workspace*' | sort)
113
+
114
+ if [[ ${#workspaces[@]} -eq 0 ]]; then
115
+ echo "No workspace directories found under: $openclaw_home" >&2
116
+ exit 1
117
+ fi
118
+
119
+ for workspace in "${workspaces[@]}"; do
120
+ skills_dir="$workspace/skills"
121
+ echo "Installing to openclaw workspace: $skills_dir"
122
+ for src in "${SKILL_PATHS[@]}"; do
123
+ replace_with_symlink "$src" "$skills_dir"
124
+ done
125
+ done
126
+ }
127
+
128
+ install_trae() {
129
+ local trae_skills_dir
130
+ trae_skills_dir="${TRAE_SKILLS_DIR:-$HOME/.trae/skills}"
131
+
132
+ echo "Installing to trae: $trae_skills_dir"
133
+ for src in "${SKILL_PATHS[@]}"; do
134
+ replace_with_symlink "$src" "$trae_skills_dir"
135
+ done
136
+ }
137
+
138
+ add_mode_once() {
139
+ local mode="$1"
140
+ local existing
141
+
142
+ if [[ ${#SELECTED_MODES[@]} -gt 0 ]]; then
143
+ for existing in "${SELECTED_MODES[@]}"; do
144
+ if [[ "$existing" == "$mode" ]]; then
145
+ return
146
+ fi
147
+ done
148
+ fi
149
+ SELECTED_MODES+=("$mode")
150
+ }
151
+
152
+ parse_mode() {
153
+ local mode="$1"
154
+
155
+ case "$mode" in
156
+ codex|openclaw|trae)
157
+ add_mode_once "$mode"
158
+ ;;
159
+ all)
160
+ add_mode_once "codex"
161
+ add_mode_once "openclaw"
162
+ add_mode_once "trae"
163
+ ;;
164
+ *)
165
+ echo "Invalid mode: $mode" >&2
166
+ usage
167
+ exit 1
168
+ ;;
169
+ esac
170
+ }
171
+
172
+ read_choice_from_user() {
173
+ local prompt="$1"
174
+ local result
175
+
176
+ if [[ -t 0 ]]; then
177
+ read -r -p "$prompt" result
178
+ elif [[ -r /dev/tty ]]; then
179
+ read -r -p "$prompt" result < /dev/tty
180
+ else
181
+ echo "Interactive input unavailable. Pass mode arguments (e.g. codex/openclaw/trae/all)." >&2
182
+ exit 1
183
+ fi
184
+
185
+ printf '%s' "$result"
186
+ }
187
+
188
+ choose_modes_interactive() {
189
+ local choice raw_choice
190
+ local -a choices
191
+
192
+ show_banner
193
+ echo
194
+ echo "Select install options (comma-separated):"
195
+ echo "1) codex (~/.codex/skills)"
196
+ echo "2) openclaw (~/.openclaw/workspace*/skills)"
197
+ echo "3) trae (~/.trae/skills)"
198
+ echo "4) all"
199
+ choice="$(read_choice_from_user 'Enter choice(s) [1-4]: ')"
200
+
201
+ IFS=',' read -r -a choices <<< "$choice"
202
+ for raw_choice in "${choices[@]}"; do
203
+ raw_choice="${raw_choice//[[:space:]]/}"
204
+ case "$raw_choice" in
205
+ 1) add_mode_once "codex" ;;
206
+ 2) add_mode_once "openclaw" ;;
207
+ 3) add_mode_once "trae" ;;
208
+ 4) add_mode_once "codex"; add_mode_once "openclaw"; add_mode_once "trae" ;;
209
+ *)
210
+ echo "Invalid choice: $raw_choice" >&2
211
+ exit 1
212
+ ;;
213
+ esac
214
+ done
215
+
216
+ if [[ ${#SELECTED_MODES[@]} -eq 0 ]]; then
217
+ echo "No install option selected." >&2
218
+ exit 1
219
+ fi
220
+ }
221
+
222
+ resolve_modes() {
223
+ local mode
224
+
225
+ if [[ $# -eq 0 ]]; then
226
+ choose_modes_interactive
227
+ return
228
+ fi
229
+
230
+ for mode in "$@"; do
231
+ parse_mode "$mode"
232
+ done
233
+ }
234
+
235
+ main() {
236
+ local mode
237
+ SELECTED_MODES=()
238
+
239
+ if [[ "${1:-}" == "-h" || "${1:-}" == "--help" ]]; then
240
+ usage
241
+ exit 0
242
+ fi
243
+
244
+ resolve_modes "$@"
245
+ collect_skills
246
+
247
+ for mode in "${SELECTED_MODES[@]}"; do
248
+ case "$mode" in
249
+ codex) install_codex ;;
250
+ openclaw) install_openclaw ;;
251
+ trae) install_trae ;;
252
+ *)
253
+ usage
254
+ exit 1
255
+ ;;
256
+ esac
257
+ done
258
+
259
+ echo "Done."
260
+ }
261
+
262
+ main "$@"
@@ -0,0 +1,194 @@
1
+ #!/usr/bin/env python3
2
+ """Validate agents/openai.yaml for all top-level skills."""
3
+
4
+ from __future__ import annotations
5
+
6
+ import re
7
+ import sys
8
+ from pathlib import Path
9
+
10
+ import yaml
11
+
12
+ TOP_LEVEL_ALLOWED_KEYS = {"interface", "dependencies", "policy"}
13
+ INTERFACE_REQUIRED_KEYS = {"display_name", "short_description", "default_prompt"}
14
+ INTERFACE_ALLOWED_KEYS = {
15
+ "display_name",
16
+ "short_description",
17
+ "default_prompt",
18
+ "icon_small",
19
+ "icon_large",
20
+ "brand_color",
21
+ }
22
+ HEX_COLOR_PATTERN = re.compile(r"^#[0-9A-Fa-f]{6}$")
23
+
24
+
25
+ def repo_root() -> Path:
26
+ return Path(__file__).resolve().parent.parent
27
+
28
+
29
+ def iter_skill_dirs(root: Path) -> list[Path]:
30
+ return sorted(path for path in root.iterdir() if path.is_dir() and (path / "SKILL.md").is_file())
31
+
32
+
33
+ def extract_frontmatter(content: str) -> dict[str, object]:
34
+ lines = content.splitlines()
35
+ if not lines or lines[0].strip() != "---":
36
+ raise ValueError("SKILL.md must start with YAML frontmatter delimiter '---'.")
37
+
38
+ for index in range(1, len(lines)):
39
+ if lines[index].strip() == "---":
40
+ raw_frontmatter = "\n".join(lines[1:index])
41
+ parsed = yaml.safe_load(raw_frontmatter)
42
+ if not isinstance(parsed, dict):
43
+ raise ValueError("SKILL.md frontmatter must be a YAML mapping.")
44
+ return parsed
45
+
46
+ raise ValueError("SKILL.md frontmatter is missing the closing '---' delimiter.")
47
+
48
+
49
+ def require_non_empty_string(container: dict[str, object], key: str, context: str, errors: list[str]) -> None:
50
+ value = container.get(key)
51
+ if not isinstance(value, str) or not value.strip():
52
+ errors.append(f"{context}: '{key}' must be a non-empty string.")
53
+
54
+
55
+ def validate_dependencies(dependencies: object, context: str, errors: list[str]) -> None:
56
+ if not isinstance(dependencies, dict):
57
+ errors.append(f"{context}: 'dependencies' must be a mapping.")
58
+ return
59
+
60
+ tools = dependencies.get("tools")
61
+ if tools is None:
62
+ return
63
+ if not isinstance(tools, list):
64
+ errors.append(f"{context}: 'dependencies.tools' must be a list.")
65
+ return
66
+
67
+ for index, item in enumerate(tools):
68
+ item_context = f"{context}: dependencies.tools[{index}]"
69
+ if not isinstance(item, dict):
70
+ errors.append(f"{item_context} must be a mapping.")
71
+ continue
72
+ require_non_empty_string(item, "type", item_context, errors)
73
+ require_non_empty_string(item, "value", item_context, errors)
74
+
75
+ tool_type = item.get("type")
76
+ if isinstance(tool_type, str) and tool_type != "mcp":
77
+ errors.append(f"{item_context}: unsupported tool type '{tool_type}', only 'mcp' is allowed.")
78
+
79
+ for optional_key in ("description", "transport", "url"):
80
+ optional_value = item.get(optional_key)
81
+ if optional_value is not None and (not isinstance(optional_value, str) or not optional_value.strip()):
82
+ errors.append(f"{item_context}: '{optional_key}' must be a non-empty string when provided.")
83
+
84
+
85
+ def validate_policy(policy: object, context: str, errors: list[str]) -> None:
86
+ if not isinstance(policy, dict):
87
+ errors.append(f"{context}: 'policy' must be a mapping.")
88
+ return
89
+
90
+ allow_implicit = policy.get("allow_implicit_invocation")
91
+ if allow_implicit is not None and not isinstance(allow_implicit, bool):
92
+ errors.append(
93
+ f"{context}: 'policy.allow_implicit_invocation' must be a boolean when provided."
94
+ )
95
+
96
+
97
+ def validate_skill(skill_dir: Path) -> list[str]:
98
+ errors: list[str] = []
99
+ skill_md = skill_dir / "SKILL.md"
100
+ openai_yaml = skill_dir / "agents" / "openai.yaml"
101
+
102
+ try:
103
+ skill_frontmatter = extract_frontmatter(skill_md.read_text(encoding="utf-8"))
104
+ except (OSError, ValueError, yaml.YAMLError) as exc:
105
+ return [f"{skill_md}: unable to read skill name for validation ({exc})."]
106
+
107
+ skill_name = skill_frontmatter.get("name")
108
+ if not isinstance(skill_name, str) or not skill_name.strip():
109
+ return [f"{skill_md}: frontmatter 'name' must be a non-empty string."]
110
+
111
+ if not openai_yaml.is_file():
112
+ return [f"{openai_yaml}: file is required for every skill."]
113
+
114
+ try:
115
+ parsed = yaml.safe_load(openai_yaml.read_text(encoding="utf-8"))
116
+ except (OSError, yaml.YAMLError) as exc:
117
+ return [f"{openai_yaml}: invalid YAML ({exc})."]
118
+
119
+ if not isinstance(parsed, dict):
120
+ return [f"{openai_yaml}: top-level structure must be a YAML mapping."]
121
+
122
+ top_level_keys = set(parsed.keys())
123
+ unsupported_top_keys = sorted(top_level_keys - TOP_LEVEL_ALLOWED_KEYS)
124
+ if unsupported_top_keys:
125
+ errors.append(
126
+ f"{openai_yaml}: unsupported top-level keys: {', '.join(unsupported_top_keys)}."
127
+ )
128
+
129
+ interface = parsed.get("interface")
130
+ if not isinstance(interface, dict):
131
+ errors.append(f"{openai_yaml}: 'interface' must be a mapping.")
132
+ return errors
133
+
134
+ missing_interface_keys = sorted(INTERFACE_REQUIRED_KEYS - set(interface.keys()))
135
+ if missing_interface_keys:
136
+ errors.append(
137
+ f"{openai_yaml}: missing required interface keys: {', '.join(missing_interface_keys)}."
138
+ )
139
+
140
+ unsupported_interface_keys = sorted(set(interface.keys()) - INTERFACE_ALLOWED_KEYS)
141
+ if unsupported_interface_keys:
142
+ errors.append(
143
+ f"{openai_yaml}: unsupported interface keys: {', '.join(unsupported_interface_keys)}."
144
+ )
145
+
146
+ for required_key in sorted(INTERFACE_REQUIRED_KEYS):
147
+ require_non_empty_string(interface, required_key, str(openai_yaml), errors)
148
+
149
+ default_prompt = interface.get("default_prompt")
150
+ expected_skill_ref = f"${skill_name.strip()}"
151
+ if isinstance(default_prompt, str) and expected_skill_ref not in default_prompt:
152
+ errors.append(
153
+ f"{openai_yaml}: interface.default_prompt must reference '{expected_skill_ref}'."
154
+ )
155
+
156
+ brand_color = interface.get("brand_color")
157
+ if brand_color is not None:
158
+ if not isinstance(brand_color, str) or not HEX_COLOR_PATTERN.fullmatch(brand_color):
159
+ errors.append(f"{openai_yaml}: interface.brand_color must be a hex color like '#1A2B3C'.")
160
+
161
+ dependencies = parsed.get("dependencies")
162
+ if dependencies is not None:
163
+ validate_dependencies(dependencies, str(openai_yaml), errors)
164
+
165
+ policy = parsed.get("policy")
166
+ if policy is not None:
167
+ validate_policy(policy, str(openai_yaml), errors)
168
+
169
+ return errors
170
+
171
+
172
+ def main() -> int:
173
+ root = repo_root()
174
+ skill_dirs = iter_skill_dirs(root)
175
+ if not skill_dirs:
176
+ print("No top-level skill directories found.")
177
+ return 1
178
+
179
+ all_errors: list[str] = []
180
+ for skill_dir in skill_dirs:
181
+ all_errors.extend(validate_skill(skill_dir))
182
+
183
+ if all_errors:
184
+ print("agents/openai.yaml validation failed:")
185
+ for error in all_errors:
186
+ print(f"- {error}")
187
+ return 1
188
+
189
+ print(f"agents/openai.yaml validation passed for {len(skill_dirs)} skills.")
190
+ return 0
191
+
192
+
193
+ if __name__ == "__main__":
194
+ sys.exit(main())
@@ -0,0 +1,110 @@
1
+ #!/usr/bin/env python3
2
+ """Validate SKILL.md frontmatter for all top-level skills."""
3
+
4
+ from __future__ import annotations
5
+
6
+ import re
7
+ import sys
8
+ from pathlib import Path
9
+
10
+ import yaml
11
+
12
+ NAME_PATTERN = re.compile(r"^[a-z0-9]+(?:-[a-z0-9]+)*$")
13
+ REQUIRED_KEYS = {"name", "description"}
14
+
15
+
16
+ def repo_root() -> Path:
17
+ return Path(__file__).resolve().parent.parent
18
+
19
+
20
+ def iter_skill_dirs(root: Path) -> list[Path]:
21
+ return sorted(path for path in root.iterdir() if path.is_dir() and (path / "SKILL.md").is_file())
22
+
23
+
24
+ def extract_frontmatter(content: str) -> str:
25
+ lines = content.splitlines()
26
+ if not lines or lines[0].strip() != "---":
27
+ raise ValueError("SKILL.md must start with YAML frontmatter delimiter '---'.")
28
+
29
+ for index in range(1, len(lines)):
30
+ if lines[index].strip() == "---":
31
+ return "\n".join(lines[1:index])
32
+
33
+ raise ValueError("SKILL.md frontmatter is missing the closing '---' delimiter.")
34
+
35
+
36
+ def validate_skill(skill_dir: Path) -> list[str]:
37
+ errors: list[str] = []
38
+ skill_md = skill_dir / "SKILL.md"
39
+
40
+ try:
41
+ content = skill_md.read_text(encoding="utf-8")
42
+ except OSError as exc:
43
+ return [f"{skill_md}: cannot read file ({exc})."]
44
+
45
+ try:
46
+ frontmatter_text = extract_frontmatter(content)
47
+ except ValueError as exc:
48
+ return [f"{skill_md}: {exc}"]
49
+
50
+ try:
51
+ frontmatter = yaml.safe_load(frontmatter_text)
52
+ except yaml.YAMLError as exc:
53
+ return [f"{skill_md}: invalid YAML in frontmatter ({exc})."]
54
+
55
+ if not isinstance(frontmatter, dict):
56
+ return [f"{skill_md}: frontmatter must be a YAML mapping."]
57
+
58
+ keys = set(frontmatter.keys())
59
+ if keys != REQUIRED_KEYS:
60
+ missing = sorted(REQUIRED_KEYS - keys)
61
+ extra = sorted(keys - REQUIRED_KEYS)
62
+ if missing:
63
+ errors.append(f"{skill_md}: missing required frontmatter keys: {', '.join(missing)}.")
64
+ if extra:
65
+ errors.append(f"{skill_md}: unsupported frontmatter keys: {', '.join(extra)}.")
66
+
67
+ name = frontmatter.get("name")
68
+ if not isinstance(name, str) or not name.strip():
69
+ errors.append(f"{skill_md}: 'name' must be a non-empty string.")
70
+ else:
71
+ normalized_name = name.strip()
72
+ if not NAME_PATTERN.fullmatch(normalized_name):
73
+ errors.append(
74
+ f"{skill_md}: 'name' must be kebab-case (lowercase letters, digits, and hyphens)."
75
+ )
76
+ if normalized_name != skill_dir.name:
77
+ errors.append(
78
+ f"{skill_md}: frontmatter name '{normalized_name}' must match folder name '{skill_dir.name}'."
79
+ )
80
+
81
+ description = frontmatter.get("description")
82
+ if not isinstance(description, str) or not description.strip():
83
+ errors.append(f"{skill_md}: 'description' must be a non-empty string.")
84
+
85
+ return errors
86
+
87
+
88
+ def main() -> int:
89
+ root = repo_root()
90
+ skill_dirs = iter_skill_dirs(root)
91
+ if not skill_dirs:
92
+ print("No top-level skill directories found.")
93
+ return 1
94
+
95
+ all_errors: list[str] = []
96
+ for skill_dir in skill_dirs:
97
+ all_errors.extend(validate_skill(skill_dir))
98
+
99
+ if all_errors:
100
+ print("SKILL.md frontmatter validation failed:")
101
+ for error in all_errors:
102
+ print(f"- {error}")
103
+ return 1
104
+
105
+ print(f"SKILL.md frontmatter validation passed for {len(skill_dirs)} skills.")
106
+ return 0
107
+
108
+
109
+ if __name__ == "__main__":
110
+ sys.exit(main())
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Lai Tsz Kin
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,57 @@
1
+ # specs-to-project-docs
2
+
3
+ A documentation skill that consolidates scattered spec files into standardized project docs. It produces a concise `README.md` plus a categorized document set grounded in real repository evidence.
4
+
5
+ ## Core capabilities
6
+
7
+ - Scans `spec.md`, `tasks.md`, and `checklist.md` collections as documentation input.
8
+ - Reconciles spec claims against code, config, scripts, and deployment files.
9
+ - Standardizes both new and existing project docs into topic-based files for setup, configuration, architecture, features, and developer onboarding.
10
+ - Provides dedicated reference templates for the top-level README, the documentation index/reference list, and each category document.
11
+ - Deletes superseded spec source files after a successful conversion, unless they still need to stay active or be archived explicitly.
12
+ - Keeps unknown or unverifiable details explicit instead of guessing.
13
+
14
+ ## Repository layout
15
+
16
+ ```text
17
+ .
18
+ ├── SKILL.md
19
+ ├── README.md
20
+ ├── LICENSE
21
+ ├── agents/
22
+ │ └── openai.yaml
23
+ └── references/
24
+ └── templates/
25
+ ├── readme.md
26
+ ├── docs-index.md
27
+ ├── getting-started.md
28
+ ├── configuration.md
29
+ ├── architecture.md
30
+ ├── features.md
31
+ └── developer-guide.md
32
+ ```
33
+
34
+ ## Default outputs
35
+
36
+ - `README.md`
37
+ - `docs/README.md`
38
+ - `docs/getting-started.md`
39
+ - `docs/configuration.md`
40
+ - `docs/architecture.md`
41
+ - `docs/features.md`
42
+ - `docs/developer-guide.md`
43
+
44
+ ## Required doc coverage
45
+
46
+ - Installation and deployment
47
+ - Configuration and environment variables
48
+ - External services and API key acquisition/setup steps when applicable
49
+ - Project architecture
50
+ - Project feature introductions
51
+ - Information developers should understand before changing the project
52
+
53
+ ## Notes
54
+
55
+ - Prefer code, config, and deployment files over stale spec text when they disagree.
56
+ - If the repository already has docs, rewrite them into the same categorized structure instead of leaving mixed documentation formats.
57
+ - Keep `README.md` short and let `docs/README.md` act as the reference list for deeper topic docs.