@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.
- package/AGENTS.md +62 -0
- package/CHANGELOG.md +100 -0
- package/LICENSE +21 -0
- package/README.md +144 -0
- package/align-project-documents/SKILL.md +94 -0
- package/align-project-documents/agents/openai.yaml +4 -0
- package/analyse-app-logs/LICENSE +21 -0
- package/analyse-app-logs/README.md +126 -0
- package/analyse-app-logs/SKILL.md +121 -0
- package/analyse-app-logs/agents/openai.yaml +4 -0
- package/analyse-app-logs/references/investigation-checklist.md +58 -0
- package/analyse-app-logs/references/log-signal-patterns.md +52 -0
- package/answering-questions-with-research/SKILL.md +46 -0
- package/answering-questions-with-research/agents/openai.yaml +4 -0
- package/bin/apollo-toolkit.js +7 -0
- package/commit-and-push/LICENSE +21 -0
- package/commit-and-push/README.md +26 -0
- package/commit-and-push/SKILL.md +70 -0
- package/commit-and-push/agents/openai.yaml +4 -0
- package/commit-and-push/references/branch-naming.md +15 -0
- package/commit-and-push/references/commit-messages.md +19 -0
- package/deep-research-topics/LICENSE +21 -0
- package/deep-research-topics/README.md +43 -0
- package/deep-research-topics/SKILL.md +84 -0
- package/deep-research-topics/agents/openai.yaml +4 -0
- package/develop-new-features/LICENSE +21 -0
- package/develop-new-features/README.md +52 -0
- package/develop-new-features/SKILL.md +105 -0
- package/develop-new-features/agents/openai.yaml +4 -0
- package/develop-new-features/references/testing-e2e.md +35 -0
- package/develop-new-features/references/testing-integration.md +42 -0
- package/develop-new-features/references/testing-property-based.md +44 -0
- package/develop-new-features/references/testing-unit.md +37 -0
- package/discover-edge-cases/CHANGELOG.md +19 -0
- package/discover-edge-cases/LICENSE +21 -0
- package/discover-edge-cases/README.md +87 -0
- package/discover-edge-cases/SKILL.md +124 -0
- package/discover-edge-cases/agents/openai.yaml +4 -0
- package/discover-edge-cases/references/architecture-edge-cases.md +41 -0
- package/discover-edge-cases/references/code-edge-cases.md +46 -0
- package/docs-to-voice/.env.example +106 -0
- package/docs-to-voice/CHANGELOG.md +71 -0
- package/docs-to-voice/LICENSE +21 -0
- package/docs-to-voice/README.md +118 -0
- package/docs-to-voice/SKILL.md +107 -0
- package/docs-to-voice/agents/openai.yaml +4 -0
- package/docs-to-voice/scripts/docs_to_voice.py +1385 -0
- package/docs-to-voice/scripts/docs_to_voice.sh +11 -0
- package/docs-to-voice/tests/test_docs_to_voice_api_max_chars.py +210 -0
- package/docs-to-voice/tests/test_docs_to_voice_sentence_timeline.py +115 -0
- package/docs-to-voice/tests/test_docs_to_voice_settings.py +43 -0
- package/docs-to-voice/tests/test_docs_to_voice_speech_rate.py +57 -0
- package/enhance-existing-features/CHANGELOG.md +35 -0
- package/enhance-existing-features/LICENSE +21 -0
- package/enhance-existing-features/README.md +54 -0
- package/enhance-existing-features/SKILL.md +120 -0
- package/enhance-existing-features/agents/openai.yaml +4 -0
- package/enhance-existing-features/references/e2e-tests.md +25 -0
- package/enhance-existing-features/references/integration-tests.md +30 -0
- package/enhance-existing-features/references/property-based-tests.md +33 -0
- package/enhance-existing-features/references/unit-tests.md +29 -0
- package/feature-propose/LICENSE +21 -0
- package/feature-propose/README.md +23 -0
- package/feature-propose/SKILL.md +107 -0
- package/feature-propose/agents/openai.yaml +4 -0
- package/feature-propose/references/enhancement-features.md +25 -0
- package/feature-propose/references/important-features.md +25 -0
- package/feature-propose/references/mvp-features.md +25 -0
- package/feature-propose/references/performance-features.md +25 -0
- package/financial-research/SKILL.md +208 -0
- package/financial-research/agents/openai.yaml +4 -0
- package/financial-research/assets/weekly_market_report_template.md +45 -0
- package/fix-github-issues/SKILL.md +98 -0
- package/fix-github-issues/agents/openai.yaml +4 -0
- package/fix-github-issues/scripts/list_issues.py +148 -0
- package/fix-github-issues/tests/test_list_issues.py +127 -0
- package/generate-spec/LICENSE +21 -0
- package/generate-spec/README.md +61 -0
- package/generate-spec/SKILL.md +96 -0
- package/generate-spec/agents/openai.yaml +4 -0
- package/generate-spec/references/templates/checklist.md +78 -0
- package/generate-spec/references/templates/spec.md +55 -0
- package/generate-spec/references/templates/tasks.md +35 -0
- package/generate-spec/scripts/create-specs +123 -0
- package/harden-app-security/CHANGELOG.md +27 -0
- package/harden-app-security/LICENSE +21 -0
- package/harden-app-security/README.md +46 -0
- package/harden-app-security/SKILL.md +127 -0
- package/harden-app-security/agents/openai.yaml +4 -0
- package/harden-app-security/references/agent-attack-catalog.md +117 -0
- package/harden-app-security/references/common-software-attack-catalog.md +168 -0
- package/harden-app-security/references/red-team-extreme-scenarios.md +81 -0
- package/harden-app-security/references/risk-checklist.md +78 -0
- package/harden-app-security/references/security-test-patterns-agent.md +101 -0
- package/harden-app-security/references/security-test-patterns-finance.md +88 -0
- package/harden-app-security/references/test-snippets.md +73 -0
- package/improve-observability/SKILL.md +114 -0
- package/improve-observability/agents/openai.yaml +4 -0
- package/learn-skill-from-conversations/CHANGELOG.md +15 -0
- package/learn-skill-from-conversations/LICENSE +22 -0
- package/learn-skill-from-conversations/README.md +47 -0
- package/learn-skill-from-conversations/SKILL.md +85 -0
- package/learn-skill-from-conversations/agents/openai.yaml +4 -0
- package/learn-skill-from-conversations/scripts/extract_recent_conversations.py +369 -0
- package/learn-skill-from-conversations/tests/test_extract_recent_conversations.py +176 -0
- package/learning-error-book/SKILL.md +112 -0
- package/learning-error-book/agents/openai.yaml +4 -0
- package/learning-error-book/assets/error_book_template.md +66 -0
- package/learning-error-book/scripts/render_markdown_to_pdf.py +367 -0
- package/lib/cli.js +338 -0
- package/lib/installer.js +225 -0
- package/maintain-project-constraints/SKILL.md +109 -0
- package/maintain-project-constraints/agents/openai.yaml +4 -0
- package/maintain-skill-catalog/README.md +18 -0
- package/maintain-skill-catalog/SKILL.md +66 -0
- package/maintain-skill-catalog/agents/openai.yaml +4 -0
- package/novel-to-short-video/CHANGELOG.md +53 -0
- package/novel-to-short-video/LICENSE +21 -0
- package/novel-to-short-video/README.md +63 -0
- package/novel-to-short-video/SKILL.md +233 -0
- package/novel-to-short-video/agents/openai.yaml +4 -0
- package/novel-to-short-video/references/plan-template.md +71 -0
- package/novel-to-short-video/references/roles-json.md +41 -0
- package/open-github-issue/LICENSE +21 -0
- package/open-github-issue/README.md +97 -0
- package/open-github-issue/SKILL.md +119 -0
- package/open-github-issue/agents/openai.yaml +4 -0
- package/open-github-issue/scripts/open_github_issue.py +380 -0
- package/open-github-issue/tests/test_open_github_issue.py +159 -0
- package/open-source-pr-workflow/CHANGELOG.md +32 -0
- package/open-source-pr-workflow/LICENSE +21 -0
- package/open-source-pr-workflow/README.md +23 -0
- package/open-source-pr-workflow/SKILL.md +123 -0
- package/open-source-pr-workflow/agents/openai.yaml +4 -0
- package/openai-text-to-image-storyboard/.env.example +10 -0
- package/openai-text-to-image-storyboard/CHANGELOG.md +49 -0
- package/openai-text-to-image-storyboard/LICENSE +21 -0
- package/openai-text-to-image-storyboard/README.md +99 -0
- package/openai-text-to-image-storyboard/SKILL.md +107 -0
- package/openai-text-to-image-storyboard/agents/openai.yaml +4 -0
- package/openai-text-to-image-storyboard/scripts/generate_storyboard_images.py +763 -0
- package/package.json +36 -0
- package/record-spending/SKILL.md +113 -0
- package/record-spending/agents/openai.yaml +4 -0
- package/record-spending/references/account-format.md +33 -0
- package/record-spending/references/workbook-layout.md +84 -0
- package/resolve-review-comments/SKILL.md +122 -0
- package/resolve-review-comments/agents/openai.yaml +4 -0
- package/resolve-review-comments/references/adoption-criteria.md +23 -0
- package/resolve-review-comments/scripts/review_threads.py +425 -0
- package/resolve-review-comments/tests/test_review_threads.py +74 -0
- package/review-change-set/LICENSE +21 -0
- package/review-change-set/README.md +55 -0
- package/review-change-set/SKILL.md +103 -0
- package/review-change-set/agents/openai.yaml +4 -0
- package/review-codebases/LICENSE +21 -0
- package/review-codebases/README.md +67 -0
- package/review-codebases/SKILL.md +109 -0
- package/review-codebases/agents/openai.yaml +4 -0
- package/scripts/install_skills.ps1 +283 -0
- package/scripts/install_skills.sh +262 -0
- package/scripts/validate_openai_agent_config.py +194 -0
- package/scripts/validate_skill_frontmatter.py +110 -0
- package/specs-to-project-docs/LICENSE +21 -0
- package/specs-to-project-docs/README.md +57 -0
- package/specs-to-project-docs/SKILL.md +111 -0
- package/specs-to-project-docs/agents/openai.yaml +4 -0
- package/specs-to-project-docs/references/templates/architecture.md +29 -0
- package/specs-to-project-docs/references/templates/configuration.md +29 -0
- package/specs-to-project-docs/references/templates/developer-guide.md +33 -0
- package/specs-to-project-docs/references/templates/docs-index.md +39 -0
- package/specs-to-project-docs/references/templates/features.md +25 -0
- package/specs-to-project-docs/references/templates/getting-started.md +38 -0
- package/specs-to-project-docs/references/templates/readme.md +49 -0
- package/systematic-debug/LICENSE +21 -0
- package/systematic-debug/README.md +81 -0
- package/systematic-debug/SKILL.md +59 -0
- package/systematic-debug/agents/openai.yaml +4 -0
- package/text-to-short-video/.env.example +36 -0
- package/text-to-short-video/LICENSE +21 -0
- package/text-to-short-video/README.md +82 -0
- package/text-to-short-video/SKILL.md +221 -0
- package/text-to-short-video/agents/openai.yaml +4 -0
- package/text-to-short-video/scripts/enforce_video_aspect_ratio.py +350 -0
- package/version-release/CHANGELOG.md +53 -0
- package/version-release/LICENSE +21 -0
- package/version-release/README.md +28 -0
- package/version-release/SKILL.md +94 -0
- package/version-release/agents/openai.yaml +4 -0
- package/version-release/references/branch-naming.md +15 -0
- package/version-release/references/changelog-writing.md +8 -0
- package/version-release/references/commit-messages.md +19 -0
- package/version-release/references/readme-writing.md +12 -0
- package/version-release/references/semantic-versioning.md +12 -0
- package/video-production/CHANGELOG.md +104 -0
- package/video-production/LICENSE +18 -0
- package/video-production/README.md +68 -0
- package/video-production/SKILL.md +213 -0
- package/video-production/agents/openai.yaml +4 -0
- package/video-production/references/plan-template.md +54 -0
- package/video-production/references/roles-json.md +41 -0
- package/weekly-financial-event-report/SKILL.md +195 -0
- package/weekly-financial-event-report/agents/openai.yaml +4 -0
- package/weekly-financial-event-report/assets/financial_event_report_template.md +53 -0
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: text-to-short-video
|
|
3
|
+
description: Generate 30-60 second short videos by directly calling an OpenAI-compatible video generation API from text. Keep role consistency by using roles.json as role prompt source and only updating role descriptions.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Text to Short Video
|
|
7
|
+
|
|
8
|
+
## Dependencies
|
|
9
|
+
|
|
10
|
+
- Required: none.
|
|
11
|
+
- Conditional: none.
|
|
12
|
+
- Optional: none.
|
|
13
|
+
- Fallback: not applicable.
|
|
14
|
+
|
|
15
|
+
## Standards
|
|
16
|
+
|
|
17
|
+
- Evidence: Use `roles.json` as the authoritative role source and collect only the minimal prompt, duration, and sizing inputs.
|
|
18
|
+
- Execution: Stay API-only: resolve roles, build one prompt, submit the video job, poll until completion, and download one final MP4.
|
|
19
|
+
- Quality: Keep duration in the 30-60 second range, preserve role identity fields, and do not route through storyboard or Remotion skills.
|
|
20
|
+
- Output: Save the prompt package, API records, and final short-video artifacts under the project video workspace.
|
|
21
|
+
|
|
22
|
+
## Overview
|
|
23
|
+
|
|
24
|
+
This skill is **API-only**:
|
|
25
|
+
|
|
26
|
+
1. Resolve role consistency from `roles.json`.
|
|
27
|
+
2. Build one short-video prompt from text + roles.
|
|
28
|
+
3. Call an OpenAI-compatible video generation API.
|
|
29
|
+
4. Poll until the job is finished.
|
|
30
|
+
5. Download one final MP4.
|
|
31
|
+
6. Optionally run aspect-ratio/size post-processing.
|
|
32
|
+
|
|
33
|
+
Do not use `openai-text-to-image-storyboard` or `remotion-best-practices` in this skill.
|
|
34
|
+
|
|
35
|
+
## Required Inputs
|
|
36
|
+
|
|
37
|
+
Collect only what is required:
|
|
38
|
+
|
|
39
|
+
- `project_dir` (absolute path)
|
|
40
|
+
- `content_name` (output folder/file name)
|
|
41
|
+
- source text or user-locked prompt
|
|
42
|
+
- target size (`width x height`, default `1080x1920`)
|
|
43
|
+
- target duration seconds (default `50`, keep in `30-60` range)
|
|
44
|
+
|
|
45
|
+
If critical inputs are missing, ask concise follow-up questions.
|
|
46
|
+
|
|
47
|
+
## Role Definition (Required)
|
|
48
|
+
|
|
49
|
+
Always use:
|
|
50
|
+
|
|
51
|
+
- `<project_dir>/pictures/<content_name>/roles.json`
|
|
52
|
+
|
|
53
|
+
Required JSON format:
|
|
54
|
+
|
|
55
|
+
```json
|
|
56
|
+
{
|
|
57
|
+
"characters": [
|
|
58
|
+
{
|
|
59
|
+
"id": "lin_xia",
|
|
60
|
+
"name": "Lin Xia",
|
|
61
|
+
"appearance": "short black hair, amber eyes, slim build",
|
|
62
|
+
"outfit": "dark trench coat, silver pendant, leather boots",
|
|
63
|
+
"description": "standing calmly, observant expression"
|
|
64
|
+
}
|
|
65
|
+
]
|
|
66
|
+
}
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
Consistency rules:
|
|
70
|
+
|
|
71
|
+
- Use `roles.json` as the role prompt source.
|
|
72
|
+
- Keep `id`, `name`, `appearance`, `outfit` unchanged for existing roles.
|
|
73
|
+
- Only modify `description` to reflect this clip's action/emotion.
|
|
74
|
+
- If a new role is required, append a new role entry; never rewrite identity fields of existing roles.
|
|
75
|
+
- If no recurring roles exist yet, initialize with:
|
|
76
|
+
|
|
77
|
+
```json
|
|
78
|
+
{"characters": []}
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
## Environment Setup
|
|
82
|
+
|
|
83
|
+
Use this template:
|
|
84
|
+
|
|
85
|
+
- `/Users/tszkinlai/.codex/skills/text-to-short-video/.env.example`
|
|
86
|
+
|
|
87
|
+
Copy to:
|
|
88
|
+
|
|
89
|
+
- `/Users/tszkinlai/.codex/skills/text-to-short-video/.env`
|
|
90
|
+
|
|
91
|
+
Required keys:
|
|
92
|
+
|
|
93
|
+
- `OPENAI_API_URL`
|
|
94
|
+
- `OPENAI_API_KEY`
|
|
95
|
+
|
|
96
|
+
Optional keys:
|
|
97
|
+
|
|
98
|
+
- `OPENAI_VIDEO_MODEL`
|
|
99
|
+
- `OPENAI_VIDEO_DURATION_SECONDS`
|
|
100
|
+
- `OPENAI_VIDEO_ASPECT_RATIO`
|
|
101
|
+
- `OPENAI_VIDEO_SIZE`
|
|
102
|
+
- `OPENAI_VIDEO_POLL_SECONDS`
|
|
103
|
+
- `TEXT_TO_SHORT_VIDEO_WIDTH`
|
|
104
|
+
- `TEXT_TO_SHORT_VIDEO_HEIGHT`
|
|
105
|
+
|
|
106
|
+
## Workflow
|
|
107
|
+
|
|
108
|
+
### 1) Resolve `roles.json` before prompt generation
|
|
109
|
+
|
|
110
|
+
- Target path: `<project_dir>/pictures/<content_name>/roles.json`.
|
|
111
|
+
- If file exists, load and reuse role identities.
|
|
112
|
+
- If file does not exist, create it with the required schema.
|
|
113
|
+
- For existing roles, update only `description` when clip-specific motion/emotion is needed.
|
|
114
|
+
|
|
115
|
+
### 2) Build one generation prompt from text + roles
|
|
116
|
+
|
|
117
|
+
- If the user already gives an exact prompt, reuse it directly.
|
|
118
|
+
- Otherwise extract one concise visual prompt from source text.
|
|
119
|
+
- Keep the prompt focused on one coherent short narrative beat.
|
|
120
|
+
- Ensure all role identity details come from `roles.json` and only `description` is clip-specific.
|
|
121
|
+
- Save prompt package under:
|
|
122
|
+
- `<project_dir>/video/<content_name>/shorts/api/prompt_input.json`
|
|
123
|
+
|
|
124
|
+
Suggested local `prompt_input.json` structure:
|
|
125
|
+
|
|
126
|
+
```json
|
|
127
|
+
{
|
|
128
|
+
"roles_file": "<project_dir>/pictures/<content_name>/roles.json",
|
|
129
|
+
"description_overrides": {
|
|
130
|
+
"lin_xia": "running through rain, breathing hard, determined"
|
|
131
|
+
},
|
|
132
|
+
"final_prompt": "..."
|
|
133
|
+
}
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
### 3) Submit video generation request
|
|
137
|
+
|
|
138
|
+
- Endpoint: `${OPENAI_API_URL%/}/videos/generations`
|
|
139
|
+
- Send model/prompt/duration/size (or aspect ratio) in JSON payload.
|
|
140
|
+
- Save request and response records under:
|
|
141
|
+
- `<project_dir>/video/<content_name>/shorts/api/`
|
|
142
|
+
|
|
143
|
+
Example request fields (provider-compatible variants are allowed):
|
|
144
|
+
|
|
145
|
+
```json
|
|
146
|
+
{
|
|
147
|
+
"model": "${OPENAI_VIDEO_MODEL}",
|
|
148
|
+
"prompt": "...",
|
|
149
|
+
"duration": 50,
|
|
150
|
+
"size": "1080x1920",
|
|
151
|
+
"aspect_ratio": "9:16"
|
|
152
|
+
}
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
### 4) Poll job status until terminal state
|
|
156
|
+
|
|
157
|
+
- Read job ID from the create response.
|
|
158
|
+
- Poll `${OPENAI_API_URL%/}/videos/generations/<job_id>` every `OPENAI_VIDEO_POLL_SECONDS` seconds.
|
|
159
|
+
- Stop only on terminal state:
|
|
160
|
+
- success: download output video URL/file
|
|
161
|
+
- failure/cancelled: report provider error and stop
|
|
162
|
+
|
|
163
|
+
### 5) Download final MP4
|
|
164
|
+
|
|
165
|
+
Save to:
|
|
166
|
+
|
|
167
|
+
- `<project_dir>/video/<content_name>/shorts/<content_name>_important.mp4`
|
|
168
|
+
|
|
169
|
+
If provider returns multiple outputs, keep the best one that matches requested size/duration closest.
|
|
170
|
+
|
|
171
|
+
### 6) Enforce final aspect ratio and size (optional but recommended)
|
|
172
|
+
|
|
173
|
+
When output ratio or resolution differs from target, run:
|
|
174
|
+
|
|
175
|
+
```bash
|
|
176
|
+
python /Users/tszkinlai/.codex/skills/text-to-short-video/scripts/enforce_video_aspect_ratio.py \
|
|
177
|
+
--input-video "<downloaded_video_path>" \
|
|
178
|
+
--output-video "<final_output_video_path>" \
|
|
179
|
+
--env-file /Users/tszkinlai/.codex/skills/text-to-short-video/.env \
|
|
180
|
+
--force
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
Behavior:
|
|
184
|
+
|
|
185
|
+
- aspect ratio mismatch: center-crop then scale
|
|
186
|
+
- same ratio but different size: scale
|
|
187
|
+
- already matching: no-op/copy
|
|
188
|
+
|
|
189
|
+
## Output Contract
|
|
190
|
+
|
|
191
|
+
Return absolute paths for:
|
|
192
|
+
|
|
193
|
+
- `roles.json` used for role consistency
|
|
194
|
+
- prompt input JSON (if saved)
|
|
195
|
+
- API request payload JSON (if saved)
|
|
196
|
+
- API create response JSON (if saved)
|
|
197
|
+
- final downloaded `.mp4`
|
|
198
|
+
- post-processed `.mp4` (if post-processing executed)
|
|
199
|
+
|
|
200
|
+
Also report:
|
|
201
|
+
|
|
202
|
+
- role reuse summary (reused/added roles)
|
|
203
|
+
- which role descriptions were modified for this clip
|
|
204
|
+
- final prompt text source (user-locked or agent-extracted)
|
|
205
|
+
- job ID and final status
|
|
206
|
+
- duration check (`30-60` seconds)
|
|
207
|
+
- final render size check (`width x height`)
|
|
208
|
+
- whether center crop was applied
|
|
209
|
+
|
|
210
|
+
## Quality Gate Checklist
|
|
211
|
+
|
|
212
|
+
Before finishing, verify:
|
|
213
|
+
|
|
214
|
+
- generation path is API-only (no storyboard/remotion orchestration)
|
|
215
|
+
- `roles.json` uses required schema and is used as prompt source
|
|
216
|
+
- existing role identity fields (`id/name/appearance/outfit`) were not modified
|
|
217
|
+
- only role `description` was changed for clip-specific behavior
|
|
218
|
+
- job reached a successful terminal state
|
|
219
|
+
- output file exists at returned absolute path
|
|
220
|
+
- output duration is within `30-60` seconds (or user-approved exception)
|
|
221
|
+
- output size matches requested target (after post-processing if needed)
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
interface:
|
|
2
|
+
display_name: "Text to Short Video"
|
|
3
|
+
short_description: "Turn text into 30-60s short videos via API"
|
|
4
|
+
default_prompt: "Use $text-to-short-video to convert my text into one 30-60 second short video using an API-only workflow. Before building the final prompt, load <project_dir>/pictures/<content_name>/roles.json as the role source and keep role consistency: do not modify existing role id/name/appearance/outfit, only update role descriptions for clip-specific actions. Then call an OpenAI-compatible /videos/generations endpoint with settings from /Users/tszkinlai/.codex/skills/text-to-short-video/.env, poll until completion, download the MP4, and run post-process crop/resize only when output aspect ratio or size mismatches."
|
|
@@ -0,0 +1,350 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import argparse
|
|
5
|
+
import json
|
|
6
|
+
import os
|
|
7
|
+
import re
|
|
8
|
+
import shutil
|
|
9
|
+
import subprocess
|
|
10
|
+
import tempfile
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
|
|
13
|
+
SKILL_DIR = Path(__file__).resolve().parent.parent
|
|
14
|
+
DEFAULT_ENV_FILE = SKILL_DIR / ".env"
|
|
15
|
+
SIZE_PATTERN = re.compile(r"^(\d{2,5})x(\d{2,5})$")
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def parse_args() -> argparse.Namespace:
|
|
19
|
+
parser = argparse.ArgumentParser(
|
|
20
|
+
description=(
|
|
21
|
+
"Enforce final video aspect ratio and size. "
|
|
22
|
+
"If input aspect ratio differs from target, center-crop then scale."
|
|
23
|
+
)
|
|
24
|
+
)
|
|
25
|
+
parser.add_argument("--input-video", required=True, help="Path to rendered input video.")
|
|
26
|
+
parser.add_argument(
|
|
27
|
+
"--output-video",
|
|
28
|
+
help=(
|
|
29
|
+
"Path to processed output video. If omitted, write next to input as "
|
|
30
|
+
"<name>_aspect_fixed.mp4."
|
|
31
|
+
),
|
|
32
|
+
)
|
|
33
|
+
parser.add_argument(
|
|
34
|
+
"--in-place",
|
|
35
|
+
action="store_true",
|
|
36
|
+
help="Overwrite input file in place (uses a temporary file then replaces input).",
|
|
37
|
+
)
|
|
38
|
+
parser.add_argument(
|
|
39
|
+
"--target-size",
|
|
40
|
+
help="Target size in WIDTHxHEIGHT format, for example 1080x1920.",
|
|
41
|
+
)
|
|
42
|
+
parser.add_argument("--target-width", type=int, help="Target width in pixels.")
|
|
43
|
+
parser.add_argument("--target-height", type=int, help="Target height in pixels.")
|
|
44
|
+
parser.add_argument(
|
|
45
|
+
"--env-file",
|
|
46
|
+
default=str(DEFAULT_ENV_FILE),
|
|
47
|
+
help=f"Path to .env file (default: {DEFAULT_ENV_FILE}).",
|
|
48
|
+
)
|
|
49
|
+
parser.add_argument("--force", action="store_true", help="Overwrite output if it exists.")
|
|
50
|
+
parser.add_argument("--ffmpeg-bin", default="ffmpeg", help="ffmpeg executable name or path.")
|
|
51
|
+
parser.add_argument("--ffprobe-bin", default="ffprobe", help="ffprobe executable name or path.")
|
|
52
|
+
return parser.parse_args()
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def parse_dotenv_line(content: str, line_no: int, file_path: Path) -> tuple[str, str] | None:
|
|
56
|
+
stripped = content.strip()
|
|
57
|
+
if not stripped or stripped.startswith("#"):
|
|
58
|
+
return None
|
|
59
|
+
if stripped.startswith("export "):
|
|
60
|
+
stripped = stripped[7:].strip()
|
|
61
|
+
|
|
62
|
+
if "=" not in stripped:
|
|
63
|
+
raise SystemExit(f"Invalid .env format at {file_path}:{line_no}: missing =")
|
|
64
|
+
|
|
65
|
+
key, value = stripped.split("=", 1)
|
|
66
|
+
key = key.strip()
|
|
67
|
+
value = value.strip()
|
|
68
|
+
|
|
69
|
+
if not re.fullmatch(r"[A-Za-z_][A-Za-z0-9_]*", key):
|
|
70
|
+
raise SystemExit(f"Invalid .env key at {file_path}:{line_no}: {key}")
|
|
71
|
+
|
|
72
|
+
if value and value[0] in {"'", '"'}:
|
|
73
|
+
quote = value[0]
|
|
74
|
+
if len(value) >= 2 and value[-1] == quote:
|
|
75
|
+
value = value[1:-1]
|
|
76
|
+
else:
|
|
77
|
+
value = value.split(" #", 1)[0].strip()
|
|
78
|
+
|
|
79
|
+
return key, value
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def load_dotenv_file(env_file: Path, override: bool = False) -> bool:
|
|
83
|
+
if not env_file.exists():
|
|
84
|
+
return False
|
|
85
|
+
|
|
86
|
+
for line_no, line in enumerate(env_file.read_text(encoding="utf-8").splitlines(), start=1):
|
|
87
|
+
parsed = parse_dotenv_line(line, line_no, env_file)
|
|
88
|
+
if not parsed:
|
|
89
|
+
continue
|
|
90
|
+
key, value = parsed
|
|
91
|
+
if override or key not in os.environ:
|
|
92
|
+
os.environ[key] = value
|
|
93
|
+
return True
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def parse_size(value: str) -> tuple[int, int]:
|
|
97
|
+
match = SIZE_PATTERN.fullmatch(value.strip().lower())
|
|
98
|
+
if not match:
|
|
99
|
+
raise SystemExit("Invalid size format. Use WIDTHxHEIGHT, for example 1080x1920.")
|
|
100
|
+
width = int(match.group(1))
|
|
101
|
+
height = int(match.group(2))
|
|
102
|
+
if width <= 0 or height <= 0:
|
|
103
|
+
raise SystemExit("Width and height must be positive integers.")
|
|
104
|
+
return width, height
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def required_command(command: str) -> None:
|
|
108
|
+
if shutil.which(command):
|
|
109
|
+
return
|
|
110
|
+
raise SystemExit(f"Missing required command: {command}")
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def probe_video_size(video_path: Path, ffprobe_bin: str) -> tuple[int, int]:
|
|
114
|
+
cmd = [
|
|
115
|
+
ffprobe_bin,
|
|
116
|
+
"-v",
|
|
117
|
+
"error",
|
|
118
|
+
"-select_streams",
|
|
119
|
+
"v:0",
|
|
120
|
+
"-show_entries",
|
|
121
|
+
"stream=width,height",
|
|
122
|
+
"-of",
|
|
123
|
+
"json",
|
|
124
|
+
str(video_path),
|
|
125
|
+
]
|
|
126
|
+
try:
|
|
127
|
+
result = subprocess.run(cmd, check=True, capture_output=True, text=True)
|
|
128
|
+
except subprocess.CalledProcessError as exc:
|
|
129
|
+
stderr = exc.stderr.strip()
|
|
130
|
+
raise SystemExit(f"ffprobe failed for {video_path}: {stderr}") from exc
|
|
131
|
+
|
|
132
|
+
try:
|
|
133
|
+
payload = json.loads(result.stdout)
|
|
134
|
+
except json.JSONDecodeError as exc:
|
|
135
|
+
raise SystemExit(f"Unable to parse ffprobe output for {video_path}.") from exc
|
|
136
|
+
|
|
137
|
+
streams = payload.get("streams")
|
|
138
|
+
if not isinstance(streams, list) or not streams:
|
|
139
|
+
raise SystemExit(f"No video stream found in {video_path}.")
|
|
140
|
+
|
|
141
|
+
first = streams[0]
|
|
142
|
+
if not isinstance(first, dict):
|
|
143
|
+
raise SystemExit(f"Unexpected ffprobe stream payload for {video_path}.")
|
|
144
|
+
|
|
145
|
+
width = first.get("width")
|
|
146
|
+
height = first.get("height")
|
|
147
|
+
if not isinstance(width, int) or not isinstance(height, int) or width <= 0 or height <= 0:
|
|
148
|
+
raise SystemExit(f"Invalid video dimensions from ffprobe for {video_path}.")
|
|
149
|
+
return width, height
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def even_floor(value: int, minimum: int = 2) -> int:
|
|
153
|
+
floored = value if value % 2 == 0 else value - 1
|
|
154
|
+
return max(floored, minimum)
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def build_video_filter(
|
|
158
|
+
input_width: int,
|
|
159
|
+
input_height: int,
|
|
160
|
+
target_width: int,
|
|
161
|
+
target_height: int,
|
|
162
|
+
) -> tuple[str | None, bool]:
|
|
163
|
+
same_ratio = input_width * target_height == input_height * target_width
|
|
164
|
+
same_size = input_width == target_width and input_height == target_height
|
|
165
|
+
|
|
166
|
+
if same_ratio and same_size:
|
|
167
|
+
return None, False
|
|
168
|
+
|
|
169
|
+
if same_ratio:
|
|
170
|
+
return f"scale={target_width}:{target_height}", False
|
|
171
|
+
|
|
172
|
+
input_wider = input_width * target_height > input_height * target_width
|
|
173
|
+
if input_wider:
|
|
174
|
+
crop_width = input_height * target_width // target_height
|
|
175
|
+
crop_height = input_height
|
|
176
|
+
else:
|
|
177
|
+
crop_width = input_width
|
|
178
|
+
crop_height = input_width * target_height // target_width
|
|
179
|
+
|
|
180
|
+
crop_width = min(even_floor(crop_width), even_floor(input_width))
|
|
181
|
+
crop_height = min(even_floor(crop_height), even_floor(input_height))
|
|
182
|
+
|
|
183
|
+
offset_x = max((input_width - crop_width) // 2, 0)
|
|
184
|
+
offset_y = max((input_height - crop_height) // 2, 0)
|
|
185
|
+
|
|
186
|
+
return f"crop={crop_width}:{crop_height}:{offset_x}:{offset_y},scale={target_width}:{target_height}", True
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def resolve_target_size(args: argparse.Namespace) -> tuple[int, int]:
|
|
190
|
+
if args.target_size and (args.target_width is not None or args.target_height is not None):
|
|
191
|
+
raise SystemExit("Use either --target-size or --target-width/--target-height, not both.")
|
|
192
|
+
|
|
193
|
+
if args.target_size:
|
|
194
|
+
return parse_size(args.target_size)
|
|
195
|
+
|
|
196
|
+
env_width = os.getenv("TEXT_TO_SHORT_VIDEO_WIDTH", "").strip()
|
|
197
|
+
env_height = os.getenv("TEXT_TO_SHORT_VIDEO_HEIGHT", "").strip()
|
|
198
|
+
|
|
199
|
+
if args.target_width is not None:
|
|
200
|
+
width = args.target_width
|
|
201
|
+
else:
|
|
202
|
+
try:
|
|
203
|
+
width = int(env_width or "1080")
|
|
204
|
+
except ValueError as exc:
|
|
205
|
+
raise SystemExit("TEXT_TO_SHORT_VIDEO_WIDTH must be an integer.") from exc
|
|
206
|
+
|
|
207
|
+
if args.target_height is not None:
|
|
208
|
+
height = args.target_height
|
|
209
|
+
else:
|
|
210
|
+
try:
|
|
211
|
+
height = int(env_height or "1920")
|
|
212
|
+
except ValueError as exc:
|
|
213
|
+
raise SystemExit("TEXT_TO_SHORT_VIDEO_HEIGHT must be an integer.") from exc
|
|
214
|
+
|
|
215
|
+
if width <= 0 or height <= 0:
|
|
216
|
+
raise SystemExit("Target width and height must be positive integers.")
|
|
217
|
+
return width, height
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def resolve_output_path(args: argparse.Namespace, input_video: Path) -> Path:
|
|
221
|
+
if args.in_place and args.output_video:
|
|
222
|
+
raise SystemExit("Do not pass --output-video with --in-place.")
|
|
223
|
+
|
|
224
|
+
if args.in_place:
|
|
225
|
+
return input_video
|
|
226
|
+
|
|
227
|
+
if args.output_video:
|
|
228
|
+
output_video = Path(args.output_video).expanduser().resolve()
|
|
229
|
+
else:
|
|
230
|
+
output_video = input_video.with_name(f"{input_video.stem}_aspect_fixed.mp4")
|
|
231
|
+
|
|
232
|
+
if output_video == input_video:
|
|
233
|
+
raise SystemExit("Output path equals input path. Use --in-place to replace the input file.")
|
|
234
|
+
|
|
235
|
+
return output_video
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
def copy_if_needed(source: Path, target: Path, force: bool) -> None:
|
|
239
|
+
if source == target:
|
|
240
|
+
return
|
|
241
|
+
if target.exists() and not force:
|
|
242
|
+
raise SystemExit(f"Output already exists: {target}. Use --force to overwrite.")
|
|
243
|
+
target.parent.mkdir(parents=True, exist_ok=True)
|
|
244
|
+
shutil.copy2(source, target)
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
def main() -> int:
|
|
248
|
+
args = parse_args()
|
|
249
|
+
|
|
250
|
+
input_video = Path(args.input_video).expanduser().resolve()
|
|
251
|
+
if not input_video.is_file():
|
|
252
|
+
raise SystemExit(f"Input video not found: {input_video}")
|
|
253
|
+
|
|
254
|
+
env_file = Path(args.env_file).expanduser()
|
|
255
|
+
if not env_file.is_absolute():
|
|
256
|
+
env_file = SKILL_DIR / env_file
|
|
257
|
+
load_dotenv_file(env_file, override=False)
|
|
258
|
+
|
|
259
|
+
target_width, target_height = resolve_target_size(args)
|
|
260
|
+
output_video = resolve_output_path(args, input_video)
|
|
261
|
+
|
|
262
|
+
required_command(args.ffmpeg_bin)
|
|
263
|
+
required_command(args.ffprobe_bin)
|
|
264
|
+
|
|
265
|
+
input_width, input_height = probe_video_size(input_video, args.ffprobe_bin)
|
|
266
|
+
filter_expression, crop_applied = build_video_filter(
|
|
267
|
+
input_width=input_width,
|
|
268
|
+
input_height=input_height,
|
|
269
|
+
target_width=target_width,
|
|
270
|
+
target_height=target_height,
|
|
271
|
+
)
|
|
272
|
+
|
|
273
|
+
if filter_expression is None:
|
|
274
|
+
print(
|
|
275
|
+
f"[INFO] Video already matches target size and aspect ratio: "
|
|
276
|
+
f"{input_width}x{input_height}."
|
|
277
|
+
)
|
|
278
|
+
copy_if_needed(input_video, output_video, args.force)
|
|
279
|
+
if input_video != output_video:
|
|
280
|
+
print(f"[OK] Copied original video to: {output_video}")
|
|
281
|
+
return 0
|
|
282
|
+
|
|
283
|
+
replace_in_place = args.in_place
|
|
284
|
+
if replace_in_place:
|
|
285
|
+
temp_fd, temp_name = tempfile.mkstemp(
|
|
286
|
+
prefix=f"{input_video.stem}_aspect_tmp_",
|
|
287
|
+
suffix=".mp4",
|
|
288
|
+
dir=str(input_video.parent),
|
|
289
|
+
)
|
|
290
|
+
os.close(temp_fd)
|
|
291
|
+
temp_output = Path(temp_name)
|
|
292
|
+
else:
|
|
293
|
+
temp_output = output_video
|
|
294
|
+
if temp_output.exists() and not args.force:
|
|
295
|
+
raise SystemExit(f"Output already exists: {temp_output}. Use --force to overwrite.")
|
|
296
|
+
temp_output.parent.mkdir(parents=True, exist_ok=True)
|
|
297
|
+
|
|
298
|
+
ffmpeg_cmd = [
|
|
299
|
+
args.ffmpeg_bin,
|
|
300
|
+
"-hide_banner",
|
|
301
|
+
"-loglevel",
|
|
302
|
+
"error",
|
|
303
|
+
"-y" if (args.force or replace_in_place) else "-n",
|
|
304
|
+
"-i",
|
|
305
|
+
str(input_video),
|
|
306
|
+
"-vf",
|
|
307
|
+
filter_expression,
|
|
308
|
+
"-map",
|
|
309
|
+
"0:v:0",
|
|
310
|
+
"-map",
|
|
311
|
+
"0:a?",
|
|
312
|
+
"-c:v",
|
|
313
|
+
"libx264",
|
|
314
|
+
"-preset",
|
|
315
|
+
"medium",
|
|
316
|
+
"-crf",
|
|
317
|
+
"18",
|
|
318
|
+
"-c:a",
|
|
319
|
+
"aac",
|
|
320
|
+
"-movflags",
|
|
321
|
+
"+faststart",
|
|
322
|
+
str(temp_output),
|
|
323
|
+
]
|
|
324
|
+
|
|
325
|
+
try:
|
|
326
|
+
subprocess.run(ffmpeg_cmd, check=True)
|
|
327
|
+
except subprocess.CalledProcessError as exc:
|
|
328
|
+
if replace_in_place and temp_output.exists():
|
|
329
|
+
temp_output.unlink(missing_ok=True)
|
|
330
|
+
raise SystemExit(f"ffmpeg failed with exit code {exc.returncode}.") from exc
|
|
331
|
+
|
|
332
|
+
if replace_in_place:
|
|
333
|
+
temp_output.replace(input_video)
|
|
334
|
+
final_output = input_video
|
|
335
|
+
else:
|
|
336
|
+
final_output = output_video
|
|
337
|
+
|
|
338
|
+
final_width, final_height = probe_video_size(final_output, args.ffprobe_bin)
|
|
339
|
+
print(
|
|
340
|
+
f"[OK] Processed video written: {final_output}\n"
|
|
341
|
+
f"[INFO] Input size: {input_width}x{input_height}\n"
|
|
342
|
+
f"[INFO] Target size: {target_width}x{target_height}\n"
|
|
343
|
+
f"[INFO] Output size: {final_width}x{final_height}\n"
|
|
344
|
+
f"[INFO] Center crop applied: {'yes' if crop_applied else 'no'}"
|
|
345
|
+
)
|
|
346
|
+
return 0
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
if __name__ == "__main__":
|
|
350
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project will be documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on Keep a Changelog.
|
|
6
|
+
|
|
7
|
+
## [0.3.0] - 2026-02-22
|
|
8
|
+
|
|
9
|
+
### Changed
|
|
10
|
+
|
|
11
|
+
- Added a default `commit+push only` workflow when users do not explicitly request versioning.
|
|
12
|
+
- Restricted version bump recommendations to explicit user requests instead of proactive suggestions.
|
|
13
|
+
- Updated workflow docs to make release-specific steps conditional on explicit version/tag/release intent.
|
|
14
|
+
|
|
15
|
+
## [0.2.0] - 2026-02-15
|
|
16
|
+
|
|
17
|
+
### Changed
|
|
18
|
+
|
|
19
|
+
- Added an explicit dependency contract requiring `edge-case-test-fixer` then `code-simplifier` for code-affecting releases.
|
|
20
|
+
- Added a docs-only classification path so documentation-only changes can skip both dependency skills.
|
|
21
|
+
- Reordered the submit workflow to run dependency checks before version/changelog/readme/agent document updates.
|
|
22
|
+
|
|
23
|
+
## [0.1.3] - 2026-02-10
|
|
24
|
+
|
|
25
|
+
### Changed
|
|
26
|
+
|
|
27
|
+
- Added explicit guidance to infer and share a preferred semver bump before user confirmation.
|
|
28
|
+
- Required submit prompts to include a recommended target version with a brief rationale.
|
|
29
|
+
- Updated README workflow description to reflect recommendation-first version selection.
|
|
30
|
+
|
|
31
|
+
## [0.1.2] - 2026-02-07
|
|
32
|
+
|
|
33
|
+
### Changed
|
|
34
|
+
|
|
35
|
+
- Unified submit behavior into a single workflow without a mode-selection step.
|
|
36
|
+
- Standardized release scope analysis to always use the last version tag to `HEAD`.
|
|
37
|
+
- Moved `AGENTS.md` synchronization to run before commit and push, aligned with README sync timing.
|
|
38
|
+
- Updated commit staging guidance to include optional `AGENTS.md` changes when applicable.
|
|
39
|
+
|
|
40
|
+
## [0.1.1] - 2026-02-07
|
|
41
|
+
|
|
42
|
+
### Changed
|
|
43
|
+
|
|
44
|
+
- Added a brief introduction section to improve skill onboarding guidance.
|
|
45
|
+
- Clarified submit workflow rules so commit messages are derived from staged diff intent.
|
|
46
|
+
- Prohibited version-only commit subjects such as `feat: vX.Y.Z`.
|
|
47
|
+
- Added release-only commit message guidance and examples in commit message references.
|
|
48
|
+
|
|
49
|
+
[0.1.1]: https://github.com/LaiTszKin/submit-changes/compare/v0.1.0...v0.1.1
|
|
50
|
+
[0.1.2]: https://github.com/LaiTszKin/submit-changes/compare/v0.1.1...v0.1.2
|
|
51
|
+
[0.1.3]: https://github.com/LaiTszKin/submit-changes/compare/v0.1.2...v0.1.3
|
|
52
|
+
[0.2.0]: https://github.com/LaiTszKin/submit-changes/compare/v0.1.3...v0.2.0
|
|
53
|
+
[0.3.0]: https://github.com/LaiTszKin/submit-changes/compare/v0.2.0...v0.3.0
|
|
@@ -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,28 @@
|
|
|
1
|
+
# version-release
|
|
2
|
+
|
|
3
|
+
A Codex skill for explicit release workflows: code/documentation alignment, version bump, changelog update, tagging, and push.
|
|
4
|
+
|
|
5
|
+
## What this skill does
|
|
6
|
+
|
|
7
|
+
`version-release` helps agents perform release work in a repeatable flow:
|
|
8
|
+
|
|
9
|
+
1. Inspect the release scope from git history.
|
|
10
|
+
2. Run quality gates for code-affecting changes.
|
|
11
|
+
3. Run `specs-to-project-docs` when the release scope contains new completed spec files.
|
|
12
|
+
4. Align project code and categorized project docs.
|
|
13
|
+
5. Resolve version and tag details.
|
|
14
|
+
6. Update version files and changelog.
|
|
15
|
+
7. Commit release metadata.
|
|
16
|
+
8. Create and push the release tag.
|
|
17
|
+
|
|
18
|
+
## Scope
|
|
19
|
+
|
|
20
|
+
Use this skill only when the user explicitly asks for:
|
|
21
|
+
|
|
22
|
+
- release preparation
|
|
23
|
+
- version bumping
|
|
24
|
+
- tag creation/publishing
|
|
25
|
+
|
|
26
|
+
If the release includes new completed specs, convert them into categorized project docs first and let `specs-to-project-docs` remove or archive the superseded spec files.
|
|
27
|
+
|
|
28
|
+
If the user only wants commit + push, use `commit-and-push`.
|