@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,11 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd -P)"
5
+
6
+ if ! command -v python3 >/dev/null 2>&1; then
7
+ echo "[ERROR] python3 is required." >&2
8
+ exit 1
9
+ fi
10
+
11
+ exec python3 "$script_dir/docs_to_voice.py" "$@"
@@ -0,0 +1,210 @@
1
+ import http.client
2
+ import unittest
3
+ from unittest.mock import patch
4
+
5
+ from scripts.docs_to_voice import (
6
+ DocsToVoiceError,
7
+ api_text_length_units,
8
+ discover_api_max_chars,
9
+ extract_max_chars_from_text,
10
+ fetch_api_model_max_chars,
11
+ is_max_chars_disabled,
12
+ probe_api_max_chars,
13
+ request_model_studio_audio,
14
+ split_text_for_tts,
15
+ )
16
+
17
+
18
+ class ExtractMaxCharsFromTextTests(unittest.TestCase):
19
+ def test_extracts_limit_from_range_message(self):
20
+ message = (
21
+ "Model Studio TTS request failed (HTTP 400): "
22
+ "InvalidParameter: Range of input length should be [0, 600]"
23
+ )
24
+ self.assertEqual(extract_max_chars_from_text(message), 600)
25
+
26
+ def test_extracts_limit_with_comma(self):
27
+ message = "Maximum input length is 1,200 characters."
28
+ self.assertEqual(extract_max_chars_from_text(message), 1200)
29
+
30
+ def test_extracts_limit_from_chinese_message(self):
31
+ message = "請求失敗:輸入長度上限為 600 字元"
32
+ self.assertEqual(extract_max_chars_from_text(message), 600)
33
+
34
+ def test_returns_none_for_unrelated_message(self):
35
+ self.assertIsNone(extract_max_chars_from_text("network timeout"))
36
+
37
+
38
+ class MaxCharsDisabledTests(unittest.TestCase):
39
+ def test_zero_string_disables_chunking(self):
40
+ self.assertTrue(is_max_chars_disabled("0"))
41
+
42
+ def test_non_zero_does_not_disable_chunking(self):
43
+ self.assertFalse(is_max_chars_disabled("1200"))
44
+
45
+
46
+ class ApiTextLengthUnitsTests(unittest.TestCase):
47
+ def test_counts_chinese_as_two_units(self):
48
+ self.assertEqual(api_text_length_units("AB測試!"), 7)
49
+
50
+ def test_split_text_respects_weighted_units(self):
51
+ chunks = split_text_for_tts("測" * 301, 600, length_func=api_text_length_units)
52
+ self.assertEqual(len(chunks), 2)
53
+ self.assertEqual(chunks[0], "測" * 300)
54
+ self.assertEqual(chunks[1], "測")
55
+
56
+
57
+ class FetchApiModelMaxCharsTests(unittest.TestCase):
58
+ @patch("scripts.docs_to_voice.fetch_json_payload")
59
+ def test_fetches_limit_from_model_catalog(self, mock_fetch_json_payload):
60
+ mock_fetch_json_payload.return_value = {
61
+ "output": {
62
+ "total": 1,
63
+ "models": [
64
+ {
65
+ "model": "qwen3-tts",
66
+ "model_info": {"max_input_tokens": 600},
67
+ }
68
+ ],
69
+ }
70
+ }
71
+
72
+ result = fetch_api_model_max_chars(
73
+ api_endpoint="https://dashscope-intl.aliyuncs.com/api/v1/services/aigc/multimodal-generation/generation",
74
+ api_key="test-key",
75
+ model="qwen3-tts",
76
+ )
77
+
78
+ self.assertEqual(result, 600)
79
+
80
+ @patch("scripts.docs_to_voice.fetch_json_payload")
81
+ def test_returns_none_when_catalog_request_fails(self, mock_fetch_json_payload):
82
+ mock_fetch_json_payload.side_effect = RuntimeError("network error")
83
+
84
+ result = fetch_api_model_max_chars(
85
+ api_endpoint="https://dashscope-intl.aliyuncs.com/api/v1/services/aigc/multimodal-generation/generation",
86
+ api_key="test-key",
87
+ model="qwen3-tts",
88
+ )
89
+
90
+ self.assertIsNone(result)
91
+
92
+ @patch("scripts.docs_to_voice.fetch_json_payload")
93
+ def test_fetches_limit_from_later_page_when_needed(self, mock_fetch_json_payload):
94
+ mock_fetch_json_payload.side_effect = [
95
+ {
96
+ "output": {
97
+ "total": 101,
98
+ "models": [{"model": "other-model", "model_info": {}}],
99
+ }
100
+ },
101
+ {
102
+ "output": {
103
+ "total": 101,
104
+ "models": [
105
+ {
106
+ "model": "qwen3-tts",
107
+ "description": "Range of input length should be [0, 800]",
108
+ }
109
+ ],
110
+ }
111
+ },
112
+ ]
113
+
114
+ result = fetch_api_model_max_chars(
115
+ api_endpoint="https://dashscope-intl.aliyuncs.com/api/v1/services/aigc/multimodal-generation/generation",
116
+ api_key="test-key",
117
+ model="qwen3-tts",
118
+ )
119
+
120
+ self.assertEqual(result, 800)
121
+ self.assertEqual(mock_fetch_json_payload.call_count, 2)
122
+
123
+
124
+ class ProbeApiMaxCharsTests(unittest.TestCase):
125
+ @patch("scripts.docs_to_voice.request_model_studio_audio")
126
+ def test_probe_extracts_limit_from_api_error(self, mock_request_model_studio_audio):
127
+ mock_request_model_studio_audio.side_effect = DocsToVoiceError(
128
+ "Model Studio TTS request failed (HTTP 400): "
129
+ "InvalidParameter: Range of input length should be [0, 600]"
130
+ )
131
+
132
+ result = probe_api_max_chars(
133
+ api_endpoint="https://dashscope-intl.aliyuncs.com/api/v1/services/aigc/multimodal-generation/generation",
134
+ api_key="test-key",
135
+ model="qwen3-tts",
136
+ voice="Cherry",
137
+ )
138
+
139
+ self.assertEqual(result, 600)
140
+
141
+ @patch("scripts.docs_to_voice.request_model_studio_audio")
142
+ def test_probe_returns_none_when_probe_request_succeeds(self, mock_request_model_studio_audio):
143
+ mock_request_model_studio_audio.return_value = {
144
+ "audio_url": "https://example.com/audio.wav",
145
+ "audio_data": "",
146
+ "audio_format": "wav",
147
+ }
148
+
149
+ result = probe_api_max_chars(
150
+ api_endpoint="https://dashscope-intl.aliyuncs.com/api/v1/services/aigc/multimodal-generation/generation",
151
+ api_key="test-key",
152
+ model="qwen3-tts",
153
+ voice="Cherry",
154
+ )
155
+
156
+ self.assertIsNone(result)
157
+
158
+
159
+ class DiscoverApiMaxCharsTests(unittest.TestCase):
160
+ @patch("scripts.docs_to_voice.probe_api_max_chars")
161
+ @patch("scripts.docs_to_voice.fetch_api_model_max_chars")
162
+ def test_prefers_catalog_limit(self, mock_fetch_api_model_max_chars, mock_probe_api_max_chars):
163
+ mock_fetch_api_model_max_chars.return_value = 700
164
+
165
+ result = discover_api_max_chars(
166
+ api_endpoint="https://dashscope-intl.aliyuncs.com/api/v1/services/aigc/multimodal-generation/generation",
167
+ api_key="test-key",
168
+ model="qwen3-tts",
169
+ voice="Cherry",
170
+ )
171
+
172
+ self.assertEqual(result, 700)
173
+ mock_probe_api_max_chars.assert_not_called()
174
+
175
+ @patch("scripts.docs_to_voice.probe_api_max_chars")
176
+ @patch("scripts.docs_to_voice.fetch_api_model_max_chars")
177
+ def test_falls_back_to_probe_limit(self, mock_fetch_api_model_max_chars, mock_probe_api_max_chars):
178
+ mock_fetch_api_model_max_chars.return_value = None
179
+ mock_probe_api_max_chars.return_value = 600
180
+
181
+ result = discover_api_max_chars(
182
+ api_endpoint="https://dashscope-intl.aliyuncs.com/api/v1/services/aigc/multimodal-generation/generation",
183
+ api_key="test-key",
184
+ model="qwen3-tts",
185
+ voice="Cherry",
186
+ )
187
+
188
+ self.assertEqual(result, 600)
189
+ mock_probe_api_max_chars.assert_called_once()
190
+
191
+
192
+ class RequestModelStudioAudioTests(unittest.TestCase):
193
+ @patch("scripts.docs_to_voice.urllib.request.urlopen")
194
+ def test_wraps_http_client_disconnect_as_user_error(self, mock_urlopen):
195
+ mock_urlopen.side_effect = http.client.RemoteDisconnected(
196
+ "Remote end closed connection without response"
197
+ )
198
+
199
+ with self.assertRaisesRegex(DocsToVoiceError, "Remote end closed connection"):
200
+ request_model_studio_audio(
201
+ api_endpoint="https://dashscope-intl.aliyuncs.com/api/v1/services/aigc/multimodal-generation/generation",
202
+ api_key="test-key",
203
+ model="qwen3-tts",
204
+ voice="Cherry",
205
+ text="test",
206
+ )
207
+
208
+
209
+ if __name__ == "__main__":
210
+ unittest.main()
@@ -0,0 +1,115 @@
1
+ import json
2
+ import pathlib
3
+ import tempfile
4
+ import unittest
5
+ from unittest.mock import patch
6
+
7
+ from scripts.docs_to_voice import (
8
+ DocsToVoiceError,
9
+ api_text_length_units,
10
+ split_text_into_api_sentence_requests,
11
+ write_sentence_timeline_files,
12
+ )
13
+
14
+
15
+ class ApiSentenceRequestTests(unittest.TestCase):
16
+ def test_builds_one_request_per_sentence_by_default(self):
17
+ source_text = "第一句。第二句!第三句?"
18
+ sentences, request_items = split_text_into_api_sentence_requests(
19
+ source_text=source_text,
20
+ max_chars=None,
21
+ length_func=api_text_length_units,
22
+ )
23
+
24
+ self.assertEqual(sentences, ["第一句。", "第二句!", "第三句?"])
25
+ self.assertEqual([item["sentence_index"] for item in request_items], [0, 1, 2])
26
+ self.assertEqual([item["text"] for item in request_items], sentences)
27
+
28
+ def test_splits_oversized_sentence_and_keeps_sentence_index(self):
29
+ source_text = "測" * 301
30
+ sentences, request_items = split_text_into_api_sentence_requests(
31
+ source_text=source_text,
32
+ max_chars=600,
33
+ length_func=api_text_length_units,
34
+ )
35
+
36
+ self.assertEqual(sentences, [source_text])
37
+ self.assertEqual(len(request_items), 2)
38
+ self.assertEqual([item["sentence_index"] for item in request_items], [0, 0])
39
+ self.assertEqual(request_items[0]["text"], "測" * 300)
40
+ self.assertEqual(request_items[1]["text"], "測")
41
+
42
+ def test_raises_when_text_is_empty(self):
43
+ with self.assertRaises(DocsToVoiceError):
44
+ split_text_into_api_sentence_requests(
45
+ source_text=" \n",
46
+ max_chars=600,
47
+ length_func=api_text_length_units,
48
+ )
49
+
50
+
51
+ class SentenceTimelineTests(unittest.TestCase):
52
+ def test_uses_sentence_audio_durations_for_timestamp(self):
53
+ with tempfile.TemporaryDirectory() as temp_dir:
54
+ temp_dir_path = pathlib.Path(temp_dir)
55
+ audio_path = temp_dir_path / "voice.wav"
56
+
57
+ with patch("scripts.docs_to_voice.read_duration_seconds", return_value=2.0):
58
+ write_sentence_timeline_files(
59
+ source_text="甲。乙。",
60
+ audio_path=audio_path,
61
+ sentence_durations=[1.2, 0.8],
62
+ timing_mode_hint="sentence-audio",
63
+ )
64
+
65
+ timeline_json_path = temp_dir_path / "voice.timeline.json"
66
+ timeline_payload = json.loads(timeline_json_path.read_text(encoding="utf-8"))
67
+
68
+ self.assertEqual(timeline_payload["timing_mode"], "sentence-audio")
69
+ self.assertEqual(timeline_payload["sentences"][0]["start_ms"], 0)
70
+ self.assertEqual(timeline_payload["sentences"][0]["end_ms"], 1200)
71
+ self.assertEqual(timeline_payload["sentences"][1]["start_ms"], 1200)
72
+ self.assertEqual(timeline_payload["sentences"][1]["end_ms"], 2000)
73
+
74
+ def test_scales_sentence_duration_to_match_output_duration(self):
75
+ with tempfile.TemporaryDirectory() as temp_dir:
76
+ temp_dir_path = pathlib.Path(temp_dir)
77
+ audio_path = temp_dir_path / "voice.wav"
78
+
79
+ with patch("scripts.docs_to_voice.read_duration_seconds", return_value=4.0):
80
+ write_sentence_timeline_files(
81
+ source_text="甲。乙。",
82
+ audio_path=audio_path,
83
+ sentence_durations=[1.0, 1.0],
84
+ timing_mode_hint="sentence-audio",
85
+ )
86
+
87
+ timeline_json_path = temp_dir_path / "voice.timeline.json"
88
+ timeline_payload = json.loads(timeline_json_path.read_text(encoding="utf-8"))
89
+
90
+ self.assertEqual(timeline_payload["timing_mode"], "sentence-audio")
91
+ self.assertEqual(timeline_payload["sentences"][0]["end_ms"], 2000)
92
+ self.assertEqual(timeline_payload["sentences"][1]["end_ms"], 4000)
93
+
94
+ def test_falls_back_when_sentence_duration_count_mismatched(self):
95
+ with tempfile.TemporaryDirectory() as temp_dir:
96
+ temp_dir_path = pathlib.Path(temp_dir)
97
+ audio_path = temp_dir_path / "voice.wav"
98
+
99
+ with patch("scripts.docs_to_voice.read_duration_seconds", return_value=2.0):
100
+ write_sentence_timeline_files(
101
+ source_text="甲。乙。",
102
+ audio_path=audio_path,
103
+ sentence_durations=[2.0],
104
+ timing_mode_hint="sentence-audio",
105
+ )
106
+
107
+ timeline_json_path = temp_dir_path / "voice.timeline.json"
108
+ timeline_payload = json.loads(timeline_json_path.read_text(encoding="utf-8"))
109
+
110
+ self.assertEqual(timeline_payload["timing_mode"], "duration-weighted")
111
+ self.assertEqual(timeline_payload["sentences"][1]["end_ms"], 2000)
112
+
113
+
114
+ if __name__ == "__main__":
115
+ unittest.main()
@@ -0,0 +1,43 @@
1
+ import os
2
+ import unittest
3
+
4
+ from scripts.docs_to_voice import resolve_setting
5
+
6
+
7
+ class ResolveSettingTests(unittest.TestCase):
8
+ def setUp(self):
9
+ self.env_key = "DOCS_TO_VOICE_TEST_SETTING"
10
+ self.previous = os.environ.get(self.env_key)
11
+ os.environ.pop(self.env_key, None)
12
+
13
+ def tearDown(self):
14
+ os.environ.pop(self.env_key, None)
15
+ if self.previous is not None:
16
+ os.environ[self.env_key] = self.previous
17
+
18
+ def test_cli_value_has_highest_priority(self):
19
+ os.environ[self.env_key] = "env-value"
20
+ result = resolve_setting("cli-value", self.env_key, {"DOCS_TO_VOICE_TEST_SETTING": "file-value"})
21
+ self.assertEqual(result, "cli-value")
22
+
23
+ def test_env_file_has_priority_over_process_env(self):
24
+ os.environ[self.env_key] = "env-value"
25
+ result = resolve_setting(None, self.env_key, {"DOCS_TO_VOICE_TEST_SETTING": "file-value"})
26
+ self.assertEqual(result, "file-value")
27
+
28
+ def test_blank_cli_value_falls_back_to_env_file(self):
29
+ result = resolve_setting(" ", self.env_key, {"DOCS_TO_VOICE_TEST_SETTING": "file-value"})
30
+ self.assertEqual(result, "file-value")
31
+
32
+ def test_process_env_used_when_env_file_missing(self):
33
+ os.environ[self.env_key] = "env-value"
34
+ result = resolve_setting(None, self.env_key, {})
35
+ self.assertEqual(result, "env-value")
36
+
37
+ def test_default_used_when_no_source_available(self):
38
+ result = resolve_setting(None, self.env_key, {}, "default-value")
39
+ self.assertEqual(result, "default-value")
40
+
41
+
42
+ if __name__ == "__main__":
43
+ unittest.main()
@@ -0,0 +1,57 @@
1
+ import unittest
2
+
3
+ from scripts.docs_to_voice import (
4
+ DocsToVoiceError,
5
+ build_atempo_filter_chain,
6
+ scale_sentence_durations,
7
+ validate_speech_rate,
8
+ )
9
+
10
+
11
+ class ValidateSpeechRateTests(unittest.TestCase):
12
+ def test_accepts_positive_float(self):
13
+ self.assertEqual(validate_speech_rate("1.25"), 1.25)
14
+
15
+ def test_returns_none_for_blank_value(self):
16
+ self.assertIsNone(validate_speech_rate(" "))
17
+
18
+ def test_rejects_non_positive_value(self):
19
+ with self.assertRaisesRegex(DocsToVoiceError, "--speech-rate"):
20
+ validate_speech_rate("0")
21
+
22
+ def test_rejects_non_numeric_value(self):
23
+ with self.assertRaisesRegex(DocsToVoiceError, "--speech-rate"):
24
+ validate_speech_rate("fast")
25
+
26
+ def test_rejects_non_finite_values(self):
27
+ for value in ("nan", "inf", "-inf"):
28
+ with self.subTest(value=value):
29
+ with self.assertRaisesRegex(DocsToVoiceError, "--speech-rate"):
30
+ validate_speech_rate(value)
31
+
32
+
33
+ class BuildAtempoFilterChainTests(unittest.TestCase):
34
+ def test_builds_single_stage_filter(self):
35
+ self.assertEqual(build_atempo_filter_chain(1.25), "atempo=1.25")
36
+
37
+ def test_builds_multi_stage_filter_for_high_rate(self):
38
+ self.assertEqual(build_atempo_filter_chain(4.0), "atempo=2.0,atempo=2.0")
39
+
40
+ def test_builds_multi_stage_filter_for_low_rate(self):
41
+ self.assertEqual(build_atempo_filter_chain(0.25), "atempo=0.5,atempo=0.5")
42
+
43
+
44
+ class ScaleSentenceDurationsTests(unittest.TestCase):
45
+ def test_scales_all_sentence_durations(self):
46
+ self.assertEqual(
47
+ scale_sentence_durations([1.2, 0.8], 2.0),
48
+ [0.6, 0.4],
49
+ )
50
+
51
+ def test_returns_original_when_rate_is_one(self):
52
+ values = [1.2, 0.8]
53
+ self.assertEqual(scale_sentence_durations(values, 1.0), values)
54
+
55
+
56
+ if __name__ == "__main__":
57
+ unittest.main()
@@ -0,0 +1,35 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project are documented in this file.
4
+
5
+ ## [v0.2.0] - 2026-02-25
6
+
7
+ ### Changed
8
+ - Replaced planning flow with conditional specs workflow (`spec.md` / `tasks.md` / `checklist.md`).
9
+ - Clarified specs trigger scope: high complexity, critical module, or cross-module changes.
10
+ - Added explicit rule that test coverage is required even when specs are not used.
11
+
12
+ ### Added
13
+ - Added `scripts/create-specs` for generating specs into `docs/plans/{YYYY-MM-DD}_{change_name}/`.
14
+ - Added reusable templates under `references/templates/`.
15
+
16
+ ## [v0.1.2] - 2026-02-12
17
+
18
+ ### Changed
19
+ - Tightened multi-module planning workflow to require explicit user approval before implementation.
20
+ - Updated usage docs and agent metadata for approval gating clarity.
21
+
22
+ ## [v0.1.1] - 2026-02-12
23
+
24
+ ### Added
25
+ - Added built-in planning script and planning template for cross-module feature changes.
26
+
27
+ ### Changed
28
+ - Updated the skill workflow to require planning before implementation when a request spans multiple modules.
29
+ - Updated skill metadata and usage docs to reflect planning-first behavior for multi-module work.
30
+ - Refreshed repository introduction wording for clearer agent-skill positioning.
31
+
32
+ ## [v0.1.0] - 2026-02-07
33
+
34
+ ### Added
35
+ - Initial release of the `enhance-existing-features` skill for dependency mapping, authoritative doc verification, focused implementation, and test updates in brownfield codebases.
@@ -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,54 @@
1
+ # enhance-existing-features
2
+
3
+ A brownfield feature-extension skill: map dependencies first, decide whether shared specs are required, then implement the approved change with risk-driven hardening tests.
4
+
5
+ ## Core capabilities
6
+
7
+ - Explores dependencies and data flow before deciding how to change the system.
8
+ - Uses `generate-spec` whenever the change is high-complexity, touches a critical module, or crosses module boundaries.
9
+ - Requires explicit approval before coding when specs are generated.
10
+ - Still requires meaningful tests even when specs are skipped.
11
+ - Keeps brownfield changes focused and traceable.
12
+
13
+ ## Repository layout
14
+
15
+ ```text
16
+ .
17
+ ├── SKILL.md
18
+ ├── README.md
19
+ ├── LICENSE
20
+ ├── agents/
21
+ │ └── openai.yaml
22
+ └── references/
23
+ ├── unit-tests.md
24
+ ├── property-based-tests.md
25
+ ├── integration-tests.md
26
+ └── e2e-tests.md
27
+ ```
28
+
29
+ ## Workflow summary
30
+
31
+ 1. Explore the existing codebase and affected logic chain first.
32
+ 2. Trigger `generate-spec` only when the change is high complexity, hits a critical module, or crosses module boundaries.
33
+ 3. Wait for explicit approval if planning docs were generated.
34
+ 4. Implement the smallest safe brownfield change.
35
+ 5. Run risk-driven tests and backfill planning docs when they exist.
36
+
37
+ ## Test requirements
38
+
39
+ - Unit: changed logic, boundaries, failure paths.
40
+ - Regression: bug-prone or high-risk behavior that must not silently return.
41
+ - Property-based: mandatory for business logic unless concrete `N/A` is recorded.
42
+ - Integration: user-critical logic chain across layers/modules.
43
+ - E2E: affected key user-visible path when the risk justifies it.
44
+ - Adversarial: abuse paths, malformed inputs, privilege issues, replay, concurrency, and edge combinations when relevant.
45
+
46
+ If E2E is not feasible, replace it with stronger integration coverage and record the reason.
47
+
48
+ ## References
49
+
50
+ - Shared planning workflow: `generate-spec`
51
+ - Unit testing guide: `references/unit-tests.md`
52
+ - Property-based testing guide: `references/property-based-tests.md`
53
+ - Integration testing guide: `references/integration-tests.md`
54
+ - E2E testing guide: `references/e2e-tests.md`
@@ -0,0 +1,120 @@
1
+ ---
2
+ name: enhance-existing-features
3
+ description: >-
4
+ Build and extend brownfield features in an existing codebase. Always explore
5
+ the codebase first, then decide from the user's requested change whether
6
+ specs (`spec.md`/`tasks.md`/`checklist.md`) are required before coding. When
7
+ specs are required, depend on `generate-spec` for the shared planning,
8
+ clarification, approval, and backfill workflow. Even when specs are not
9
+ required, still add and run related tests for
10
+ unit/property-based/user-critical integration chain/E2E coverage. Tests must
11
+ not stop at happy-path validation: for business-logic changes require
12
+ property-based testing unless explicitly `N/A` with reason, design
13
+ adversarial/regression/authorization/idempotency/concurrency coverage where
14
+ relevant, use mocks for external services in logic chains, and verify
15
+ meaningful business outcomes rather than smoke-only success.
16
+ ---
17
+
18
+ # Enhance Existing Features
19
+
20
+ ## Dependencies
21
+
22
+ - Required: `generate-spec` for shared planning docs when spec-trigger conditions are met.
23
+ - Conditional: none.
24
+ - Optional: none.
25
+ - Fallback: If specs are required and `generate-spec` is unavailable, stop and report the missing dependency.
26
+
27
+ ## Standards
28
+
29
+ - Evidence: Explore the existing codebase first and verify the latest authoritative docs for the involved stack or integrations.
30
+ - Execution: Decide whether specs are required from the actual change surface, run `generate-spec` when needed, then implement minimally.
31
+ - Quality: Add risk-based tests with property-based, regression, integration, E2E, adversarial, and rollback coverage when relevant.
32
+ - Output: Keep implementation and any planning artifacts traceable, updated, and aligned with actual completion results.
33
+
34
+ ## Overview
35
+
36
+ Safely extend brownfield systems by exploring the existing codebase first, using the user's requested change plus discovered impact to decide whether specs are needed, then following a consistent implementation and testing workflow with minimal, well-validated changes.
37
+
38
+ ## Workflow
39
+
40
+ ### 1) Explore codebase first
41
+
42
+ - Read the relevant existing code before deciding process or editing anything.
43
+ - Locate entrypoints, configuration, and primary data flow.
44
+ - Trace module relationships (imports, call graph, shared models, side effects).
45
+ - Identify integration points (DB, RPC, external APIs, queues, filesystems).
46
+ - Identify user-critical logic chains affected by the change.
47
+ - Summarize findings and the likely change surface before editing.
48
+
49
+ ### 2) Decide whether specs are required from the requested change
50
+
51
+ Use the user's requested change together with the codebase exploration results to decide whether to generate specs.
52
+
53
+ Trigger specs when any of the following is true:
54
+ - high complexity changes
55
+ - critical module changes
56
+ - cross-module changes
57
+
58
+ If triggered:
59
+ - Run `$generate-spec` and follow its workflow completely.
60
+ - Use it to create or update `docs/plans/{YYYY-MM-DD}_{change_name}/spec.md`, `tasks.md`, and `checklist.md`.
61
+ - Ensure planned behaviors and edge cases cover external dependency states, abuse/adversarial paths, and any relevant authorization/idempotency/concurrency/data-integrity risks.
62
+ - If users answer clarification questions, update the planning docs and obtain explicit approval again before implementation.
63
+ - Do not modify implementation code before approval.
64
+
65
+ If not triggered:
66
+ - Continue directly with the same downstream workflow below.
67
+
68
+ ### 3) Verify latest authoritative docs
69
+
70
+ - Identify the tech stack, libraries, and external dependencies involved.
71
+ - Use official documentation as the source of truth.
72
+ - Prefer Context7 for framework/library APIs; use web for latest official docs.
73
+ - If required docs are private or missing, request access or user-provided references.
74
+
75
+ ### 4) Implement the feature
76
+
77
+ - Reuse existing patterns and abstractions; avoid over-engineering.
78
+ - Keep changes focused and minimal; preserve current behavior unless required.
79
+ - Follow project conventions (naming, linting, formatting, configuration).
80
+ - Update environment examples only when new inputs are required.
81
+
82
+ ### 5) Testing coverage (required with or without specs)
83
+
84
+ For every non-trivial change, evaluate all categories and add test cases or record justified `N/A`:
85
+ - Start from a risk inventory, not from the happy path: assess misuse/abuse, authorization, invalid transitions, idempotency, replay/duplication, concurrency/races, data-integrity, and partial-failure/rollback risks.
86
+ - Unit tests: changed logic, boundaries, failure paths, and exact error/side-effect expectations.
87
+ - Regression tests: bug-prone or high-risk behavior that should never silently regress again.
88
+ - Property-based tests: required for business-logic changes unless truly unsuitable; use them for invariants, generated business input spaces, state-machine/metamorphic checks when useful, and output expectation checks.
89
+ - Integration tests: user-critical logic chain across modules/layers.
90
+ - E2E tests: key user-visible path impacted by this change; prefer one minimal critical success path plus one highest-value denial/failure path when the risk warrants it.
91
+ - Adversarial/penetration-style cases: abuse paths, malformed inputs, forged identities/privileges, invalid transitions, replay/duplication, stale/out-of-order events, toxic payload sizes, and risky edge combinations.
92
+
93
+ Rules:
94
+ - If E2E is too costly or unstable, add stronger integration coverage for the same risk and record the reason.
95
+ - If property-based testing is not suitable, record `N/A` with a concrete reason.
96
+ - For logic chains with external services, mock or fake those services unless the real contract itself is under test; simulate diverse external states and verify the business chain remains correct.
97
+ - Where the feature can partially commit work, test rollback, compensation, or no-partial-write behavior explicitly.
98
+ - Each test must assert a meaningful oracle: exact business output, persisted state, emitted side effects, or intentional lack of side effects. Avoid assertion-light smoke tests and snapshot-only coverage.
99
+ - Run relevant tests when possible and fix failures.
100
+
101
+ ### 6) Completion updates
102
+
103
+ - If specs were used, backfill `tasks.md` and `checklist.md` through `$generate-spec` workflow based on actual completion and test outcomes.
104
+ - If specs were not used, provide a concise execution summary including test IDs/results, regression coverage, mock scenario coverage, adversarial coverage, and any `N/A` reasons.
105
+
106
+ ## Working Rules
107
+
108
+ - Keep the solution minimal and executable.
109
+ - Always decide the need for specs only after exploring the existing codebase.
110
+ - Maintain traceability between requirements, tasks, and tests when specs are present.
111
+ - Treat checklists as living artifacts: adjust items to match real change scope.
112
+ - Every planned test should justify a distinct risk; remove shallow duplicates that only prove the code "still runs".
113
+
114
+ ## References
115
+
116
+ - `$generate-spec`: shared planning and approval workflow.
117
+ - `references/unit-tests.md`: unit testing guidance.
118
+ - `references/property-based-tests.md`: property-based testing guidance.
119
+ - `references/integration-tests.md`: integration testing guidance.
120
+ - `references/e2e-tests.md`: E2E decision and design guidance.
@@ -0,0 +1,4 @@
1
+ interface:
2
+ display_name: "enhance-existing-features"
3
+ short_description: "Extend brownfield features with conditional generate-spec planning and risk-driven tests"
4
+ default_prompt: "Use $enhance-existing-features to extend a brownfield feature: map the affected code and dependencies first, decide whether the change is high complexity / critical module / cross-module, run $generate-spec when specs are required, wait for explicit approval before coding, and always add risk-driven tests plus clear N/A reasons when a category truly does not apply."