Skip to content

feat(workflows): add the short-form video workflow pack - #2521

Open
Wirasm wants to merge 1 commit into
devfrom
feat/video-workflow-pack
Open

Wirasm wants to merge 1 commit into
devfrom
feat/video-workflow-pack

Conversation

@Wirasm

@Wirasm Wirasm commented Aug 10, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • Problem: Archon positions itself as a governed automation engine beyond coding, but the repo carries no worked example of a non-coding, media-producing automation with human review loops.
  • Why it matters: the video pack is a concrete ops-automation proof — topic in, finished 1080x1920 MP4 + per-platform post copy out, with artifacts, state, and an approval-gated review loop that evolves per-kind playbooks.
  • What changed: adds .archon/workflows/video/ (three producer workflows: social-short, ugc-video, product-video; a human-gated video-quality-review; a shared video-render-block include) and five uv-runtime Python scripts under .archon/scripts/ (Pexels clip fetch, Cartesia TTS with word timestamps, ASS karaoke captions, ffmpeg compose, quality gate) plus _common.py.
  • What did not change (scope boundary): zero engine/package code — no packages/ changes, no bundled defaults (workflows/defaults/ untouched, no generate:bundled needed), no schema, no API. Publishing/posting is explicitly out of scope; runs stop at store.

UX Journey

Before

  User                          Archon
  ────                          ──────
  wants a short-form video ──▶  (nothing — no media workflows in repo)
  produces video by hand

After

  User                          Archon                              External services
  ────                          ──────                              ─────────────────
  workflow run social-short ──▶ read-strategy ($STATE_DIR playbook)
                                gen-brief / write-copy (LLM tiers)
                                [include: video-render-block]
                                  fetch-clips ──────────────────▶  Pexels API
                                  narrate ─────────────────────▶  Cartesia TTS (word timestamps)
                                  captions → compose → qc          (local ffmpeg)
                                store ──▶ $STATE_DIR/content/videos/<run-id>/
  gets MP4 + post copy ◀──────  (never publishes)

  workflow run video-quality-review ──▶ frame stills → vision review (Claude)
                                        [approval gate] ──▶ playbook update (one file, per kind)

Architecture Diagram

Before

.archon/workflows/   (coding-oriented workflows only)
.archon/scripts/     (no media scripts)

After

[+] .archon/workflows/video/social-short.yaml ──┐
[+] .archon/workflows/video/ugc-video.yaml ─────┼── include: ═══▶ [+] video-render-block.yaml
[+] .archon/workflows/video/product-video.yaml ─┘                     │ bash/script nodes
[+] .archon/workflows/video/video-quality-review.yaml                 ▼
[+] .archon/scripts/{fetch-clips,tts-narrate,build-captions,compose-video,quality-check,_common}.py
        │ reads/writes                                          reads .env keys
        ▼                                                       (PEXELS_API_KEY, CARTESIA_API_KEY)
    $ARTIFACTS_DIR (per-run scratch) / $STATE_DIR/content/ (playbooks + video library)

Connection inventory (list every module-to-module edge, mark changes):

From To Status Notes
producer workflows ×3 video-render-block new load-time include: expansion; block reads brief.json/copy.json from $ARTIFACTS_DIR, no node refs into the parent
video-render-block .archon/scripts/*.py new script: nodes, runtime: uv, explicit timeout:
scripts Pexels / Cartesia APIs new keys from repo-root .env or Archon-managed env vars; fail fast by name when unset
video-quality-review $STATE_DIR/content/ playbooks new approval-gated; per-kind playbook files, writes exactly one file per run
workflow engine / packages anything here unchanged pure content addition — engine sees ordinary workflows

Label Snapshot

  • Risk: risk: low
  • Size: size: L
  • Scope: workflows
  • Module: workflows:examples

Change Metadata

  • Change type: feature
  • Primary scope: workflows

Linked Issue

Validation Evidence (required)

Commands and result summary:

bun run validate   # full suite — passed (type-check, lint, format, tests, bundled/schema/capability checks)
  • Evidence provided (test/log/trace/screenshot): full bun run validate pass on this branch; the diff touches no TypeScript, so the suite guards against accidental engine/bundle drift. Workflows load through normal discovery (archon workflow list).
  • If any command is intentionally skipped, explain why: n/a

Security Impact (required)

  • New permissions/capabilities? (No)
  • New external network calls? (Yes)
  • Secrets/tokens handling changed? (No)
  • File system access scope changed? (No)
  • If any Yes, describe risk and mitigation: scripts call Pexels (stock footage) and Cartesia (TTS) with user-supplied API keys read from .env/managed env vars. Keys are never logged or written to artifacts; an unset key fails immediately by name. Both calls happen only inside runs of these opt-in workflows.

Compatibility / Migration

  • Backward compatible? (Yes)
  • Config/env changes? (Yes — two new optional env vars, PEXELS_API_KEY and CARTESIA_API_KEY (+ optional CARTESIA_VOICE_ID), needed only to run this pack; ffmpeg and uv on PATH)
  • Database migration needed? (No)
  • If yes, exact upgrade steps: add the two keys to the repo .env (or Archon per-project env vars) before first run.

Human Verification (required)

What was personally validated beyond CI:

  • Verified scenarios: end-to-end runs of the producer workflows (real Pexels + Cartesia + ffmpeg) producing stored MP4s with karaoke captions and post copy; video-quality-review run against a finished video including the approval gate and a per-kind playbook write.
  • Edge cases checked: unset API key fails fast by name; store staging via <run-id>.partial so an interrupted run can't leave a half-populated library entry; tier/provider interaction warnings (README "Authoring gotchas").
  • What was not verified: non-Claude providers for the vision review node (deliberately pinned to provider: claude; other providers fail loudly there), Cartesia voices other than the default.

Side Effects / Blast Radius (required)

  • Affected subsystems/workflows: none outside this pack — additive content in .archon/; engine, defaults bundle, and all existing workflows untouched.
  • Potential unintended effects: workflow discovery now lists five more workflows in this repo; runs consume Cartesia credits (~$0.02–0.15/video).
  • Guardrails/monitoring for early detection: quality-check node gates compose output; runs stop at store (no publishing surface exists); all output lands under $STATE_DIR/$ARTIFACTS_DIR, never in the repo.

Rollback Plan (required)

  • Fast rollback command/path: revert the single commit (git revert 58196a5f) — no state, schema, or generated files to unwind.
  • Feature flags or config toggles (if any): none needed; workflows simply disappear from discovery on revert.
  • Observable failure symptoms: failed runs of the video workflows; nothing else can be affected.

Risks and Mitigations

  • Risk: external API drift (Pexels/Cartesia response shapes) breaking scripts silently.
    • Mitigation: scripts fail loudly on unexpected responses; quality-check node validates the composed artifact before store.
  • Risk: per-kind playbooks drifting into contradiction over time.
    • Mitigation: reviewer declares target: kind | common and writes exactly one file per run; human approval gates every playbook change.

Summary by CodeRabbit

  • New Features

    • Added workflows for product marketing videos, social shorts, and UGC videos.
    • Generate vertical 1080×1920 videos with narration, captions, stock footage, music, and platform-specific copy.
    • Added automated video quality checks covering dimensions, timing, audio, frame rate, and visual issues.
    • Added a human-gated quality review workflow with thumbnail selection and strategy updates.
    • Video outputs and supporting artifacts are stored in a searchable library without automatic publishing.
  • Documentation

    • Added setup, execution, review, and authoring guidance for the video workflow pack.

Three video kinds — social-short, ugc-video, product-video — sharing one
mechanical render block via include:, plus a human-gated craft review that
maintains per-kind playbooks.

Content generation (script, description, hashtags, search keywords, alt
text) is workflow-owned so it is reproducible and inside the audit trail.
Preview/presentation is deliberately NOT here.

Needs PEXELS_API_KEY and CARTESIA_API_KEY in the repo-root .env plus ffmpeg
and uv; no LLM key of its own. Unconfigured runs fail by name rather than
degrading. No secrets are committed.
@coderabbitai

coderabbitai Bot commented Aug 10, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

The PR adds a complete video workflow pack. Shared scripts fetch footage, generate narration and captions, compose portrait videos, run quality checks, and store artifacts. Product, social-short, and UGC workflows generate content. A human-gated workflow reviews stored videos and updates playbooks.

Changes

Video pipeline

Layer / File(s) Summary
Shared media helpers
.archon/scripts/_common.py
Adds shared environment, artifact, JSON, subprocess, and media metadata helpers.
Media input generation
.archon/scripts/fetch-clips.py, .archon/scripts/tts-narrate.py, .archon/scripts/build-captions.py
Fetches portrait clips, generates timestamped narration, and writes karaoke-style ASS captions.
Video rendering and storage
.archon/scripts/compose-video.py, .archon/scripts/quality-check.py, .archon/workflows/video/video-render-block.yaml
Composes portrait video with captions and mixed audio, validates technical quality, and stores artifacts in the video library.
Content generation workflows
.archon/workflows/video/product-video.yaml, .archon/workflows/video/social-short.yaml, .archon/workflows/video/ugc-video.yaml, .archon/workflows/video/README.md
Adds three workflows that generate briefs and platform copy, validate JSON artifacts, and invoke the shared renderer.
Human-gated quality review
.archon/workflows/video/video-quality-review.yaml
Selects stored videos, collects review inputs, requests a structured Claude assessment, applies approved playbook changes, and records review metadata.

Estimated code review effort: 5 (Critical) | ~120 minutes

Sequence Diagram(s)

sequenceDiagram
  participant ContentWorkflow
  participant video-render-block
  participant MediaScripts
  participant compose-video
  participant quality-check
  participant VideoLibrary
  ContentWorkflow->>video-render-block: start rendering after brief and copy
  video-render-block->>MediaScripts: fetch clips, narration, and captions
  MediaScripts-->>video-render-block: media artifacts
  video-render-block->>compose-video: compose video.mp4
  compose-video-->>video-render-block: rendered video
  video-render-block->>quality-check: validate video and create frames
  quality-check-->>video-render-block: quality.json and frames
  video-render-block->>VideoLibrary: store artifacts and catalog entry
Loading
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 38.10% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely describes the primary change: adding a short-form video workflow pack.
Description check ✅ Passed The description covers the required summary, UX, architecture, metadata, validation, security, compatibility, verification, risks, and rollback sections.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/video-workflow-pack

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 12

🧹 Nitpick comments (4)
.archon/scripts/compose-video.py (2)

26-29: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Silence the random lint or use secrets.

Ruff reports S311 as an error on line 29. The selection is a music bed, so random is acceptable. Add an inline suppression with the reason to keep the lint gate green.

♻️ Proposed change
-    return random.choice(tracks) if tracks else None
+    # Cosmetic variety only; no security property depends on this choice.
+    return random.choice(tracks) if tracks else None  # noqa: S311
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.archon/scripts/compose-video.py around lines 26 - 29, Update the
random.choice call in pick_music with an inline Ruff S311 suppression, including
a brief reason that non-cryptographic randomness is acceptable for selecting a
music bed.

Source: Linters/SAST tools


101-109: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Pass cwd through run

Add an optional cwd parameter to _common.run, forward it to subprocess.run, and replace the os.chdir block with run(args, cwd=wd).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.archon/scripts/compose-video.py around lines 101 - 109, Update _common.run
to accept an optional cwd parameter and pass it through to subprocess.run. In
the video composition flow, remove the os.chdir try/finally block and invoke run
with cwd=wd while preserving the existing output reporting.
.archon/workflows/video/video-render-block.yaml (2)

68-81: 🗄️ Data Integrity & Integration | 🔵 Trivial | 💤 Low value

Confirm the concurrency behaviour of latest and library.jsonl.

Two runs of different video kinds can execute in parallel. Line 81 repoints content/latest, and line 110 appends to content/library.jsonl. The staged .partial directory protects the per-run directory, but the symlink and the ledger have no such protection. If parallel runs are supported, document the last-writer-wins behaviour of latest, or write per-kind symlinks.

Also applies to: 82-113

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.archon/workflows/video/video-render-block.yaml around lines 68 - 81, Update
the workflow around the `content/latest` symlink and `content/library.jsonl`
append to explicitly handle concurrent runs of different video kinds: either
document and preserve last-writer-wins semantics for `latest` while ensuring
ledger appends are concurrency-safe, or replace the shared symlink with per-kind
symlinks. Keep the existing `.partial` staging protection for each run
directory.

79-79: 🗄️ Data Integrity & Integration | 🔵 Trivial | 💤 Low value

Build meta.json with a JSON serializer.

printf with %s does not escape the values. If VIDEO_KIND or RUN_ID contains a quote or a backslash, meta.json becomes invalid JSON and any consumer fails to parse it. The heredoc on line 82 already runs python3, so write the file there with json.dumps.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.archon/workflows/video/video-render-block.yaml at line 79, Replace the
printf-based meta.json creation in the video render workflow with JSON
serialization in the existing Python heredoc that runs afterward. Pass
VIDEO_KIND and RUN_ID into that Python block, serialize them with json.dumps as
the kind and run_id fields, and preserve the destination path and output
structure.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In @.archon/scripts/_common.py:
- Around line 44-47: Update load_env to include non-empty variables from
os.environ even when their keys are absent from the parsed .env mapping, while
preserving existing .env values unless a process value should override them.
Ensure process-only values such as CARTESIA_VOICE_ID are available to callers
like tts-narrate.py.

In @.archon/scripts/compose-video.py:
- Around line 34-54: Validate that clips loaded by compose-video’s
read_json("clips.json") call are a non-empty list before the total/ideal segment
math and any len(clips) or division usage. Raise SystemExit with a clear message
identifying the missing, invalid, or empty clips manifest, while preserving the
existing selection logic for valid clips.

In @.archon/scripts/quality-check.py:
- Around line 55-57: In .archon/scripts/quality-check.py lines 55-57, update the
framerate calculation to treat a zero denominator as an unknown framerate
without dividing by zero, while preserving the existing check behavior. In lines
59-67, use the video stream duration when format.duration is absent; if neither
duration is available, raise SystemExit with a clear diagnostic message so
quality.json is still produced when possible.
- Around line 27-30: Update ffmpeg_stderr to check the CompletedProcess return
code after running ffmpeg and fail immediately on non-zero status, propagating
the analysis error instead of returning stderr for downstream parsing. Preserve
the existing stderr return behavior for successful executions.

In @.archon/workflows/video/README.md:
- Line 30: Update the three fenced code blocks in the README to specify the text
language identifier, including the blocks containing the error output, workflow
directory layout, and $STATE_DIR/content/ path. Use text for each fence while
preserving their contents.

In @.archon/workflows/video/social-short.yaml:
- Around line 35-52: Update the prompt in
.archon/workflows/video/social-short.yaml at lines 35-52 to require a sourced
fact bundle before making factual public-facing claims; when sources are absent,
restrict the script to clearly framed opinion rather than presenting claims as
facts. Update .archon/workflows/video/ugc-video.yaml at lines 38-65 to require
verified first-person source material and consent for testimonials, otherwise
generate a clearly disclosed dramatization, and do not require personal details
that the input does not provide.

In @.archon/workflows/video/video-quality-review.yaml:
- Around line 29-34: Update the target-discovery assignment in the video quality
review workflow so an expected non-zero status from ls does not trigger set -e
before the existing target validation guard runs. Preserve the current
newest-directory selection, then allow the [ -z "${target:-}" ] or [ ! -d
"$target" ] check to emit the friendly guidance and exit.
- Line 53: Update the kind extraction command in the pick-video workflow to pass
the metadata path through an environment variable rather than interpolating $t
inside Python source. Have the Python code read that variable before loading
meta.json, while preserving the existing unknown fallback for failures.
- Around line 166-180: Update the approval message in the approval gate to
display the resolved playbook filename, not the raw target kind, and include the
new_strategy_markdown content or a diff-style preview that apply-strategy will
write. Ensure the gate exposes the actual destination and full pending document
so approval does not rely solely on proposed_change or its summary.
- Around line 227-231: Update the thumbnail_frame persistence block to accept
only non-boolean integers within the valid 1-based range of frames actually
present, rejecting zero, negative values, and values beyond the available frame
count before writing copy.json. Use the workflow’s existing frame collection or
metadata to determine the upper bound, and preserve the current write behavior
for valid values.
- Around line 211-223: Update the review persistence flow around the strategy
write and reviews.jsonl append: retain the existing archive copy before
replacement, append the ledger row before changing the active playbook, and
write the playbook through a temporary file followed by an atomic rename. Ensure
interrupted writes cannot truncate the playbook and a crash leaves the review
recorded but unapplied.
- Around line 199-202: Update the kind resolution and validation in the review
workflow, including the gather logic, so a missing or unresolved meta.json kind
does not default to "unknown" when review.target is "kind". Fail the node before
reading or writing a playbook in that case, while preserving the common-target
path and existing behavior for valid kinds.

---

Nitpick comments:
In @.archon/scripts/compose-video.py:
- Around line 26-29: Update the random.choice call in pick_music with an inline
Ruff S311 suppression, including a brief reason that non-cryptographic
randomness is acceptable for selecting a music bed.
- Around line 101-109: Update _common.run to accept an optional cwd parameter
and pass it through to subprocess.run. In the video composition flow, remove the
os.chdir try/finally block and invoke run with cwd=wd while preserving the
existing output reporting.

In @.archon/workflows/video/video-render-block.yaml:
- Around line 68-81: Update the workflow around the `content/latest` symlink and
`content/library.jsonl` append to explicitly handle concurrent runs of different
video kinds: either document and preserve last-writer-wins semantics for
`latest` while ensuring ledger appends are concurrency-safe, or replace the
shared symlink with per-kind symlinks. Keep the existing `.partial` staging
protection for each run directory.
- Line 79: Replace the printf-based meta.json creation in the video render
workflow with JSON serialization in the existing Python heredoc that runs
afterward. Pass VIDEO_KIND and RUN_ID into that Python block, serialize them
with json.dumps as the kind and run_id fields, and preserve the destination path
and output structure.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 4c07ee6c-3640-4dbc-9f2e-d84628bb1766

📥 Commits

Reviewing files that changed from the base of the PR and between 41765d6 and 58196a5.

📒 Files selected for processing (12)
  • .archon/scripts/_common.py
  • .archon/scripts/build-captions.py
  • .archon/scripts/compose-video.py
  • .archon/scripts/fetch-clips.py
  • .archon/scripts/quality-check.py
  • .archon/scripts/tts-narrate.py
  • .archon/workflows/video/README.md
  • .archon/workflows/video/product-video.yaml
  • .archon/workflows/video/social-short.yaml
  • .archon/workflows/video/ugc-video.yaml
  • .archon/workflows/video/video-quality-review.yaml
  • .archon/workflows/video/video-render-block.yaml

Comment on lines +44 to +47
for k in list(env):
if os.environ.get(k):
env[k] = os.environ[k]
return env

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Load process-only environment variables.

load_env only copies process values for keys already present in .env. If Archon injects only CARTESIA_VOICE_ID, .archon/scripts/tts-narrate.py line 35 ignores it and uses DEFAULT_VOICE. require_key still finds injected API keys, so missing-key tests do not detect this configuration defect.

Proposed fix
-    for k in list(env):
-        if os.environ.get(k):
-            env[k] = os.environ[k]
+    env.update(os.environ)
     return env
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
for k in list(env):
if os.environ.get(k):
env[k] = os.environ[k]
return env
env.update(os.environ)
return env
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.archon/scripts/_common.py around lines 44 - 47, Update load_env to include
non-empty variables from os.environ even when their keys are absent from the
parsed .env mapping, while preserving existing .env values unless a process
value should override them. Ensure process-only values such as CARTESIA_VOICE_ID
are available to callers like tts-narrate.py.

Comment on lines +34 to +54
clips = read_json("clips.json")
words = read_json("words.json")

narration = wd / "narration.wav"
captions = wd / "captions.ass"
for required in (narration, captions):
if not required.exists():
raise SystemExit(f"missing {required}")

total = float(words["duration"]) + TAIL_PADDING
ideal = max(1, round(total / SECONDS_PER_CLIP))
if len(clips) >= ideal:
chosen = clips[:ideal]
elif total / len(clips) <= MAX_SECONDS_PER_CLIP:
# Slightly longer shots beat showing the same footage twice.
chosen = list(clips)
else:
# Genuinely not enough footage — cycle, and say so.
chosen = [clips[i % len(clips)] for i in range(ideal)]
log(f"WARN only {len(clips)} clips for {ideal} segments — footage will repeat")
seg = total / len(chosen)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify the producer contract for clips.json and the read_json return type.
fd -t f 'fetch-clips.py' -x rg -n -C4 'clips\.json|write_json|"path"'
fd -t f '_common.py' -x rg -n -C3 'def read_json|def write_json|def run'

Repository: coleam00/Archon

Length of output: 153


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== files =="
git ls-files | sed -n '1,200p' | rg '(^|/)compose-video\.py$|(^|/)fetch-.*clips.*\.py$|(^|*)_common\.py$|clips\.json$|words\.json$' || true

echo "== compose-video outline/slices =="
compose="$(git ls-files | rg '(^|/)compose-video\.py$' | head -n1)"
if [ -n "${compose:-}" ]; then
  wc -l "$compose"
  cat -n "$compose" | sed -n '1,140p'
fi

echo "== fetch scripts matching clips =="
for f in $(git ls-files | rg 'fetch-.*clips.*\.py$|fetch-clips.*\.py$'); do
  echo "--- $f"
  wc -l "$f"
  cat -n "$f" | sed -n '1,220p'
done

echo "== _common read_json/write_json snippets =="
for f in $(git ls-files | rg '(^|)_common\.py$'); do
  echo "--- $f"
  rg -n -C5 'read_json|write_json|def run' "$f" || true
done

Repository: coleam00/Archon

Length of output: 10753


Guard clips.json before segment math.

.archon/scripts/fetch-clips.py writes manifest directly, so the list shape is intended. But an absent, corrupted, or non-list/empty clips.json reaches .archon/scripts/compose-video.py:47 and either TypeError: len() of unsized object or ZeroDivisionError, with no clear cause. Add a validation guard and fail with an informative message before computing segments.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.archon/scripts/compose-video.py around lines 34 - 54, Validate that clips
loaded by compose-video’s read_json("clips.json") call are a non-empty list
before the total/ideal segment math and any len(clips) or division usage. Raise
SystemExit with a clear message identifying the missing, invalid, or empty clips
manifest, while preserving the existing selection logic for valid clips.

Source: Linters/SAST tools

Comment on lines +27 to +30
def ffmpeg_stderr(args: list[str]) -> str:
return subprocess.run(
["ffmpeg", "-hide_banner", *args], capture_output=True, text=True
).stderr

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Check the ffmpeg exit status in ffmpeg_stderr.

The function ignores the return code. If ffmpeg fails, it returns error text or an empty string. Two downstream effects follow:

  • The volumedetect regexes do not match, mean_db becomes -99.0, and QC reports a fatal audio_level defect that does not exist.
  • The blackdetect output is empty, so no_dead_frames passes even though the analysis never ran.

Fail on a non-zero exit so an analysis error is not reported as a video defect.

🛡️ Proposed fix
 def ffmpeg_stderr(args: list[str]) -> str:
-    return subprocess.run(
-        ["ffmpeg", "-hide_banner", *args], capture_output=True, text=True
-    ).stderr
+    proc = subprocess.run(
+        ["ffmpeg", "-hide_banner", *args], capture_output=True, text=True
+    )
+    if proc.returncode != 0:
+        raise SystemExit(f"ffmpeg analysis failed ({proc.returncode}): {proc.stderr.strip()[-500:]}")
+    return proc.stderr
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
def ffmpeg_stderr(args: list[str]) -> str:
return subprocess.run(
["ffmpeg", "-hide_banner", *args], capture_output=True, text=True
).stderr
def ffmpeg_stderr(args: list[str]) -> str:
proc = subprocess.run(
["ffmpeg", "-hide_banner", *args], capture_output=True, text=True
)
if proc.returncode != 0:
raise SystemExit(
f"ffmpeg analysis failed ({proc.returncode}): "
f"{proc.stderr.strip()[-500:]}"
)
return proc.stderr
🧰 Tools
🪛 ast-grep (0.45.0)

[error] 27-29: Command coming from incoming request
Context: subprocess.run(
["ffmpeg", "-hide_banner", *args], capture_output=True, text=True
)
Note: [CWE-78] Improper Neutralization of Special Elements used in an OS Command ('OS Command Injection').

(subprocess-from-request)

🪛 Ruff (0.16.1)

[error] 28-28: subprocess call: check for execution of untrusted input

(S603)


[error] 29-29: Starting a process with a partial executable path

(S607)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.archon/scripts/quality-check.py around lines 27 - 30, Update ffmpeg_stderr
to check the CompletedProcess return code after running ffmpeg and fail
immediately on non-zero status, propagating the analysis error instead of
returning stderr for downstream parsing. Preserve the existing stderr return
behavior for successful executions.

Comment on lines +55 to +57
num, den = (vstream.get("r_frame_rate") or "0/1").split("/")
fps = float(num) / float(den or 1)
check("framerate", 29.0 <= fps <= 31.0, f"{fps:.2f} fps")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Unguarded ffprobe field reads abort QC before quality.json exists. Both sites trust the ffprobe payload shape. A malformed or missing field raises an unhandled exception, so the run produces no diagnostic report and the operator sees only a traceback.

  • .archon/scripts/quality-check.py#L55-L57: treat a zero denominator in r_frame_rate as an unknown framerate instead of dividing by it.
  • .archon/scripts/quality-check.py#L59-L67: fall back to the video stream duration when format.duration is absent, or raise SystemExit with a clear message.
📍 Affects 1 file
  • .archon/scripts/quality-check.py#L55-L57 (this comment)
  • .archon/scripts/quality-check.py#L59-L67
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.archon/scripts/quality-check.py around lines 55 - 57, In
.archon/scripts/quality-check.py lines 55-57, update the framerate calculation
to treat a zero denominator as an unknown framerate without dividing by zero,
while preserving the existing check behavior. In lines 59-67, use the video
stream duration when format.duration is absent; if neither duration is
available, raise SystemExit with a clear diagnostic message so quality.json is
still produced when possible.

per-project env vars, which take precedence). They are deliberately not in this
repo's `.env`, so an unconfigured run fails immediately and by name:

```

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Add language identifiers to the fenced code blocks.

markdownlint-cli2 reports MD040 for these three fences. Use text because each block shows output or a directory layout.

Proposed fix
-```
+```text
 PEXELS_API_KEY is not set (add it to /path/to/repo/.env)

- +text
read-strategy ─> gen-brief ─┬─> write-brief ──┐


-```
+```text
$STATE_DIR/content/

Also applies to: 51-51, 77-77

🧰 Tools
🪛 markdownlint-cli2 (0.23.2)

[warning] 30-30: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.archon/workflows/video/README.md at line 30, Update the three fenced code
blocks in the README to specify the text language identifier, including the
blocks containing the error output, workflow directory layout, and
$STATE_DIR/content/ path. Use text for each fence while preserving their
contents.

Source: Linters/SAST tools

# Assign bare: the engine already shell-quotes an output ref, so adding
# our own quotes around it puts literal quote characters in the value.
t=$pick-video.output
kind="$(python3 -c "import json,sys;print(json.load(open('$t/meta.json'))['kind'])" 2>/dev/null || echo unknown)"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Do not interpolate the path into the Python source string.

$t derives from $ARGUMENTS through pick-video. Line 53 embeds it inside a single-quoted Python literal. A run id that contains a single quote terminates the literal and the remaining text is executed as Python. Pass the path through the environment instead. This also removes the quoting hazard entirely.

🔒️ Proposed fix
-      kind="$(python3 -c "import json,sys;print(json.load(open('$t/meta.json'))['kind'])" 2>/dev/null || echo unknown)"
+      kind="$(META="$t/meta.json" python3 -c "import json,os;print(json.load(open(os.environ['META']))['kind'])" 2>/dev/null || echo unknown)"
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
kind="$(python3 -c "import json,sys;print(json.load(open('$t/meta.json'))['kind'])" 2>/dev/null || echo unknown)"
kind="$(META="$t/meta.json" python3 -c "import json,os;print(json.load(open(os.environ['META']))['kind'])" 2>/dev/null || echo unknown)"
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.archon/workflows/video/video-quality-review.yaml at line 53, Update the
kind extraction command in the pick-video workflow to pass the metadata path
through an environment variable rather than interpolating $t inside Python
source. Have the Python code read that variable before loading meta.json, while
preserving the existing unknown fallback for failures.

Comment on lines +166 to +180
approval:
message: |
Quality review of $pick-video.output

VERDICT: $review.output.verdict

Writing to: $review.output.target playbook

Proposed single change:
$review.output.proposed_change

Why: $review.output.rationale
Watch next: $review.output.what_to_watch_next

Approve to write this into the house playbook; reject to discard it.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

The approval gate does not show what will be written.

apply-strategy overwrites the playbook with new_strategy_markdown (Line 211). The approval message does not include that field. It also shows target as the literal kind or common, not the resolved filename such as strategy-social-short.md. The human therefore approves a summary of the change, not the content and not the destination file. A reviewer that returns proposed_change: "none" but a rewritten document passes this gate unnoticed.

Include the resolved playbook name and the new content, or at least a diff-style preview, in the message.

🔒️ Proposed change to the gate message
         Writing to: $review.output.target playbook
 
         Proposed single change:
         $review.output.proposed_change
 
         Why: $review.output.rationale
         Watch next: $review.output.what_to_watch_next
 
+        New playbook content to be written:
+        ---
+        $review.output.new_strategy_markdown
+        ---
+
         Approve to write this into the house playbook; reject to discard it.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.archon/workflows/video/video-quality-review.yaml around lines 166 - 180,
Update the approval message in the approval gate to display the resolved
playbook filename, not the raw target kind, and include the
new_strategy_markdown content or a diff-style preview that apply-strategy will
write. Ensure the gate exposes the actual destination and full pending document
so approval does not rely solely on proposed_change or its summary.

Comment on lines +199 to +202
kind = json.loads(meta_path.read_text())["kind"] if meta_path.exists() else "unknown"
# Write back to exactly one playbook — the one the reviewer targeted.
target = review.get("target", "kind")
strategy = state / ("strategy-common.md" if target == "common" else f"strategy-{kind}.md")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

An unresolved kind writes an orphan playbook.

If meta.json is missing, Line 199 sets kind to "unknown". Line 202 then resolves the destination to strategy-unknown.md. No producer reads that file: social-short.yaml reads strategy-social-short.md, and the other producers follow the same pattern. The approved lesson is written to disk and never applied. The run reports success.

The same fallback exists in gather at Line 53, so the reviewer also judges against the wrong playbook.

Fail the node when kind cannot be resolved and target is kind.

🐛 Proposed fix
       target = review.get("target", "kind")
+      if target != "common" and kind == "unknown":
+          raise SystemExit(
+              f"cannot resolve video kind from {meta_path}; refusing to write strategy-unknown.md"
+          )
       strategy = state / ("strategy-common.md" if target == "common" else f"strategy-{kind}.md")
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
kind = json.loads(meta_path.read_text())["kind"] if meta_path.exists() else "unknown"
# Write back to exactly one playbook — the one the reviewer targeted.
target = review.get("target", "kind")
strategy = state / ("strategy-common.md" if target == "common" else f"strategy-{kind}.md")
kind = json.loads(meta_path.read_text())["kind"] if meta_path.exists() else "unknown"
# Write back to exactly one playbook — the one the reviewer targeted.
target = review.get("target", "kind")
if target != "common" and kind == "unknown":
raise SystemExit(
f"cannot resolve video kind from {meta_path}; refusing to write strategy-unknown.md"
)
strategy = state / ("strategy-common.md" if target == "common" else f"strategy-{kind}.md")
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.archon/workflows/video/video-quality-review.yaml around lines 199 - 202,
Update the kind resolution and validation in the review workflow, including the
gather logic, so a missing or unresolved meta.json kind does not default to
"unknown" when review.target is "kind". Fail the node before reading or writing
a playbook in that case, while preserving the common-target path and existing
behavior for valid kinds.

Comment on lines +211 to +223
strategy.write_text(review["new_strategy_markdown"].rstrip() + "\n")
log = state / "reviews.jsonl"
with log.open("a") as fh:
fh.write(json.dumps({
"reviewed_at": datetime.datetime.now().astimezone().isoformat(timespec="seconds"),
"review_run_id": os.environ["RUN_ID"],
"video_run_id": pathlib.Path(os.environ["VIDEO_DIR"]).name,
"kind": kind,
"playbook": strategy.name,
"verdict": review["verdict"],
"change": review["proposed_change"],
"watch": review["what_to_watch_next"],
}) + "\n")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

A failure between the playbook write and the ledger append allows a double review.

Line 211 overwrites the playbook. Lines 212-223 then append to reviews.jsonl. The guard at Lines 38-43 uses reviews.jsonl to refuse a second review of the same video. If the process stops after Line 211 and before the append completes, the playbook already carries the change but the video is not recorded as reviewed. The next run picks the same video and stacks a second change on top. This is the exact outcome the guard exists to prevent.

write_text is also non-atomic. An interrupted write truncates the playbook. The store node in video-render-block.yaml stages into a .partial directory for this reason.

Write the playbook atomically through a temporary file and rename, and append the ledger row before the playbook write so a crash produces a recorded-but-unapplied review rather than an applied-but-unrecorded one.

🛡️ Proposed fix
-      strategy.write_text(review["new_strategy_markdown"].rstrip() + "\n")
       log = state / "reviews.jsonl"
       with log.open("a") as fh:
           fh.write(json.dumps({
               ...
           }) + "\n")
+          fh.flush()
+          os.fsync(fh.fileno())
+      tmp = strategy.with_suffix(strategy.suffix + ".partial")
+      tmp.write_text(review["new_strategy_markdown"].rstrip() + "\n")
+      tmp.replace(strategy)

Keep the archive copy at Lines 203-210 before the replace.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
strategy.write_text(review["new_strategy_markdown"].rstrip() + "\n")
log = state / "reviews.jsonl"
with log.open("a") as fh:
fh.write(json.dumps({
"reviewed_at": datetime.datetime.now().astimezone().isoformat(timespec="seconds"),
"review_run_id": os.environ["RUN_ID"],
"video_run_id": pathlib.Path(os.environ["VIDEO_DIR"]).name,
"kind": kind,
"playbook": strategy.name,
"verdict": review["verdict"],
"change": review["proposed_change"],
"watch": review["what_to_watch_next"],
}) + "\n")
log = state / "reviews.jsonl"
with log.open("a") as fh:
fh.write(json.dumps({
"reviewed_at": datetime.datetime.now().astimezone().isoformat(timespec="seconds"),
"review_run_id": os.environ["RUN_ID"],
"video_run_id": pathlib.Path(os.environ["VIDEO_DIR"]).name,
"kind": kind,
"playbook": strategy.name,
"verdict": review["verdict"],
"change": review["proposed_change"],
"watch": review["what_to_watch_next"],
}) + "\n")
fh.flush()
os.fsync(fh.fileno())
tmp = strategy.with_suffix(strategy.suffix + ".partial")
tmp.write_text(review["new_strategy_markdown"].rstrip() + "\n")
tmp.replace(strategy)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.archon/workflows/video/video-quality-review.yaml around lines 211 - 223,
Update the review persistence flow around the strategy write and reviews.jsonl
append: retain the existing archive copy before replacement, append the ledger
row before changing the active playbook, and write the playbook through a
temporary file followed by an atomic rename. Ensure interrupted writes cannot
truncate the playbook and a crash leaves the review recorded but unapplied.

Comment on lines +227 to +231
if cp.exists() and isinstance(review.get("thumbnail_frame"), int):
data = json.loads(cp.read_text())
data["thumbnail_frame"] = review["thumbnail_frame"]
cp.write_text(json.dumps(data, indent=2))
print(f"thumbnail_frame={review['thumbnail_frame']} -> {cp}")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

Validate the thumbnail_frame range before writing it into copy.json.

Line 227 checks the type only. The schema at Lines 155-156 sets no bounds, and the prompt at Lines 106-107 asks for a 1-based index. The model can return 0, a negative value, or an index past the last frame. The value is then persisted into copy.json and any consumer that indexes the frame list fails or selects the wrong frame.

isinstance(x, int) also accepts True, because bool subclasses int.

Bound the value against the frames actually present.

🐛 Proposed fix
       cp = vid / "copy.json"
-      if cp.exists() and isinstance(review.get("thumbnail_frame"), int):
+      frames = sorted((vid / "frames").glob("*.jpg"))
+      idx = review.get("thumbnail_frame")
+      if cp.exists() and type(idx) is int and 1 <= idx <= len(frames):
           data = json.loads(cp.read_text())
-          data["thumbnail_frame"] = review["thumbnail_frame"]
-          cp.write_text(json.dumps(data, indent=2))
-          print(f"thumbnail_frame={review['thumbnail_frame']} -> {cp}")
+          data["thumbnail_frame"] = idx
+          cp.write_text(json.dumps(data, indent=2) + "\n")
+          print(f"thumbnail_frame={idx} -> {cp}")
+      elif idx is not None:
+          print(f"ignoring out-of-range thumbnail_frame={idx!r} ({len(frames)} frames)")
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if cp.exists() and isinstance(review.get("thumbnail_frame"), int):
data = json.loads(cp.read_text())
data["thumbnail_frame"] = review["thumbnail_frame"]
cp.write_text(json.dumps(data, indent=2))
print(f"thumbnail_frame={review['thumbnail_frame']} -> {cp}")
frames = sorted((vid / "frames").glob("*.jpg"))
idx = review.get("thumbnail_frame")
if cp.exists() and type(idx) is int and 1 <= idx <= len(frames):
data = json.loads(cp.read_text())
data["thumbnail_frame"] = idx
cp.write_text(json.dumps(data, indent=2) + "\n")
print(f"thumbnail_frame={idx} -> {cp}")
elif idx is not None:
print(f"ignoring out-of-range thumbnail_frame={idx!r} ({len(frames)} frames)")
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.archon/workflows/video/video-quality-review.yaml around lines 227 - 231,
Update the thumbnail_frame persistence block to accept only non-boolean integers
within the valid 1-based range of frames actually present, rejecting zero,
negative values, and values beyond the available frame count before writing
copy.json. Use the workflow’s existing frame collection or metadata to determine
the upper bound, and preserve the current write behavior for valid values.

@Wirasm Wirasm added the needs rework Might be a good idea, but needs rethinking before implementation label Aug 31, 2026
@Wirasm

Wirasm commented Aug 31, 2026

Copy link
Copy Markdown
Collaborator Author

not ready, needs rework

will go in as

workflows/video-pack

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

needs rework Might be a good idea, but needs rethinking before implementation

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant