@laitszkin/apollo-toolkit 2.14.23 → 3.0.1
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.
- package/AGENTS.md +3 -0
- package/CHANGELOG.md +17 -0
- package/README.md +9 -0
- package/analyse-app-logs/README.md +5 -5
- package/analyse-app-logs/SKILL.md +7 -5
- package/analyse-app-logs/scripts/__pycache__/filter_logs_by_time.cpython-312.pyc +0 -0
- package/analyse-app-logs/scripts/__pycache__/log_cli_utils.cpython-312.pyc +0 -0
- package/analyse-app-logs/scripts/__pycache__/search_logs.cpython-312.pyc +0 -0
- package/codex/codex-memory-manager/README.md +2 -2
- package/codex/codex-memory-manager/SKILL.md +5 -5
- package/codex/codex-memory-manager/tests/test_extract_recent_conversations.py +3 -2
- package/codex/learn-skill-from-conversations/README.md +1 -1
- package/codex/learn-skill-from-conversations/SKILL.md +2 -2
- package/codex/learn-skill-from-conversations/tests/test_extract_recent_conversations.py +3 -2
- package/docs-to-voice/README.md +3 -3
- package/docs-to-voice/SKILL.md +4 -4
- package/docs-to-voice/scripts/__pycache__/docs_to_voice.cpython-312.pyc +0 -0
- package/docs-to-voice/tests/test_docs_to_voice_shell_wrapper.py +51 -0
- package/feature-propose/SKILL.md +1 -0
- package/generate-spec/README.md +3 -6
- package/generate-spec/SKILL.md +2 -3
- package/generate-spec/scripts/__pycache__/create-specscpython-312.pyc +0 -0
- package/generate-spec/tests/test_create_specs.py +166 -0
- package/jupiter-development/SKILL.md +5 -0
- package/katex/SKILL.md +3 -3
- package/katex/scripts/__pycache__/render_katex.cpython-312.pyc +0 -0
- package/katex/tests/test_render_katex.py +174 -0
- package/learning-error-book/SKILL.md +2 -2
- package/learning-error-book/tests/test_render_error_book_json_to_pdf.py +134 -0
- package/lib/cli.js +66 -0
- package/lib/tool-runner.js +214 -0
- package/maintain-project-constraints/SKILL.md +3 -3
- package/maintain-skill-catalog/SKILL.md +2 -2
- package/novel-to-short-video/SKILL.md +2 -2
- package/open-github-issue/README.md +31 -22
- package/open-github-issue/SKILL.md +54 -40
- package/open-github-issue/scripts/__pycache__/open_github_issue.cpython-312.pyc +0 -0
- package/open-github-issue/scripts/open_github_issue.py +130 -3
- package/open-github-issue/tests/test_open_github_issue.py +95 -0
- package/openai-text-to-image-storyboard/README.md +1 -1
- package/openai-text-to-image-storyboard/SKILL.md +1 -1
- package/openai-text-to-image-storyboard/tests/test_generate_storyboard_images.py +177 -0
- package/package.json +1 -1
- package/read-github-issue/SKILL.md +9 -9
- package/read-github-issue/scripts/__pycache__/find_issues.cpython-312.pyc +0 -0
- package/read-github-issue/scripts/__pycache__/read_issue.cpython-312.pyc +0 -0
- package/resolve-review-comments/SKILL.md +8 -8
- package/resolve-review-comments/scripts/__pycache__/review_threads.cpython-312.pyc +0 -0
- package/review-codebases/README.md +2 -0
- package/review-codebases/SKILL.md +1 -0
- package/scheduled-runtime-health-check/SKILL.md +3 -0
- package/systematic-debug/SKILL.md +3 -0
- package/text-to-short-video/README.md +1 -1
- package/text-to-short-video/SKILL.md +1 -1
- package/text-to-short-video/scripts/__pycache__/enforce_video_aspect_ratio.cpython-312.pyc +0 -0
- package/text-to-short-video/tests/test_enforce_video_aspect_ratio.py +194 -0
- package/weekly-financial-event-report/SKILL.md +2 -2
- 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: 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
|
|
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
|
|
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
|
|
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
|
|
38
|
+
### 2) Find candidate issues with the bundled CLI
|
|
39
39
|
|
|
40
40
|
- Preferred command:
|
|
41
41
|
|
|
42
42
|
```bash
|
|
43
|
-
|
|
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
|
-
|
|
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
|
|
85
|
+
## Included CLI
|
|
86
86
|
|
|
87
|
-
### `
|
|
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
|
-
### `
|
|
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.
|
|
Binary file
|
|
@@ -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
|
-
|
|
50
|
-
|
|
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
|
-
|
|
59
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
##
|
|
116
|
+
## CLI
|
|
117
117
|
|
|
118
|
-
### `
|
|
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.
|
|
Binary file
|
|
@@ -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
|
|
|
@@ -52,6 +52,7 @@ This skill is an orchestration layer. It owns the background terminal session, o
|
|
|
52
52
|
- Do not call a module healthy unless there is at least one positive signal for it.
|
|
53
53
|
- Separate scheduler failures, boot failures, runtime failures, and shutdown failures.
|
|
54
54
|
- For complex pipelines, identify the last successful stage before attributing the failure to application logic.
|
|
55
|
+
- When the user asks to compare a bounded run with a previous run, compare only runs with the same command or preset, duration, runtime mode, and complete structured artifacts. If the previous run lacks canonical reports, databases, or startup artifacts, mark the runs incomparable and explain the artifact completeness gap instead of inventing performance deltas.
|
|
55
56
|
- If logs cannot support a health judgment, mark the module as `unknown` instead of guessing.
|
|
56
57
|
|
|
57
58
|
## Required workflow
|
|
@@ -93,6 +94,8 @@ This skill is an orchestration layer. It owns the background terminal session, o
|
|
|
93
94
|
- Invoke `analyse-app-logs` on only the captured runtime window.
|
|
94
95
|
- Pass the service or module names, environment, timezone, canonical run folder, relevant log files, and the exact start/end boundaries.
|
|
95
96
|
- When the command produced reports, databases, or other structured artifacts, compare them against the same run's logs before making a health judgment.
|
|
97
|
+
- For follow-up questions about why most business events did not happen, build a stage-by-stage funnel from the canonical artifacts before reading isolated logs: candidate counts, admission/precheck decisions, queue or governor outcomes, skipped/blocked reasons, executed counts, retry/remediation outcomes, and persistence records.
|
|
98
|
+
- For follow-up questions about runtime speed, report latency from structured timestamps when available, separating startup/readiness, queue wait, precheck/final-prepare, submission, confirmation, and end-to-end timings rather than collapsing them into one vague duration.
|
|
96
99
|
- Reuse its confirmed issues, hypotheses, and monitoring improvements instead of rewriting a separate incident workflow.
|
|
97
100
|
8. Produce the final report
|
|
98
101
|
- Always summarize the actual command executed, actual start/end timestamps, execution status, and log locations.
|
|
@@ -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 comparing runtime runs, first verify the baseline run is complete enough for the requested comparison: same command or scenario, same runtime mode, same bounded duration, and matching structured artifacts. If the baseline is incomplete, report that the only proven change is artifact/run completeness and avoid drawing strategy or performance conclusions from missing data.
|
|
28
29
|
- 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.
|
|
29
30
|
- When the failing flow crosses multiple layers, identify the last confirmed successful stage before assigning blame.
|
|
30
31
|
- When tests fail, separate stale assertions and fixture drift from real implementation regressions before changing product code.
|
|
@@ -58,6 +59,8 @@ Also auto-invoke this skill when mismatch evidence appears during normal executi
|
|
|
58
59
|
- If a hypothesized cause cannot be reproduced, document why and deprioritize it explicitly.
|
|
59
60
|
- For long-running or generated-artifact workflows, record the exact command, timestamps, and artifact paths before inspecting outputs so later comparisons stay on the same evidence set.
|
|
60
61
|
- Do not mix baseline data and rerun data casually; compare the same scenario or command across runs and call out when a conclusion comes from a rerun rather than the original failure.
|
|
62
|
+
- For post-run "why did most events not happen" questions, derive the answer from a funnel over structured artifacts first, then corroborate with logs: discovered or eligible candidates, admission blocks, stale or skipped decisions, queue/governor outcomes, execution attempts, confirmations, retries, and persisted event rows.
|
|
63
|
+
- For speed questions, compute per-stage timings from available event timestamps and state which stages are measured versus unavailable; avoid treating wall-clock bounded duration as pipeline latency.
|
|
61
64
|
- When test fixtures or assertions no longer match the implemented contract, update the tests instead of weakening the product behavior to satisfy stale expectations.
|
|
62
65
|
- When tests shell out to shared local infrastructure, add deterministic isolation such as mutexes, unique temp roots, or serialized sections before accepting flakes as inevitable.
|
|
63
66
|
|
|
@@ -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
|
-
|
|
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
|
-
|
|
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 \
|
|
Binary file
|
|
@@ -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 `
|
|
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
|
-
|
|
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.
|