@event4u/agent-config 2.23.0 → 2.24.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. package/.agent-src/commands/video/from-script.md +123 -0
  2. package/.agent-src/commands/video/scene.md +92 -0
  3. package/.agent-src/commands/video/stitch.md +83 -0
  4. package/.agent-src/commands/video/storyboard.md +95 -0
  5. package/.agent-src/commands/video.md +59 -0
  6. package/.agent-src/personas/README.md +3 -0
  7. package/.agent-src/personas/ai-video-technical-director.md +81 -0
  8. package/.agent-src/personas/hollywood-director.md +99 -0
  9. package/.agent-src/personas/pixar-storyboard-artist.md +98 -0
  10. package/.agent-src/skills/character-consistency/SKILL.md +120 -0
  11. package/.agent-src/skills/motion-choreographer/SKILL.md +149 -0
  12. package/.agent-src/skills/pixar-storyteller/SKILL.md +107 -0
  13. package/.agent-src/skills/scene-expander/SKILL.md +122 -0
  14. package/.agent-src/skills/scene-expander/scene-blueprint.schema.yaml +108 -0
  15. package/.agent-src/skills/subagent-orchestration/SKILL.md +17 -15
  16. package/.agent-src/skills/video-director/SKILL.md +113 -0
  17. package/.agent-src/templates/agent-settings.md +19 -0
  18. package/.agent-src/templates/agents/agent-project-settings.example.yml +1 -1
  19. package/.claude-plugin/marketplace.json +11 -1
  20. package/CHANGELOG.md +22 -0
  21. package/README.md +4 -4
  22. package/config/agent-settings.template.yml +28 -0
  23. package/docs/adrs/caveman/0001-default-off-until-bench.md +2 -2
  24. package/docs/adrs/cost/0001-hard-stop-hook.md +1 -1
  25. package/docs/adrs/smoke/0001-per-tier-smoke-scripts.md +2 -2
  26. package/docs/architecture.md +2 -2
  27. package/docs/catalog.md +14 -4
  28. package/docs/contracts/command-clusters.md +1 -0
  29. package/docs/contracts/compression-default-kill-criterion.md +1 -1
  30. package/docs/contracts/file-ownership-matrix.json +337 -0
  31. package/docs/getting-started.md +1 -1
  32. package/docs/parity/ruflo.md +3 -3
  33. package/package.json +1 -1
  34. package/scripts/ai-video/adapters/gemini-veo.sh +57 -0
  35. package/scripts/ai-video/adapters/higgsfield.sh +82 -0
  36. package/scripts/ai-video/adapters/kling.sh +54 -0
  37. package/scripts/ai-video/adapters/openai-images.sh +52 -0
  38. package/scripts/ai-video/adapters/sora.sh +54 -0
  39. package/scripts/ai-video/lib/adapter-common.sh +116 -0
  40. package/scripts/ai-video/lib/adapter-contract.md +163 -0
  41. package/scripts/ai-video/lib/fixtures/gemini-veo/result.json +1 -0
  42. package/scripts/ai-video/lib/fixtures/gemini-veo/scene-0001.mp4 +1 -0
  43. package/scripts/ai-video/lib/fixtures/higgsfield/result.json +1 -0
  44. package/scripts/ai-video/lib/fixtures/higgsfield/scene-0001.mp4 +1 -0
  45. package/scripts/ai-video/lib/fixtures/kling/result.json +1 -0
  46. package/scripts/ai-video/lib/fixtures/kling/scene-0001.mp4 +1 -0
  47. package/scripts/ai-video/lib/fixtures/openai-images/result.json +1 -0
  48. package/scripts/ai-video/lib/fixtures/openai-images/scene-0001.png +3 -0
  49. package/scripts/ai-video/lib/fixtures/sora/result.json +1 -0
  50. package/scripts/ai-video/lib/fixtures/sora/scene-0001.mp4 +1 -0
  51. package/scripts/ai-video/lib/load-config.sh +140 -0
  52. package/scripts/ai-video/lib/operator-pick.sh +119 -0
  53. package/scripts/ai-video/lib/parse-blueprint.sh +122 -0
  54. package/scripts/ai-video/lib/redact.sh +85 -0
  55. package/scripts/ai-video/lib/validate-deps.sh +132 -0
  56. package/scripts/ai-video/stitch.sh +154 -0
  57. package/scripts/ai-video/test-pipeline.sh +169 -0
  58. package/scripts/schemas/command.schema.json +8 -0
@@ -0,0 +1,119 @@
1
+ #!/usr/bin/env bash
2
+ # operator-pick.sh — best-of-N selection checkpoint invoked after image
3
+ # render by /video:from-script and /video:scene. Renders a thumbnail
4
+ # contact-sheet PNG of the candidates under a scene, then waits for the
5
+ # operator to write <project>/scenes/<id>/selection.json.
6
+ #
7
+ # Dry-run mode (AIV_DRYRUN=true, default) auto-picks candidate 1 and
8
+ # writes the same selection.json so smoke tests stay unattended.
9
+ #
10
+ # Usage:
11
+ # operator-pick.sh <project-dir> <scene-id>
12
+ #
13
+ # Inputs:
14
+ # <project-dir>/scenes/<scene-id>/candidates/*.png (N>=1 image files)
15
+ #
16
+ # Outputs (stdout, one path per line so callers can capture both):
17
+ # sheet=<project-dir>/scenes/<scene-id>/contact-sheet.png
18
+ # selected=<absolute path to locked image>
19
+ #
20
+ # Exit codes:
21
+ # 0 selection.json present, locked image path emitted
22
+ # 2 missing candidates directory or empty
23
+ # 3 selection.json malformed or names an unknown candidate
24
+ # 4 operator declined (selection.json absent in live mode)
25
+
26
+ set -euo pipefail
27
+
28
+ if [ "$#" -ne 2 ]; then
29
+ printf 'operator-pick: usage: %s <project-dir> <scene-id>\n' "$0" >&2
30
+ exit 2
31
+ fi
32
+
33
+ project_dir="$1"
34
+ scene_id="$2"
35
+ scene_dir="${project_dir}/scenes/${scene_id}"
36
+ cand_dir="${scene_dir}/candidates"
37
+ sheet="${scene_dir}/contact-sheet.png"
38
+ sel_file="${scene_dir}/selection.json"
39
+
40
+ if [ ! -d "${cand_dir}" ]; then
41
+ printf 'operator-pick: missing candidate dir: %s\n' "${cand_dir}" >&2
42
+ exit 2
43
+ fi
44
+
45
+ # Collect candidates in deterministic order (lexical). Portable to
46
+ # macOS bash 3.2 — no `mapfile`.
47
+ candidates=()
48
+ while IFS= read -r _p; do
49
+ candidates+=("${_p}")
50
+ done < <(find "${cand_dir}" -maxdepth 1 -type f -name '*.png' | LC_ALL=C sort)
51
+ if [ "${#candidates[@]}" -eq 0 ]; then
52
+ printf 'operator-pick: no candidate PNGs under %s\n' "${cand_dir}" >&2
53
+ exit 2
54
+ fi
55
+
56
+ # Build the contact-sheet PNG via ffmpeg. ceil(sqrt(N)) columns.
57
+ n="${#candidates[@]}"
58
+ cols=1
59
+ while [ $((cols * cols)) -lt "${n}" ]; do cols=$((cols + 1)); done
60
+
61
+ # ffmpeg's `tile` filter needs an input concat; use `-pattern_type glob`.
62
+ if command -v ffmpeg >/dev/null 2>&1; then
63
+ ffmpeg -y -loglevel error \
64
+ -pattern_type glob -i "${cand_dir}/*.png" \
65
+ -filter_complex "tile=${cols}x0:padding=8:margin=16" \
66
+ "${sheet}" || {
67
+ printf 'operator-pick: ffmpeg tile failed; falling back to first candidate as sheet\n' >&2
68
+ cp "${candidates[0]}" "${sheet}"
69
+ }
70
+ else
71
+ # ffmpeg absent (e.g. minimal CI). Copy the first candidate as the sheet.
72
+ cp "${candidates[0]}" "${sheet}"
73
+ fi
74
+
75
+ # Dry-run: auto-select candidate 1 and write selection.json verbatim so
76
+ # downstream resume reads it identically to a real operator pick.
77
+ if [ "${AIV_DRYRUN:-true}" = "true" ]; then
78
+ first="$(basename "${candidates[0]}")"
79
+ cat > "${sel_file}" <<JSON
80
+ {
81
+ "selected": "${first}",
82
+ "reason": "auto-selected by operator-pick.sh (AIV_DRYRUN=true)"
83
+ }
84
+ JSON
85
+ fi
86
+
87
+ # Wait-loop is not appropriate here — the caller (command file) decides
88
+ # whether to poll or hand back. We assert selection.json exists and
89
+ # resolve it; if missing in live mode, we exit non-zero so the caller
90
+ # can pause and re-invoke.
91
+ if [ ! -f "${sel_file}" ]; then
92
+ printf 'operator-pick: selection.json absent; write %s with {"selected":"<filename>"} and re-run\n' "${sel_file}" >&2
93
+ exit 4
94
+ fi
95
+
96
+ # Read selection.json (jq required — already a hard dep for adapters).
97
+ if ! command -v jq >/dev/null 2>&1; then
98
+ printf 'operator-pick: jq is required\n' >&2
99
+ exit 3
100
+ fi
101
+ selected_name="$(jq -r '.selected // empty' "${sel_file}")"
102
+ if [ -z "${selected_name}" ]; then
103
+ printf 'operator-pick: selection.json missing "selected" field\n' >&2
104
+ exit 3
105
+ fi
106
+
107
+ selected_path="${cand_dir}/${selected_name}"
108
+ if [ ! -f "${selected_path}" ]; then
109
+ printf 'operator-pick: selected candidate not found: %s\n' "${selected_path}" >&2
110
+ exit 3
111
+ fi
112
+
113
+ # Also write a stable locked.png symlink/copy at the scene root so the
114
+ # motion step can resolve the locked image without re-reading selection.json.
115
+ locked="${scene_dir}/locked.png"
116
+ cp -f "${selected_path}" "${locked}"
117
+
118
+ printf 'sheet=%s\n' "${sheet}"
119
+ printf 'selected=%s\n' "${locked}"
@@ -0,0 +1,122 @@
1
+ #!/usr/bin/env bash
2
+ # parse-blueprint.sh — read a 12-block Cinematic Scene Blueprint
3
+ # from stdin (or a file arg) and emit adapter-contract JSON on
4
+ # stdout. Pure POSIX-compatible bash (no associative arrays, runs
5
+ # on macOS bash 3.2); jq required for JSON safety.
6
+ #
7
+ # Schema: .agent-src.uncompressed/skills/scene-expander/scene-blueprint.schema.yaml
8
+ # Contract: scripts/ai-video/lib/adapter-contract.md
9
+ #
10
+ # Usage:
11
+ # parse-blueprint.sh < prompt.txt > blueprint.json
12
+ # parse-blueprint.sh prompt.txt > blueprint.json
13
+ #
14
+ # Exit codes:
15
+ # 0 valid blueprint, JSON emitted on stdout
16
+ # 2 missing required block — name on stderr
17
+ # 3 parse error (unknown block, malformed DURATION, etc.)
18
+
19
+ set -euo pipefail
20
+
21
+ command -v jq >/dev/null 2>&1 || {
22
+ echo "parse-blueprint: jq required" >&2
23
+ exit 3
24
+ }
25
+
26
+ input="${1:-/dev/stdin}"
27
+ [ -r "$input" ] || { echo "parse-blueprint: cannot read $input" >&2; exit 3; }
28
+
29
+ V_STYLE=""; V_SUBJECT=""; V_ENVIRONMENT=""; V_ACTION=""
30
+ V_CAMERA=""; V_LENS=""; V_LIGHTING=""; V_MOOD=""
31
+ V_DIALOGUE=""; V_AMBIENT_SOUND=""; V_DURATION=""; V_NEGATIVE=""
32
+
33
+ append_to() {
34
+ local name="$1"; local line="$2"
35
+ local cur; eval "cur=\${$name}"
36
+ if [ -z "$cur" ]; then
37
+ eval "$name=\$line"
38
+ else
39
+ eval "$name=\"\${$name}
40
+ \$line\""
41
+ fi
42
+ }
43
+
44
+ set_var() {
45
+ eval "$1=\$2"
46
+ }
47
+
48
+ current=""
49
+ while IFS= read -r line || [ -n "$line" ]; do
50
+ stripped="$(printf '%s' "$line" | sed -E 's/^[[:space:]]*//; s/^\*\*//; s/\*\*[[:space:]]*$//')"
51
+ label="$(printf '%s' "$stripped" | sed -nE 's/^([A-Z][A-Z ]+)[[:space:]]*:.*$/\1/p')"
52
+ if [ -n "$label" ]; then
53
+ key="$(printf '%s' "$label" | tr ' ' '_' | tr '[:lower:]' '[:upper:]')"
54
+ case "$key" in
55
+ STYLE|SUBJECT|ENVIRONMENT|ACTION|CAMERA|LENS|LIGHTING|MOOD|DIALOGUE|AMBIENT_SOUND|DURATION|NEGATIVE)
56
+ current="V_$key"
57
+ rest="$(printf '%s' "$stripped" | sed -E "s/^${label}[[:space:]]*:[[:space:]]*//")"
58
+ set_var "$current" "$rest"
59
+ continue
60
+ ;;
61
+ esac
62
+ fi
63
+ if [ -n "$current" ] && [ -n "$line" ]; then
64
+ append_to "$current" "$line"
65
+ fi
66
+ done < "$input"
67
+
68
+ for short in STYLE SUBJECT ENVIRONMENT ACTION CAMERA LENS LIGHTING MOOD DURATION NEGATIVE; do
69
+ eval "v=\$V_$short"
70
+ if [ -z "$v" ]; then
71
+ echo "parse-blueprint: missing required block: $short" >&2
72
+ exit 2
73
+ fi
74
+ done
75
+
76
+ if ! printf '%s' "$V_DURATION" | grep -Eq '^[0-9]+(\.[0-9])?$'; then
77
+ echo "parse-blueprint: DURATION not numeric: $V_DURATION" >&2
78
+ exit 3
79
+ fi
80
+
81
+ negative_json="$(printf '%s' "$V_NEGATIVE" | tr ',\n' '\n\n' | sed '/^[[:space:]]*$/d' | sed -E 's/^[[:space:]]*//; s/[[:space:]]*$//' | jq -R . | jq -s .)"
82
+
83
+ to_array_or_null() {
84
+ if [ -z "$1" ]; then
85
+ printf '%s' "null"
86
+ else
87
+ printf '%s\n' "$1" | sed '/^[[:space:]]*$/d' | jq -R . | jq -s .
88
+ fi
89
+ }
90
+ dialogue_json="$(to_array_or_null "$V_DIALOGUE")"
91
+ ambient_json="$(to_array_or_null "$V_AMBIENT_SOUND")"
92
+
93
+ audio_native=false
94
+ if [ "$dialogue_json" != "null" ] || [ "$ambient_json" != "null" ]; then
95
+ audio_native=true
96
+ fi
97
+
98
+ jq -n \
99
+ --arg style "$V_STYLE" \
100
+ --arg subject "$V_SUBJECT" \
101
+ --arg environment "$V_ENVIRONMENT" \
102
+ --arg action "$V_ACTION" \
103
+ --arg camera "$V_CAMERA" \
104
+ --arg lens "$V_LENS" \
105
+ --arg lighting "$V_LIGHTING" \
106
+ --arg mood "$V_MOOD" \
107
+ --argjson dialogue "$dialogue_json" \
108
+ --argjson ambient "$ambient_json" \
109
+ --argjson duration "$V_DURATION" \
110
+ --argjson negative "$negative_json" \
111
+ --argjson native "$audio_native" \
112
+ '{
113
+ prompt: {
114
+ style: $style, subject: $subject, environment: $environment,
115
+ action: $action, camera: $camera, lens: $lens,
116
+ lighting: $lighting, mood: $mood
117
+ },
118
+ audio: { dialogue: $dialogue, ambient: $ambient, enable_native_audio: $native },
119
+ duration: $duration,
120
+ negative: $negative,
121
+ requires: { audio_native: $native }
122
+ }'
@@ -0,0 +1,85 @@
1
+ #!/usr/bin/env bash
2
+ # scripts/ai-video/lib/redact.sh — secret-scrubbing helper for /video:* adapters.
3
+ #
4
+ # Sourced by every adapter under scripts/ai-video/adapters/*.sh. Provides
5
+ # two helpers:
6
+ #
7
+ # aiv_redact_register <secret> — register a string to scrub (idempotent)
8
+ # aiv_redact <text> — print text with every registered
9
+ # secret replaced by ***REDACTED***
10
+ #
11
+ # Iron Law: an API key must never reach stdout/stderr verbatim. Adapters
12
+ # pipe every network response, curl error, and trace through aiv_redact
13
+ # before printing. Empty / unset values are skipped silently — an unset
14
+ # secret cannot leak.
15
+ #
16
+ # Pure bash, no external dependencies. Safe under `set -euo pipefail`.
17
+
18
+ # Guard against double-source.
19
+ if [ -n "${AIV_REDACT_LOADED:-}" ]; then
20
+ return 0 2>/dev/null || exit 0
21
+ fi
22
+ AIV_REDACT_LOADED=1
23
+
24
+ # Registry of secrets to scrub. Newline-separated; populated by
25
+ # aiv_redact_register. Never echoed.
26
+ AIV_REDACT_SECRETS=""
27
+
28
+ aiv_redact_register() {
29
+ local secret="${1:-}"
30
+ # Treat empty / placeholder values as no-op so adapters can call this
31
+ # unconditionally without leaking the placeholder string.
32
+ case "${secret}" in
33
+ ""|"REPLACE-ME"|*"-REPLACE-ME") return 0 ;;
34
+ esac
35
+ # Require a minimum length so single characters do not nuke the log.
36
+ if [ "${#secret}" -lt 8 ]; then
37
+ return 0
38
+ fi
39
+ # Idempotent — skip if already registered.
40
+ case "${AIV_REDACT_SECRETS}" in
41
+ *"${secret}"*) return 0 ;;
42
+ esac
43
+ AIV_REDACT_SECRETS="${AIV_REDACT_SECRETS}${secret}
44
+ "
45
+ }
46
+
47
+ aiv_redact() {
48
+ local input
49
+ if [ "$#" -gt 0 ]; then
50
+ input="$*"
51
+ else
52
+ input="$(cat)"
53
+ fi
54
+ if [ -z "${AIV_REDACT_SECRETS}" ]; then
55
+ printf '%s\n' "${input}"
56
+ return 0
57
+ fi
58
+ # Apply replacements one secret at a time using awk so special chars
59
+ # in the secret cannot break a sed expression. Use a here-string to
60
+ # avoid running the loop in a subshell (which would discard mutations
61
+ # to ${input}).
62
+ local secret
63
+ while IFS= read -r secret; do
64
+ [ -z "${secret}" ] && continue
65
+ input="$(printf '%s' "${input}" | awk -v s="${secret}" '
66
+ {
67
+ out = ""
68
+ rest = $0
69
+ while ((i = index(rest, s)) > 0) {
70
+ out = out substr(rest, 1, i - 1) "***REDACTED***"
71
+ rest = substr(rest, i + length(s))
72
+ }
73
+ print out rest
74
+ }')"
75
+ done <<< "${AIV_REDACT_SECRETS}"
76
+ printf '%s\n' "${input}"
77
+ }
78
+
79
+ # Convenience wrapper: pipe stdin through aiv_redact line by line so
80
+ # adapters can do `curl … 2>&1 | aiv_redact_stream`.
81
+ aiv_redact_stream() {
82
+ while IFS= read -r line; do
83
+ aiv_redact "${line}"
84
+ done
85
+ }
@@ -0,0 +1,132 @@
1
+ #!/usr/bin/env bash
2
+ # validate-deps.sh — startup validator invoked by every /video:* command
3
+ # BEFORE any adapter or network call. Reads the YAML frontmatter from
4
+ # the command file, resolves every declared persona + skill against
5
+ # .agent-src/personas/ and .agent-src/skills/, and fails fast with the
6
+ # missing-id list.
7
+ #
8
+ # Scope (per roadmap Phase 5 Step 5): existence + frontmatter `id` match
9
+ # only. No version pinning (scope-control § rules); no schema validation
10
+ # of persona / skill bodies — that lives in `task lint-skills`.
11
+ #
12
+ # Usage:
13
+ # validate-deps.sh <path-to-command.md>
14
+ #
15
+ # Exit codes:
16
+ # 0 all declared personas + skills resolve
17
+ # 2 command file missing or no frontmatter
18
+ # 3 one or more declared ids do not resolve (list on stderr)
19
+
20
+ set -euo pipefail
21
+
22
+ if [ "$#" -ne 1 ]; then
23
+ printf 'validate-deps: usage: %s <path-to-command.md>\n' "$0" >&2
24
+ exit 2
25
+ fi
26
+
27
+ cmd_file="$1"
28
+ if [ ! -f "${cmd_file}" ]; then
29
+ printf 'validate-deps: command file not found: %s\n' "${cmd_file}" >&2
30
+ exit 2
31
+ fi
32
+
33
+ # Resolve repo root from this script's location (scripts/ai-video/lib/).
34
+ script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
35
+ repo_root="$(cd "${script_dir}/../../.." && pwd)"
36
+
37
+ # AIV_PERSONAS_DIR / AIV_SKILLS_DIR allow overrides for tests; defaults
38
+ # point at the generated mirrors (skills+personas live there at runtime).
39
+ personas_dir="${AIV_PERSONAS_DIR:-${repo_root}/.agent-src/personas}"
40
+ skills_dir="${AIV_SKILLS_DIR:-${repo_root}/.agent-src/skills}"
41
+
42
+ # Extract the frontmatter block (between the first two `---` lines).
43
+ fm="$(awk '
44
+ BEGIN { in_fm=0; count=0 }
45
+ /^---[[:space:]]*$/ { count++; if (count==1) { in_fm=1; next } if (count==2) { exit } }
46
+ in_fm { print }
47
+ ' "${cmd_file}")"
48
+
49
+ if [ -z "${fm}" ]; then
50
+ printf 'validate-deps: no YAML frontmatter in %s\n' "${cmd_file}" >&2
51
+ exit 2
52
+ fi
53
+
54
+ # Parse `personas:` and `skills:` lines. We accept inline-list form only
55
+ # (the template we ship), e.g. `personas: [a, b]`. No multi-line block
56
+ # form — keeps the validator dependency-free (no yq / python).
57
+ _extract_list() {
58
+ # $1 = key name (personas|skills)
59
+ printf '%s\n' "${fm}" \
60
+ | awk -v key="$1" '
61
+ $0 ~ "^" key ":" {
62
+ line=$0
63
+ sub("^" key ":[[:space:]]*", "", line)
64
+ sub("^\\[", "", line)
65
+ sub("\\][[:space:]]*$", "", line)
66
+ gsub(",", " ", line)
67
+ print line
68
+ }' \
69
+ | tr -s ' ' '\n' \
70
+ | sed 's/^[[:space:]]*//; s/[[:space:]]*$//' \
71
+ | sed '/^$/d'
72
+ }
73
+
74
+ # Portable to macOS bash 3.2 — no `mapfile`.
75
+ personas=()
76
+ while IFS= read -r _id; do
77
+ personas+=("${_id}")
78
+ done < <(_extract_list personas)
79
+ skills=()
80
+ while IFS= read -r _id; do
81
+ skills+=("${_id}")
82
+ done < <(_extract_list skills)
83
+
84
+ missing=()
85
+
86
+ # bash 3.2 treats `"${arr[@]}"` on an empty array as unbound under
87
+ # `set -u`. Guard each loop with the length check.
88
+
89
+ # A persona is "present" if .agent-src/personas/<id>.md exists OR
90
+ # .agent-src/personas/<id>/persona.md exists (template-specialist shape).
91
+ if [ "${#personas[@]}" -gt 0 ]; then
92
+ for p in "${personas[@]}"; do
93
+ [ -z "${p}" ] && continue
94
+ if [ ! -f "${personas_dir}/${p}.md" ] && [ ! -f "${personas_dir}/${p}/persona.md" ]; then
95
+ missing+=("persona:${p}")
96
+ continue
97
+ fi
98
+ # Frontmatter id match: read `id:` (optional) or `name:` from the
99
+ # persona file; if present it must equal the declared slug.
100
+ src_file="${personas_dir}/${p}.md"
101
+ [ -f "${src_file}" ] || src_file="${personas_dir}/${p}/persona.md"
102
+ declared_id="$(awk '/^id:[[:space:]]*/ { sub("^id:[[:space:]]*", ""); print; exit } /^name:[[:space:]]*/ { sub("^name:[[:space:]]*", ""); print; exit }' "${src_file}" | tr -d '"' | tr -d "'")"
103
+ if [ -n "${declared_id}" ] && [ "${declared_id}" != "${p}" ]; then
104
+ missing+=("persona:${p} (file declares id=${declared_id})")
105
+ fi
106
+ done
107
+ fi
108
+
109
+ # A skill is "present" if .agent-src/skills/<id>/SKILL.md exists.
110
+ if [ "${#skills[@]}" -gt 0 ]; then
111
+ for s in "${skills[@]}"; do
112
+ [ -z "${s}" ] && continue
113
+ skill_file="${skills_dir}/${s}/SKILL.md"
114
+ if [ ! -f "${skill_file}" ]; then
115
+ missing+=("skill:${s}")
116
+ continue
117
+ fi
118
+ declared_id="$(awk '/^name:[[:space:]]*/ { sub("^name:[[:space:]]*", ""); print; exit }' "${skill_file}" | tr -d '"' | tr -d "'")"
119
+ if [ -n "${declared_id}" ] && [ "${declared_id}" != "${s}" ]; then
120
+ missing+=("skill:${s} (file declares name=${declared_id})")
121
+ fi
122
+ done
123
+ fi
124
+
125
+ if [ "${#missing[@]}" -gt 0 ]; then
126
+ printf 'validate-deps: unresolved declarations in %s:\n' "${cmd_file}" >&2
127
+ printf ' - %s\n' "${missing[@]}" >&2
128
+ exit 3
129
+ fi
130
+
131
+ # Silent success — callers can `set -e` and continue.
132
+ exit 0
@@ -0,0 +1,154 @@
1
+ #!/usr/bin/env bash
2
+ # stitch.sh — ffmpeg-based clip concatenator for /video:* pipelines.
3
+ #
4
+ # Drives the cut from <project>/manifest.json (an ordered array of
5
+ # `{scene_id, clip_path, audio_embedded, audio_path?, duration}`).
6
+ # Pass-through for audio_embedded=true; ffmpeg mux for audio_embedded
7
+ # =false; operator-supplied bed for video-only clips.
8
+ #
9
+ # Usage:
10
+ # stitch.sh <manifest.json> <output.mp4>
11
+ # [--skip-scene <id> ...]
12
+ # [--abort-on-missing | --continue]
13
+ # [--crossfade <seconds>]
14
+ #
15
+ # Failure semantics:
16
+ # - Missing clip → fail loud with scene_id and re-render hint
17
+ # unless --skip-scene <id> drops the clip from the cut or
18
+ # --continue is passed (the dropped scenes are written to stderr).
19
+ # - --abort-on-missing is the default.
20
+ # - Adapter-failure rollback contract: a failed adapter writes
21
+ # <project>/scenes/<id>/error.json; stitch.sh checks for that
22
+ # file alongside the clip and surfaces it instead of an opaque
23
+ # ffmpeg error.
24
+
25
+ set -euo pipefail
26
+
27
+ # shellcheck source=lib/adapter-common.sh
28
+ . "$(dirname "$0")/lib/adapter-common.sh"
29
+
30
+ aiv_require_cmd jq
31
+
32
+ DRYRUN="${AIV_DRYRUN:-true}"
33
+ case "${DRYRUN}" in
34
+ false|FALSE|0|no|NO) DRYRUN_FLAG=0 ;;
35
+ *) DRYRUN_FLAG=1 ;;
36
+ esac
37
+ [ "${DRYRUN_FLAG}" -eq 0 ] && aiv_require_cmd ffmpeg
38
+
39
+ MANIFEST="${1:-}"
40
+ OUTPUT="${2:-}"
41
+ [ -n "${MANIFEST}" ] && [ -n "${OUTPUT}" ] \
42
+ || aiv_die 2 "usage: stitch.sh <manifest.json> <output.mp4> [--skip-scene <id>] [--abort-on-missing|--continue] [--crossfade <s>]"
43
+ [ -r "${MANIFEST}" ] || aiv_die 2 "manifest not readable: ${MANIFEST}"
44
+ shift 2 || true
45
+
46
+ SKIP_IDS=""
47
+ MISSING_POLICY="abort"
48
+ CROSSFADE=""
49
+ while [ "$#" -gt 0 ]; do
50
+ case "$1" in
51
+ --skip-scene) SKIP_IDS="${SKIP_IDS} ${2:-}"; shift 2 ;;
52
+ --abort-on-missing) MISSING_POLICY="abort"; shift ;;
53
+ --continue) MISSING_POLICY="continue"; shift ;;
54
+ --crossfade) CROSSFADE="${2:-}"; shift 2 ;;
55
+ *) aiv_die 2 "stitch.sh: unknown flag '$1'" ;;
56
+ esac
57
+ done
58
+
59
+ skip_match() {
60
+ local id="$1" s
61
+ for s in ${SKIP_IDS}; do
62
+ [ "${s}" = "${id}" ] && return 0
63
+ done
64
+ return 1
65
+ }
66
+
67
+ PROJECT_DIR="$(cd "$(dirname "${MANIFEST}")" && pwd)"
68
+ WORK_DIR="$(mktemp -d -t aiv-stitch-XXXXXX)"
69
+ trap 'rm -rf "${WORK_DIR}"' EXIT
70
+ CONCAT_LIST="${WORK_DIR}/concat.txt"
71
+ : > "${CONCAT_LIST}"
72
+
73
+ # Accept both bare-array manifests and the documented object shape
74
+ # (`{ "scenes": [...] }`). Field names tolerate the legacy
75
+ # `scene_id` / `clip_path` keys and the example `id` / `expected_clip`.
76
+ SCENES_JQ='if type == "array" then .[] else .scenes[] end'
77
+
78
+ count=0
79
+ missing=0
80
+ plan=""
81
+ while IFS= read -r row; do
82
+ [ -n "${row}" ] || continue
83
+ scene_id="$(printf '%s' "${row}" | jq -r '.scene_id // .id')"
84
+ clip_path="$(printf '%s' "${row}" | jq -r '.clip_path // .expected_clip')"
85
+ audio_embedded="$(printf '%s' "${row}" | jq -r '.audio_embedded // false')"
86
+ audio_path="$(printf '%s' "${row}" | jq -r '.audio_path // empty')"
87
+
88
+ case "${clip_path}" in
89
+ /*) ;;
90
+ *) clip_path="${PROJECT_DIR}/${clip_path}" ;;
91
+ esac
92
+
93
+ if skip_match "${scene_id}"; then
94
+ printf 'stitch: skip scene=%s (operator --skip-scene)\n' "${scene_id}" >&2
95
+ continue
96
+ fi
97
+
98
+ if [ "${DRYRUN_FLAG}" -eq 1 ]; then
99
+ plan="${plan}${scene_id}\taudio_embedded=${audio_embedded}\t${clip_path}\n"
100
+ count=$((count + 1))
101
+ continue
102
+ fi
103
+
104
+ if [ ! -f "${clip_path}" ]; then
105
+ err_json="${PROJECT_DIR}/scenes/${scene_id}/error.json"
106
+ if [ -f "${err_json}" ]; then
107
+ printf 'stitch: scene=%s adapter-error: ' "${scene_id}" >&2
108
+ jq -r '"adapter=\(.adapter) exit=\(.exit_code) user_action=\(.user_action)"' "${err_json}" >&2
109
+ fi
110
+ if [ "${MISSING_POLICY}" = "continue" ]; then
111
+ printf 'stitch: continue past missing scene=%s clip=%s (re-render or --skip-scene next time)\n' \
112
+ "${scene_id}" "${clip_path}" >&2
113
+ missing=$((missing + 1))
114
+ continue
115
+ fi
116
+ aiv_die 7 "missing clip for scene=${scene_id} at ${clip_path} (re-render the scene, pass --skip-scene ${scene_id}, or use --continue)"
117
+ fi
118
+
119
+ # For clips without embedded audio that ship a sibling track, mux
120
+ # via ffmpeg into a tmp file before concat. Pure pass-through for
121
+ # the embedded-audio case.
122
+ if [ "${audio_embedded}" = "false" ] && [ -n "${audio_path}" ] && [ -f "${audio_path}" ]; then
123
+ muxed="${WORK_DIR}/${scene_id}.mp4"
124
+ ffmpeg -loglevel error -y -i "${clip_path}" -i "${audio_path}" \
125
+ -c:v copy -c:a aac -shortest "${muxed}" >/dev/null \
126
+ || aiv_die 8 "ffmpeg mux failed for scene=${scene_id}"
127
+ printf "file '%s'\n" "${muxed}" >> "${CONCAT_LIST}"
128
+ else
129
+ printf "file '%s'\n" "${clip_path}" >> "${CONCAT_LIST}"
130
+ fi
131
+ count=$((count + 1))
132
+ done <<EOF
133
+ $(jq -c "${SCENES_JQ}" "${MANIFEST}")
134
+ EOF
135
+
136
+ [ "${count}" -gt 0 ] || aiv_die 7 "stitch: no usable clips after manifest scan (missing=${missing}, skipped via --skip-scene)"
137
+
138
+ if [ "${DRYRUN_FLAG}" -eq 1 ]; then
139
+ printf 'stitch: dry-run plan (output=%s, scenes=%d):\n' "${OUTPUT}" "${count}" >&2
140
+ printf '%b' "${plan}" >&2
141
+ printf '{"output":"%s","scenes":%d,"missing":0,"dry_run":true}\n' "${OUTPUT}" "${count}"
142
+ exit 0
143
+ fi
144
+
145
+ if [ -n "${CROSSFADE}" ]; then
146
+ printf 'stitch: crossfade=%ss requested — passing through ffmpeg xfade filter not yet implemented (concat path used)\n' \
147
+ "${CROSSFADE}" >&2
148
+ fi
149
+
150
+ ffmpeg -loglevel error -y -f concat -safe 0 -i "${CONCAT_LIST}" \
151
+ -c copy "${OUTPUT}" >/dev/null \
152
+ || aiv_die 8 "ffmpeg concat failed (manifest=${MANIFEST}, output=${OUTPUT})"
153
+
154
+ printf '{"output":"%s","scenes":%d,"missing":%d}\n' "${OUTPUT}" "${count}" "${missing}"