@laitszkin/apollo-toolkit 2.14.22 → 3.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 (58) hide show
  1. package/AGENTS.md +3 -0
  2. package/CHANGELOG.md +18 -0
  3. package/README.md +9 -0
  4. package/analyse-app-logs/README.md +5 -5
  5. package/analyse-app-logs/SKILL.md +7 -5
  6. package/analyse-app-logs/scripts/__pycache__/filter_logs_by_time.cpython-312.pyc +0 -0
  7. package/analyse-app-logs/scripts/__pycache__/log_cli_utils.cpython-312.pyc +0 -0
  8. package/analyse-app-logs/scripts/__pycache__/search_logs.cpython-312.pyc +0 -0
  9. package/codex/codex-memory-manager/README.md +2 -2
  10. package/codex/codex-memory-manager/SKILL.md +5 -5
  11. package/codex/codex-memory-manager/tests/test_extract_recent_conversations.py +3 -2
  12. package/codex/learn-skill-from-conversations/README.md +1 -1
  13. package/codex/learn-skill-from-conversations/SKILL.md +2 -2
  14. package/codex/learn-skill-from-conversations/tests/test_extract_recent_conversations.py +3 -2
  15. package/docs-to-voice/README.md +3 -3
  16. package/docs-to-voice/SKILL.md +4 -4
  17. package/docs-to-voice/scripts/__pycache__/docs_to_voice.cpython-312.pyc +0 -0
  18. package/docs-to-voice/tests/test_docs_to_voice_shell_wrapper.py +51 -0
  19. package/feature-propose/SKILL.md +1 -0
  20. package/generate-spec/README.md +3 -6
  21. package/generate-spec/SKILL.md +2 -3
  22. package/generate-spec/scripts/__pycache__/create-specscpython-312.pyc +0 -0
  23. package/generate-spec/tests/test_create_specs.py +166 -0
  24. package/improve-observability/SKILL.md +12 -1
  25. package/katex/SKILL.md +3 -3
  26. package/katex/scripts/__pycache__/render_katex.cpython-312.pyc +0 -0
  27. package/katex/tests/test_render_katex.py +174 -0
  28. package/learning-error-book/SKILL.md +2 -2
  29. package/learning-error-book/tests/test_render_error_book_json_to_pdf.py +134 -0
  30. package/lib/cli.js +66 -0
  31. package/lib/tool-runner.js +214 -0
  32. package/maintain-project-constraints/SKILL.md +3 -3
  33. package/maintain-skill-catalog/SKILL.md +2 -2
  34. package/novel-to-short-video/SKILL.md +2 -2
  35. package/open-github-issue/README.md +31 -22
  36. package/open-github-issue/SKILL.md +54 -40
  37. package/open-github-issue/scripts/__pycache__/open_github_issue.cpython-312.pyc +0 -0
  38. package/open-github-issue/scripts/open_github_issue.py +130 -3
  39. package/open-github-issue/tests/test_open_github_issue.py +95 -0
  40. package/openai-text-to-image-storyboard/README.md +1 -1
  41. package/openai-text-to-image-storyboard/SKILL.md +1 -1
  42. package/openai-text-to-image-storyboard/tests/test_generate_storyboard_images.py +177 -0
  43. package/package.json +1 -1
  44. package/read-github-issue/SKILL.md +9 -9
  45. package/read-github-issue/scripts/__pycache__/find_issues.cpython-312.pyc +0 -0
  46. package/read-github-issue/scripts/__pycache__/read_issue.cpython-312.pyc +0 -0
  47. package/resolve-review-comments/SKILL.md +8 -8
  48. package/resolve-review-comments/scripts/__pycache__/review_threads.cpython-312.pyc +0 -0
  49. package/review-codebases/README.md +2 -0
  50. package/review-codebases/SKILL.md +1 -0
  51. package/scheduled-runtime-health-check/SKILL.md +4 -1
  52. package/systematic-debug/SKILL.md +3 -2
  53. package/text-to-short-video/README.md +1 -1
  54. package/text-to-short-video/SKILL.md +1 -1
  55. package/text-to-short-video/scripts/__pycache__/enforce_video_aspect_ratio.cpython-312.pyc +0 -0
  56. package/text-to-short-video/tests/test_enforce_video_aspect_ratio.py +194 -0
  57. package/weekly-financial-event-report/SKILL.md +2 -2
  58. package/weekly-financial-event-report/tests/test_extract_pdf_text_pdfkit.py +64 -0
@@ -0,0 +1,177 @@
1
+ #!/usr/bin/env python3
2
+
3
+ from __future__ import annotations
4
+
5
+ import importlib.util
6
+ import io
7
+ import json
8
+ import random
9
+ import string
10
+ import tempfile
11
+ import types
12
+ import unittest
13
+ from pathlib import Path
14
+ from unittest.mock import patch
15
+
16
+ PIL_AVAILABLE = importlib.util.find_spec("PIL") is not None
17
+
18
+ if PIL_AVAILABLE:
19
+ from PIL import Image
20
+ SCRIPT_PATH = Path(__file__).resolve().parents[1] / "scripts" / "generate_storyboard_images.py"
21
+ SPEC = importlib.util.spec_from_file_location("generate_storyboard_images", SCRIPT_PATH)
22
+ MODULE = importlib.util.module_from_spec(SPEC)
23
+ SPEC.loader.exec_module(MODULE)
24
+ else:
25
+ Image = None
26
+ MODULE = None
27
+
28
+
29
+ def png_bytes(width: int, height: int, color: tuple[int, int, int] = (64, 128, 255)) -> bytes:
30
+ buffer = io.BytesIO()
31
+ Image.new("RGB", (width, height), color).save(buffer, format="PNG")
32
+ return buffer.getvalue()
33
+
34
+
35
+ @unittest.skipUnless(PIL_AVAILABLE, "Pillow is required for storyboard image tests")
36
+ class GenerateStoryboardImagesTests(unittest.TestCase):
37
+ def test_sanitize_component_property_removes_invalid_path_characters(self) -> None:
38
+ generator = random.Random(20260418)
39
+ alphabet = string.ascii_letters + string.digits + string.punctuation + " \t中文"
40
+
41
+ for _ in range(200):
42
+ raw = "".join(generator.choice(alphabet) for _ in range(generator.randint(0, 30)))
43
+ sanitized = MODULE.sanitize_component(raw, "fallback")
44
+ with self.subTest(raw=raw, sanitized=sanitized):
45
+ self.assertTrue(sanitized)
46
+ self.assertNotRegex(sanitized, MODULE.INVALID_PATH_CHARS)
47
+ self.assertNotIn(" ", sanitized)
48
+
49
+ def test_parse_image_dimensions_supports_png_and_jpeg(self) -> None:
50
+ png = png_bytes(320, 180)
51
+ self.assertEqual(MODULE.parse_image_dimensions(png), (320, 180))
52
+
53
+ buffer = io.BytesIO()
54
+ Image.new("RGB", (160, 90), (0, 0, 0)).save(buffer, format="JPEG")
55
+ self.assertEqual(MODULE.parse_image_dimensions(buffer.getvalue()), (160, 90))
56
+
57
+ def test_parse_prompts_file_supports_structured_scene_mode(self) -> None:
58
+ payload = {
59
+ "characters": [
60
+ {
61
+ "id": "hero",
62
+ "name": "Kai",
63
+ "appearance": "short black hair",
64
+ "outfit": "blue jacket",
65
+ "description": "determined teenage inventor",
66
+ }
67
+ ],
68
+ "scenes": [
69
+ {
70
+ "title": "Workshop",
71
+ "description": "Kai repairs a flickering lantern inside a cramped workshop.",
72
+ "character_ids": ["hero"],
73
+ "character_descriptions": {"hero": "focused and covered with grease"},
74
+ "camera": "medium shot",
75
+ }
76
+ ],
77
+ }
78
+
79
+ with tempfile.TemporaryDirectory() as temp_dir:
80
+ prompts_file = Path(temp_dir) / "prompts.json"
81
+ prompts_file.write_text(json.dumps(payload, ensure_ascii=False), encoding="utf-8")
82
+ items = MODULE.parse_prompts_file(prompts_file)
83
+
84
+ self.assertEqual(len(items), 1)
85
+ self.assertEqual(items[0]["title"], "Workshop")
86
+ prompt_payload = json.loads(items[0]["prompt"])
87
+ self.assertEqual(prompt_payload["characters"][0]["description"], "focused and covered with grease")
88
+ self.assertEqual(prompt_payload["camera"], "medium shot")
89
+
90
+ def test_build_scene_json_prompt_rejects_unknown_override_character(self) -> None:
91
+ scene = {
92
+ "title": "Workshop",
93
+ "description": "Lantern repair",
94
+ "character_ids": ["hero"],
95
+ "character_descriptions": {"ghost": "not listed"},
96
+ }
97
+
98
+ with self.assertRaises(SystemExit) as context:
99
+ MODULE.build_scene_json_prompt(
100
+ scene=scene,
101
+ scene_index=1,
102
+ prompts_file=Path("prompts.json"),
103
+ character_profiles={
104
+ "hero": {
105
+ "id": "hero",
106
+ "name": "Kai",
107
+ "appearance": "short black hair",
108
+ "outfit": "blue jacket",
109
+ "description": "determined teenage inventor",
110
+ }
111
+ },
112
+ )
113
+
114
+ self.assertIn("ids not listed in character_ids", str(context.exception))
115
+
116
+ def test_generate_image_supports_b64_payload(self) -> None:
117
+ raw = png_bytes(100, 60)
118
+
119
+ with patch.object(
120
+ MODULE,
121
+ "post_json",
122
+ return_value={"data": [{"b64_json": MODULE.base64.b64encode(raw).decode("ascii"), "revised_prompt": "ok"}]},
123
+ ):
124
+ image_bytes, revised_prompt = MODULE.generate_image(
125
+ base_url="https://example.com",
126
+ api_key="token",
127
+ image_model="gpt-image-1",
128
+ prompt="scene",
129
+ aspect_ratio=None,
130
+ image_size=None,
131
+ quality=None,
132
+ style=None,
133
+ )
134
+
135
+ self.assertEqual(image_bytes, raw)
136
+ self.assertEqual(revised_prompt, "ok")
137
+
138
+ def test_main_generates_cropped_images_and_summary(self) -> None:
139
+ with tempfile.TemporaryDirectory() as temp_dir:
140
+ project_dir = Path(temp_dir)
141
+ args = types.SimpleNamespace(
142
+ content_name="Lantern / Story",
143
+ project_dir=str(project_dir),
144
+ env_file=str(project_dir / ".env"),
145
+ api_url="https://example.com",
146
+ api_key="token",
147
+ prompts_file=None,
148
+ prompt=["Lantern workshop at dusk"],
149
+ image_model="gpt-image-1",
150
+ aspect_ratio="1:1",
151
+ image_size=None,
152
+ quality=None,
153
+ style=None,
154
+ )
155
+
156
+ with patch.object(MODULE, "parse_args", return_value=args), patch.object(
157
+ MODULE, "load_dotenv_file", return_value=False
158
+ ), patch.object(
159
+ MODULE,
160
+ "generate_image",
161
+ return_value=(png_bytes(200, 100), "Lantern workshop at dusk, improved"),
162
+ ):
163
+ exit_code = MODULE.main()
164
+
165
+ self.assertEqual(exit_code, 0)
166
+ output_dir = project_dir / "pictures" / "Lantern_Story"
167
+ summary = json.loads((output_dir / "storyboard.json").read_text(encoding="utf-8"))
168
+ self.assertEqual(summary["aspect_ratio"], "1:1")
169
+ self.assertEqual(summary["images"][0]["width"], 100)
170
+ self.assertEqual(summary["images"][0]["height"], 100)
171
+ self.assertEqual(summary["images"][0]["source_width"], 200)
172
+ self.assertEqual(summary["images"][0]["source_height"], 100)
173
+ self.assertEqual(summary["images"][0]["revised_prompt"], "Lantern workshop at dusk, improved")
174
+
175
+
176
+ if __name__ == "__main__":
177
+ unittest.main()
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@laitszkin/apollo-toolkit",
3
- "version": "2.14.22",
3
+ "version": "3.0.0",
4
4
  "description": "Apollo Toolkit npm installer for managed skill copying across Codex, OpenClaw, and Trae.",
5
5
  "license": "MIT",
6
6
  "author": "LaiTszKin",
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: read-github-issue
3
- description: Read and search remote GitHub issues via GitHub CLI (`gh`). Use when users ask to list issues, filter issue candidates, inspect a specific issue with comments, or gather issue context before planning follow-up work. Prefer the bundled scripts when they are present and working, but fall back to direct `gh issue list` / `gh issue view` commands when the scripts are missing or fail for repository-specific reasons.
3
+ description: Read and search remote GitHub issues via GitHub CLI (`gh`). Use when users ask to list issues, filter issue candidates, inspect a specific issue with comments, or gather issue context before planning follow-up work. Prefer the bundled CLI tools when they are present and working, but fall back to direct `gh issue list` / `gh issue view` commands when the tools are missing or fail for repository-specific reasons.
4
4
  ---
5
5
 
6
6
  # Read GitHub Issue
@@ -10,12 +10,12 @@ description: Read and search remote GitHub issues via GitHub CLI (`gh`). Use whe
10
10
  - Required: none.
11
11
  - Conditional: none.
12
12
  - Optional: none.
13
- - Fallback: If the bundled scripts are missing or fail but `gh` is available, continue with raw `gh issue list` / `gh issue view`; only stop when `gh` itself is unavailable or unauthenticated.
13
+ - Fallback: If the bundled CLI tools are missing or fail but `gh` is available, continue with raw `gh issue list` / `gh issue view`; only stop when `gh` itself is unavailable or unauthenticated.
14
14
 
15
15
  ## Standards
16
16
 
17
17
  - Evidence: Verify repository context first, then read remote issue data directly from `gh issue list` / `gh issue view` instead of paraphrasing from memory.
18
- - Execution: Confirm the target repo, prefer the bundled scripts for deterministic output, then fall back to raw `gh` commands whenever the scripts are unavailable or broken in the target repository.
18
+ - Execution: Confirm the target repo, prefer the bundled CLI tools for deterministic output, then fall back to raw `gh` commands whenever the tools are unavailable or broken in the target repository.
19
19
  - Quality: Keep the skill focused on issue discovery and retrieval only; do not embed any hardcoded fixing, branching, PR, or push workflow.
20
20
  - Output: Return candidate issues, selected issue details, comments summary, and any missing information needed before follow-up work.
21
21
 
@@ -35,12 +35,12 @@ Use this skill to gather trustworthy GitHub issue context with `gh`: discover op
35
35
  - Run `gh repo view --json nameWithOwner,isPrivate,defaultBranchRef` to confirm target repo.
36
36
  - If the user specifies another repository, always use `--repo <owner>/<repo>` in issue commands.
37
37
 
38
- ### 2) Find candidate issues with the bundled script
38
+ ### 2) Find candidate issues with the bundled CLI
39
39
 
40
40
  - Preferred command:
41
41
 
42
42
  ```bash
43
- python3 scripts/find_issues.py --limit 50 --state open
43
+ apltk find-github-issues --limit 50 --state open
44
44
  ```
45
45
 
46
46
  - Optional filters:
@@ -61,7 +61,7 @@ gh issue list --limit 50 --state open
61
61
  - Preferred command:
62
62
 
63
63
  ```bash
64
- python3 scripts/read_issue.py 123 --comments
64
+ apltk read-github-issue 123 --comments
65
65
  ```
66
66
 
67
67
  - Optional flags:
@@ -82,16 +82,16 @@ gh issue view 123 --comments
82
82
  - Identify missing acceptance criteria, repro details, affected components, or environment context.
83
83
  - If issue text and comments are insufficient, state exactly what is missing instead of inventing a fix plan.
84
84
 
85
- ## Included Script
85
+ ## Included CLI
86
86
 
87
- ### `scripts/find_issues.py`
87
+ ### `apltk find-github-issues`
88
88
 
89
89
  - Purpose: consistent remote issue listing via `gh issue list`.
90
90
  - Outputs a readable table by default, or JSON with `--output json`.
91
91
  - Uses only GitHub CLI so it reflects remote GitHub state.
92
92
  - Treat it as a convenience wrapper, not a hard dependency.
93
93
 
94
- ### `scripts/read_issue.py`
94
+ ### `apltk read-github-issue`
95
95
 
96
96
  - Purpose: deterministic issue detail retrieval via `gh issue view`.
97
97
  - Outputs either a human-readable summary or full JSON for downstream automation.
@@ -46,8 +46,8 @@ Use this skill to run an end-to-end GitHub PR review loop: collect review thread
46
46
  - If user does not provide PR number, infer from current branch context.
47
47
 
48
48
  ```bash
49
- python3 scripts/review_threads.py list --repo <owner>/<repo> --pr <number>
50
- python3 scripts/review_threads.py list
49
+ apltk review-threads list --repo <owner>/<repo> --pr <number>
50
+ apltk review-threads list
51
51
  ```
52
52
 
53
53
  ## 2) Read unresolved review threads
@@ -55,8 +55,8 @@ python3 scripts/review_threads.py list
55
55
  Use table view for quick scan, then JSON when you need full details.
56
56
 
57
57
  ```bash
58
- python3 scripts/review_threads.py list --pr <number> --state unresolved --output table
59
- python3 scripts/review_threads.py list --pr <number> --state unresolved --output json > /tmp/pr_threads.json
58
+ apltk review-threads list --pr <number> --state unresolved --output table
59
+ apltk review-threads list --pr <number> --state unresolved --output json > /tmp/pr_threads.json
60
60
  ```
61
61
 
62
62
  The JSON output contains `thread_id`, `path`, `line`, and comment bodies for decision and resolution.
@@ -98,13 +98,13 @@ Track adopted thread IDs in a JSON file:
98
98
  Resolve only threads you actually addressed in code.
99
99
 
100
100
  ```bash
101
- python3 scripts/review_threads.py resolve --pr <number> --thread-id-file adopted_threads.json
101
+ apltk review-threads resolve --pr <number> --thread-id-file adopted_threads.json
102
102
  ```
103
103
 
104
104
  Optional preview without mutating GitHub state:
105
105
 
106
106
  ```bash
107
- python3 scripts/review_threads.py resolve --pr <number> --thread-id-file adopted_threads.json --dry-run
107
+ apltk review-threads resolve --pr <number> --thread-id-file adopted_threads.json --dry-run
108
108
  ```
109
109
 
110
110
  ## 8) Handle non-adopted comments
@@ -113,9 +113,9 @@ python3 scripts/review_threads.py resolve --pr <number> --thread-id-file adopted
113
113
  - Reply with a concise technical reason and, if needed, a proposed follow-up.
114
114
  - Never resolve rejected or unhandled feedback threads.
115
115
 
116
- ## Scripts
116
+ ## CLI
117
117
 
118
- ### `scripts/review_threads.py`
118
+ ### `apltk review-threads`
119
119
 
120
120
  - `list`: fetch PR review threads via GitHub GraphQL (`gh api graphql`), supports repo/PR inference.
121
121
  - `resolve`: resolve selected review threads by thread IDs.
@@ -50,6 +50,8 @@ For each confirmed finding, delegate publication to `$open-github-issue` with:
50
50
  - file references and causal reasoning in `suspected-cause`
51
51
  - reproduction conditions when known
52
52
 
53
+ When invoking the publisher CLI directly, use `apltk open-github-issue --payload-file <json>` or `@file` inputs for Markdown-rich fields so shell parsing cannot consume backticks or code snippets.
54
+
53
55
  If issue publication is unavailable, return draft issue content instead of switching to an ad-hoc publishing path.
54
56
 
55
57
  ## Output expectations
@@ -74,6 +74,7 @@ Only continue to the next level when the current level has no confirmed findings
74
74
  - `suspected-cause`: file references, causal chain, and confidence
75
75
  - `reproduction`: concrete trigger or conditions when known; otherwise leave empty
76
76
  - `repo`: target repository in `owner/repo` format when known
77
+ - If invoking the publisher CLI directly, pass finding details through `apltk open-github-issue --payload-file <json>` or `@file` inputs rather than inline shell arguments so code snippets and backticks survive unchanged.
77
78
 
78
79
  ## Evidence standard
79
80
 
@@ -15,7 +15,7 @@ description: Use a background terminal to run a user-specified command immediate
15
15
  ## Standards
16
16
 
17
17
  - Evidence: Anchor every conclusion to the requested command, execution window, startup/shutdown timestamps, one canonical run folder or artifact root, captured logs, and concrete runtime signals.
18
- - Execution: Collect the run contract, verify the real stop mechanism before launch, use a background terminal, optionally update the code only when the user asks, execute the requested command immediately or in the requested window, record the canonical run folder once the process materializes it, capture logs, stop cleanly when bounded, then delegate log review to `analyse-app-logs` only when findings are requested or needed.
18
+ - Execution: Collect the run contract, verify the real stop mechanism before launch, choose the highest-fidelity execution mode that matches the user's intent, use a background terminal, optionally update the code only when the user asks, execute the requested command immediately or in the requested window, record the canonical run folder once the process materializes it, capture logs, stop cleanly when bounded, then delegate log review to `analyse-app-logs` only when findings are requested or needed.
19
19
  - Quality: Keep scheduling, execution, and shutdown deterministic; separate confirmed findings from hypotheses; and mark each assessed module healthy/degraded/failed/unknown with reasons.
20
20
  - Output: Return the run configuration, execution status, log locations, optional code-update result, optional module health by area, confirmed issues, potential issues, observability gaps, and scheduler status when applicable.
21
21
 
@@ -46,6 +46,7 @@ This skill is an orchestration layer. It owns the background terminal session, o
46
46
  - Prefer one bounded observation window over open-ended monitoring.
47
47
  - Use one dedicated background terminal session per requested run so execution and logs stay correlated.
48
48
  - Record the canonical run directory, artifact root, or other generated output location as soon as it exists, and use that as the source of truth for later analysis.
49
+ - When a repository exposes both synthetic harnesses and production-like runtime entrypoints, prefer the production-like path for claims about real runtime, market, or operator behavior; use the lower-fidelity harness only when the user explicitly asked for it or when it is the only safe reproduction surface.
49
50
  - Treat code update as optional and only perform it when the user explicitly requests it.
50
51
  - Treat startup, steady-state, and shutdown as part of the same investigation.
51
52
  - Do not call a module healthy unless there is at least one positive signal for it.
@@ -58,6 +59,7 @@ This skill is an orchestration layer. It owns the background terminal session, o
58
59
  1. Define the run contract
59
60
  - Confirm or derive the workspace, execution command, optional code-update step, optional schedule, optional duration, readiness signal, log locations, and whether post-run findings are required.
60
61
  - Derive commands from trustworthy sources first: `package.json`, `Makefile`, `docker-compose.yml`, `Procfile`, scripts, or project docs.
62
+ - If multiple commands exist for the same workflow, rank them by fidelity and state explicitly which mode you are choosing: production-like runtime, bounded integration harness, or synthetic scenario replay.
61
63
  - If no trustworthy execution command or stop method can be found, stop and ask only for the missing command rather than guessing.
62
64
  2. Prepare the background terminal run
63
65
  - Use a dedicated background terminal session for the whole workflow.
@@ -77,6 +79,7 @@ This skill is an orchestration layer. It owns the background terminal session, o
77
79
  5. Run and capture readiness
78
80
  - Execute the requested command in the same background terminal.
79
81
  - As soon as the command emits or creates its canonical run directory, artifact root, or equivalent output location, record that path and reuse it for every later check.
82
+ - Report the exact runtime mode used in the evidence record so later analysis does not accidentally treat synthetic-harness results as proof about production behavior.
80
83
  - Wait for a concrete readiness signal when the command is expected to stay up, such as a health endpoint, listening-port log, worker boot line, or queue-consumer ready message.
81
84
  - If readiness never arrives, stop the run, preserve logs, and treat it as a failed startup window.
82
85
  6. Observe and stop when bounded
@@ -15,7 +15,7 @@ description: "Systematic debugging workflow for program issues: understand obser
15
15
  ## Standards
16
16
 
17
17
  - Evidence: Gather expected versus observed behavior from code and runtime facts before deciding on a cause, and when the issue involves a runtime pipeline or bounded run, anchor the investigation to one canonical artifact root or run directory instead of mixed terminal snippets from multiple runs.
18
- - Execution: Inspect the relevant paths, reproduce every plausible cause with tests or bounded reruns, map each observed failure to a concrete pipeline stage, distinguish toolchain/platform faults from application-logic faults, classify failing tests as stale test contract vs test-harness interference vs real product bug, then apply the minimal fix at the true owner.
18
+ - Execution: Inspect the relevant paths, reproduce every plausible cause with tests or bounded reruns, choose a reproduction mode whose fidelity matches the user's claim, map each observed failure to a concrete pipeline stage, distinguish toolchain/platform faults from application-logic faults, classify failing tests as stale test contract vs test-harness interference vs real product bug, then apply the minimal fix at the true owner.
19
19
  - Quality: Keep scope focused on the bug, prefer existing test patterns, explicitly rule out hypotheses that could not be reproduced, and when failures disappear in isolated reruns treat shared-state or parallel-test interference as a first-class hypothesis instead of silently dismissing the original failure.
20
20
  - Output: Deliver the plausible-cause list, the canonical evidence source, reproduction tests or reruns, the final failure classification for each investigated symptom, validated fix summary, and passing-test confirmation.
21
21
 
@@ -25,6 +25,7 @@ description: "Systematic debugging workflow for program issues: understand obser
25
25
  - Cover all plausible causes with reproducible tests instead of guessing a single cause.
26
26
  - Keep fixes minimal, focused, and validated by passing tests.
27
27
  - When logs or runtime artifacts exist, treat one run as canonical and compare every conclusion against that same run's generated artifacts, not against ad hoc console recollection.
28
+ - When a repository has both scenario or harness runs and a production-like runtime, do not treat the lower-fidelity mode as proof about the higher-fidelity mode unless you explicitly state that limitation and the user agrees.
28
29
  - When the failing flow crosses multiple layers, identify the last confirmed successful stage before assigning blame.
29
30
  - When tests fail, separate stale assertions and fixture drift from real implementation regressions before changing product code.
30
31
  - If failures only appear under parallel execution or shared shell-out paths, investigate test isolation, shared locks, temp directories, run-name collisions, and environment leakage before blaming the product.
@@ -45,7 +46,7 @@ Also auto-invoke this skill when mismatch evidence appears during normal executi
45
46
 
46
47
  1. **Understand and inspect**: Parse expected vs observed behavior, explore relevant code paths, record the canonical failing run or artifact root when runtime output is involved, and build a list of plausible root causes.
47
48
  2. **Map the failure boundary**: Break the flow into concrete stages such as setup, startup, readiness, steady-state execution, persistence, and shutdown, then identify the last stage that is confirmed to have succeeded.
48
- 3. **Reproduce with tests or bounded reruns**: Write or extend tests that reproduce every plausible cause, and when the bug depends on runtime orchestration rerun the same bounded command or scenario instead of switching contexts mid-investigation. When a failing test passes in isolation, rerun it under the original suite shape to determine whether the real cause is stale expectations, fixture drift, or shared-state interference.
49
+ 3. **Reproduce with tests or bounded reruns**: Write or extend tests that reproduce every plausible cause, and when the bug depends on runtime orchestration rerun the same bounded command or the same runtime mode instead of switching contexts mid-investigation. If the user is asking about real runtime or market behavior, prefer the production-like bounded run over a synthetic scenario replay unless safety or tooling constraints make that impossible. When a failing test passes in isolation, rerun it under the original suite shape to determine whether the real cause is stale expectations, fixture drift, or shared-state interference.
49
50
  4. **Diagnose and confirm**: Use reproduction evidence to confirm the true root cause, explicitly rule out non-causes, and classify whether each investigated failure belongs to the toolchain/platform layer, test contract drift, test-harness interference, orchestration, or application logic.
50
51
  5. **Fix and validate**: Implement focused fixes and iterate until all reproduction tests or bounded reruns pass.
51
52
 
@@ -56,7 +56,7 @@ cp ~/.codex/skills/text-to-short-video/.env.example \
56
56
  If API-generated video ratio/size does not match the target, run:
57
57
 
58
58
  ```bash
59
- python ~/.codex/skills/text-to-short-video/scripts/enforce_video_aspect_ratio.py \
59
+ apltk enforce-video-aspect-ratio \
60
60
  --input-video "<downloaded_video_path>" \
61
61
  --output-video "<final_output_video_path>" \
62
62
  --env-file ~/.codex/skills/text-to-short-video/.env \
@@ -173,7 +173,7 @@ If provider returns multiple outputs, keep the best one that matches requested s
173
173
  When output ratio or resolution differs from target, run:
174
174
 
175
175
  ```bash
176
- python ~/.codex/skills/text-to-short-video/scripts/enforce_video_aspect_ratio.py \
176
+ apltk enforce-video-aspect-ratio \
177
177
  --input-video "<downloaded_video_path>" \
178
178
  --output-video "<final_output_video_path>" \
179
179
  --env-file ~/.codex/skills/text-to-short-video/.env \
@@ -0,0 +1,194 @@
1
+ #!/usr/bin/env python3
2
+
3
+ from __future__ import annotations
4
+
5
+ import importlib.util
6
+ import io
7
+ import json
8
+ import os
9
+ import random
10
+ import re
11
+ import tempfile
12
+ import types
13
+ import unittest
14
+ from pathlib import Path
15
+ from unittest.mock import patch
16
+
17
+
18
+ SCRIPT_PATH = Path(__file__).resolve().parents[1] / "scripts" / "enforce_video_aspect_ratio.py"
19
+ SPEC = importlib.util.spec_from_file_location("enforce_video_aspect_ratio", SCRIPT_PATH)
20
+ MODULE = importlib.util.module_from_spec(SPEC)
21
+ SPEC.loader.exec_module(MODULE)
22
+
23
+
24
+ def parse_crop(expression: str) -> tuple[int, int, int, int] | None:
25
+ match = re.search(r"crop=(\d+):(\d+):(\d+):(\d+)", expression)
26
+ if not match:
27
+ return None
28
+ return tuple(int(value) for value in match.groups())
29
+
30
+
31
+ class EnforceVideoAspectRatioTests(unittest.TestCase):
32
+ def test_parse_size_property_round_trips_positive_dimensions(self) -> None:
33
+ generator = random.Random(20260418)
34
+ for _ in range(150):
35
+ width = generator.randint(1, 9999)
36
+ height = generator.randint(1, 9999)
37
+ raw = f"{width}x{height}"
38
+ with self.subTest(raw=raw):
39
+ self.assertEqual(MODULE.parse_size(raw), (width, height))
40
+
41
+ def test_load_dotenv_file_parses_quotes_and_respects_override(self) -> None:
42
+ with tempfile.TemporaryDirectory() as temp_dir:
43
+ env_file = Path(temp_dir) / ".env"
44
+ env_file.write_text(
45
+ 'TEXT_TO_SHORT_VIDEO_WIDTH="720"\n'
46
+ "TEXT_TO_SHORT_VIDEO_HEIGHT=1280 # comment\n",
47
+ encoding="utf-8",
48
+ )
49
+ original = os.environ.get("TEXT_TO_SHORT_VIDEO_WIDTH")
50
+ os.environ["TEXT_TO_SHORT_VIDEO_WIDTH"] = "999"
51
+ try:
52
+ loaded = MODULE.load_dotenv_file(env_file, override=False)
53
+ self.assertTrue(loaded)
54
+ self.assertEqual(os.environ["TEXT_TO_SHORT_VIDEO_WIDTH"], "999")
55
+ self.assertEqual(os.environ["TEXT_TO_SHORT_VIDEO_HEIGHT"], "1280")
56
+ MODULE.load_dotenv_file(env_file, override=True)
57
+ self.assertEqual(os.environ["TEXT_TO_SHORT_VIDEO_WIDTH"], "720")
58
+ finally:
59
+ if original is None:
60
+ os.environ.pop("TEXT_TO_SHORT_VIDEO_WIDTH", None)
61
+ else:
62
+ os.environ["TEXT_TO_SHORT_VIDEO_WIDTH"] = original
63
+ os.environ.pop("TEXT_TO_SHORT_VIDEO_HEIGHT", None)
64
+
65
+ def test_build_video_filter_property_keeps_crop_within_bounds(self) -> None:
66
+ generator = random.Random(20260418)
67
+ for _ in range(200):
68
+ input_width = generator.randint(32, 4096)
69
+ input_height = generator.randint(32, 4096)
70
+ target_width = generator.randint(32, 2160)
71
+ target_height = generator.randint(32, 3840)
72
+
73
+ expression, crop_applied = MODULE.build_video_filter(
74
+ input_width=input_width,
75
+ input_height=input_height,
76
+ target_width=target_width,
77
+ target_height=target_height,
78
+ )
79
+
80
+ with self.subTest(
81
+ input_width=input_width,
82
+ input_height=input_height,
83
+ target_width=target_width,
84
+ target_height=target_height,
85
+ expression=expression,
86
+ ):
87
+ if expression is None:
88
+ self.assertFalse(crop_applied)
89
+ self.assertEqual((input_width, input_height), (target_width, target_height))
90
+ continue
91
+
92
+ self.assertIn(f"scale={target_width}:{target_height}", expression)
93
+ crop = parse_crop(expression)
94
+ if crop is None:
95
+ self.assertFalse(crop_applied)
96
+ self.assertEqual(input_width * target_height, input_height * target_width)
97
+ continue
98
+
99
+ crop_width, crop_height, offset_x, offset_y = crop
100
+ self.assertTrue(crop_applied)
101
+ self.assertEqual(crop_width % 2, 0)
102
+ self.assertEqual(crop_height % 2, 0)
103
+ self.assertGreaterEqual(crop_width, 2)
104
+ self.assertGreaterEqual(crop_height, 2)
105
+ self.assertLessEqual(crop_width, input_width)
106
+ self.assertLessEqual(crop_height, input_height)
107
+ self.assertGreaterEqual(offset_x, 0)
108
+ self.assertGreaterEqual(offset_y, 0)
109
+ self.assertLessEqual(offset_x + crop_width, input_width)
110
+ self.assertLessEqual(offset_y + crop_height, input_height)
111
+
112
+ def test_resolve_output_path_enforces_in_place_rules(self) -> None:
113
+ input_video = Path("/tmp/input.mp4")
114
+ args = types.SimpleNamespace(in_place=False, output_video=None)
115
+ self.assertEqual(MODULE.resolve_output_path(args, input_video), Path("/tmp/input_aspect_fixed.mp4"))
116
+
117
+ args = types.SimpleNamespace(in_place=True, output_video=None)
118
+ self.assertEqual(MODULE.resolve_output_path(args, input_video), input_video)
119
+
120
+ args = types.SimpleNamespace(in_place=True, output_video="/tmp/out.mp4")
121
+ with self.assertRaises(SystemExit):
122
+ MODULE.resolve_output_path(args, input_video)
123
+
124
+ def test_main_copies_file_when_video_already_matches_target(self) -> None:
125
+ with tempfile.TemporaryDirectory() as temp_dir:
126
+ input_video = Path(temp_dir) / "input.mp4"
127
+ input_video.write_bytes(b"video")
128
+ output_video = Path(temp_dir) / "copy.mp4"
129
+ args = types.SimpleNamespace(
130
+ input_video=str(input_video),
131
+ output_video=str(output_video),
132
+ in_place=False,
133
+ target_size="640x360",
134
+ target_width=None,
135
+ target_height=None,
136
+ env_file=str(Path(temp_dir) / ".env"),
137
+ force=False,
138
+ ffmpeg_bin="ffmpeg",
139
+ ffprobe_bin="ffprobe",
140
+ )
141
+
142
+ with patch.object(MODULE, "parse_args", return_value=args), patch.object(
143
+ MODULE, "load_dotenv_file", return_value=False
144
+ ), patch.object(MODULE, "required_command"), patch.object(
145
+ MODULE, "probe_video_size", return_value=(640, 360)
146
+ ), patch("sys.stdout", new_callable=io.StringIO) as stdout:
147
+ exit_code = MODULE.main()
148
+
149
+ self.assertEqual(exit_code, 0)
150
+ self.assertEqual(output_video.read_bytes(), b"video")
151
+ self.assertIn("already matches target size", stdout.getvalue())
152
+
153
+ def test_main_runs_ffmpeg_and_reports_output_dimensions(self) -> None:
154
+ with tempfile.TemporaryDirectory() as temp_dir:
155
+ input_video = Path(temp_dir) / "input.mp4"
156
+ output_video = Path(temp_dir) / "output.mp4"
157
+ input_video.write_bytes(b"video")
158
+ args = types.SimpleNamespace(
159
+ input_video=str(input_video),
160
+ output_video=str(output_video),
161
+ in_place=False,
162
+ target_size="1080x1920",
163
+ target_width=None,
164
+ target_height=None,
165
+ env_file=str(Path(temp_dir) / ".env"),
166
+ force=True,
167
+ ffmpeg_bin="ffmpeg",
168
+ ffprobe_bin="ffprobe",
169
+ )
170
+
171
+ def fake_run(command, check):
172
+ self.assertIn("-vf", command)
173
+ Path(command[-1]).write_bytes(b"processed")
174
+ return types.SimpleNamespace(returncode=0)
175
+
176
+ with patch.object(MODULE, "parse_args", return_value=args), patch.object(
177
+ MODULE, "load_dotenv_file", return_value=False
178
+ ), patch.object(MODULE, "required_command"), patch.object(
179
+ MODULE,
180
+ "probe_video_size",
181
+ side_effect=[(1920, 1080), (1080, 1920)],
182
+ ), patch.object(MODULE.subprocess, "run", side_effect=fake_run), patch(
183
+ "sys.stdout", new_callable=io.StringIO
184
+ ) as stdout:
185
+ exit_code = MODULE.main()
186
+
187
+ self.assertEqual(exit_code, 0)
188
+ self.assertTrue(output_video.is_file())
189
+ self.assertIn("Center crop applied: yes", stdout.getvalue())
190
+ self.assertIn("Output size: 1080x1920", stdout.getvalue())
191
+
192
+
193
+ if __name__ == "__main__":
194
+ unittest.main()
@@ -10,7 +10,7 @@ description: Read a user-specified PDF that marks the week's key financial event
10
10
  - Required: `pdf` to render the final report.
11
11
  - Conditional: `document-vision-reader` when the source PDF's highlighted markers are visible in layout but not recoverable from machine-readable text alone.
12
12
  - Optional: none.
13
- - Fallback: If source-PDF extraction through `pdf` is unavailable or fails on macOS, use the bundled `scripts/extract_pdf_text_pdfkit.swift` helper before giving up; only stop when neither `pdf` nor the local PDFKit fallback can recover the marked events, or when final PDF rendering itself cannot be completed.
13
+ - Fallback: If source-PDF extraction through `pdf` is unavailable or fails on macOS, use the bundled `apltk extract-pdf-text-pdfkit` helper before giving up; only stop when neither `pdf` nor the local PDFKit fallback can recover the marked events, or when final PDF rendering itself cannot be completed.
14
14
 
15
15
  ## Standards
16
16
 
@@ -72,7 +72,7 @@ Do not guess any input that materially changes the research window or report sco
72
72
  - If `pdf` extraction is unavailable or fails because the current machine lacks local PDF tooling, and the host is macOS, run:
73
73
 
74
74
  ```bash
75
- swift scripts/extract_pdf_text_pdfkit.swift /absolute/path/to/source.pdf
75
+ apltk extract-pdf-text-pdfkit /absolute/path/to/source.pdf
76
76
  ```
77
77
 
78
78
  - The bundled extractor prints page-delimited text directly from PDFKit so the agent can still build the source-event table without adding Python PDF packages ad hoc.