@event4u/agent-config 2.21.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 (67) 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/adversarial-review/SKILL.md +2 -1
  11. package/.agent-src/skills/canvas-design/SKILL.md +11 -6
  12. package/.agent-src/skills/character-consistency/SKILL.md +120 -0
  13. package/.agent-src/skills/fe-design/SKILL.md +8 -0
  14. package/.agent-src/skills/motion-choreographer/SKILL.md +149 -0
  15. package/.agent-src/skills/pixar-storyteller/SKILL.md +107 -0
  16. package/.agent-src/skills/prompt-optimizer/SKILL.md +29 -5
  17. package/.agent-src/skills/react-shadcn-ui/SKILL.md +9 -0
  18. package/.agent-src/skills/refine-prompt/SKILL.md +57 -0
  19. package/.agent-src/skills/scene-expander/SKILL.md +122 -0
  20. package/.agent-src/skills/scene-expander/scene-blueprint.schema.yaml +108 -0
  21. package/.agent-src/skills/subagent-orchestration/SKILL.md +17 -15
  22. package/.agent-src/skills/tailwind-engineer/SKILL.md +14 -0
  23. package/.agent-src/skills/video-director/SKILL.md +113 -0
  24. package/.agent-src/templates/agent-settings.md +19 -0
  25. package/.agent-src/templates/agents/agent-project-settings.example.yml +53 -1
  26. package/.claude-plugin/marketplace.json +11 -1
  27. package/CHANGELOG.md +88 -138
  28. package/README.md +4 -4
  29. package/config/agent-settings.template.yml +28 -0
  30. package/docs/adrs/caveman/0001-default-off-until-bench.md +2 -2
  31. package/docs/adrs/cost/0001-hard-stop-hook.md +1 -1
  32. package/docs/adrs/smoke/0001-per-tier-smoke-scripts.md +2 -2
  33. package/docs/architecture.md +3 -3
  34. package/docs/archive/CHANGELOG-pre-2.20.0.md +159 -0
  35. package/docs/catalog.md +16 -5
  36. package/docs/contracts/command-clusters.md +1 -0
  37. package/docs/contracts/compression-default-kill-criterion.md +1 -1
  38. package/docs/contracts/file-ownership-matrix.json +344 -0
  39. package/docs/getting-started.md +1 -1
  40. package/docs/guidelines/prompt-templates.md +166 -0
  41. package/docs/parity/ruflo.md +3 -3
  42. package/package.json +1 -1
  43. package/scripts/ai-video/adapters/gemini-veo.sh +57 -0
  44. package/scripts/ai-video/adapters/higgsfield.sh +82 -0
  45. package/scripts/ai-video/adapters/kling.sh +54 -0
  46. package/scripts/ai-video/adapters/openai-images.sh +52 -0
  47. package/scripts/ai-video/adapters/sora.sh +54 -0
  48. package/scripts/ai-video/lib/adapter-common.sh +116 -0
  49. package/scripts/ai-video/lib/adapter-contract.md +163 -0
  50. package/scripts/ai-video/lib/fixtures/gemini-veo/result.json +1 -0
  51. package/scripts/ai-video/lib/fixtures/gemini-veo/scene-0001.mp4 +1 -0
  52. package/scripts/ai-video/lib/fixtures/higgsfield/result.json +1 -0
  53. package/scripts/ai-video/lib/fixtures/higgsfield/scene-0001.mp4 +1 -0
  54. package/scripts/ai-video/lib/fixtures/kling/result.json +1 -0
  55. package/scripts/ai-video/lib/fixtures/kling/scene-0001.mp4 +1 -0
  56. package/scripts/ai-video/lib/fixtures/openai-images/result.json +1 -0
  57. package/scripts/ai-video/lib/fixtures/openai-images/scene-0001.png +3 -0
  58. package/scripts/ai-video/lib/fixtures/sora/result.json +1 -0
  59. package/scripts/ai-video/lib/fixtures/sora/scene-0001.mp4 +1 -0
  60. package/scripts/ai-video/lib/load-config.sh +140 -0
  61. package/scripts/ai-video/lib/operator-pick.sh +119 -0
  62. package/scripts/ai-video/lib/parse-blueprint.sh +122 -0
  63. package/scripts/ai-video/lib/redact.sh +85 -0
  64. package/scripts/ai-video/lib/validate-deps.sh +132 -0
  65. package/scripts/ai-video/stitch.sh +154 -0
  66. package/scripts/ai-video/test-pipeline.sh +169 -0
  67. package/scripts/schemas/command.schema.json +8 -0
@@ -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}"
@@ -0,0 +1,169 @@
1
+ #!/usr/bin/env bash
2
+ # test-pipeline.sh — offline smoke test for /video:* against
3
+ # agents/ai-video/examples/banana-arc/. Dry-run only; no network.
4
+ #
5
+ # Asserts (per agents/roadmaps/ai-video-pipeline.md Phase 6 Step 3):
6
+ # 1. parse-blueprint.sh output matches the committed expected.json
7
+ # per scene (3 scenes, 3 tiers).
8
+ # 2. character.json descriptor tokens (silhouette, palette, wardrobe,
9
+ # prop) appear verbatim in each scene's prompt.subject — the
10
+ # load-bearing character-lock substring assertion.
11
+ # 3. audio.* branching is correct: scene 2 → enable_native_audio=true,
12
+ # scenes 1+3 → false.
13
+ # 4. native-audio-capable adapter (gemini-veo) advertises capability
14
+ # audio=native; non-audio adapter (openai-images) advertises
15
+ # audio=none; stitch.sh sees audio_embedded per scene.
16
+ # 5. stitch.sh dry-run returns the committed manifest's stitch_output
17
+ # path without invoking ffmpeg or any network.
18
+ # 6. visual regression: each scene's locked.png is non-empty + has
19
+ # PNG magic; pairwise NCC ≥ 0.95 when `compare` is available.
20
+ # When unavailable, asserts byte-identity (the three frames are
21
+ # committed identical for the offline path).
22
+ #
23
+ # Exit 0 = all assertions pass; 1 = at least one failure (counted +
24
+ # summarized at the end).
25
+
26
+ set -uo pipefail
27
+
28
+ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
29
+ ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
30
+ PROJECT="$ROOT/agents/ai-video/examples/banana-arc"
31
+
32
+ PASS=0
33
+ FAIL=0
34
+ FAILS=""
35
+
36
+ ok() { PASS=$((PASS+1)); printf ' ✅ %s\n' "$1"; }
37
+ fail() { FAIL=$((FAIL+1)); FAILS="$FAILS\n ❌ $1"; printf ' ❌ %s\n' "$1"; }
38
+
39
+ require() {
40
+ if command -v "$1" >/dev/null 2>&1; then return 0; fi
41
+ fail "required tool missing: $1"
42
+ return 1
43
+ }
44
+
45
+ require jq || exit 1
46
+
47
+ printf '\n== test-pipeline.sh — banana-arc golden run (offline) ==\n\n'
48
+
49
+ # ---------------------------------------------------------------- 1
50
+ printf '[1/6] parse-blueprint vs. expected.json\n'
51
+ SCENES="01-simple 02-dialogue-native-audio 03-edge-duration"
52
+ for s in $SCENES; do
53
+ actual="$(bash "$ROOT/scripts/ai-video/lib/parse-blueprint.sh" "$PROJECT/scenes/$s/blueprint.txt" 2>/dev/null \
54
+ | jq -S . 2>/dev/null || true)"
55
+ expected="$(jq -S . "$PROJECT/scenes/$s/expected.json" 2>/dev/null || true)"
56
+ if [ -z "$actual" ]; then
57
+ fail "$s: parse-blueprint produced no output"
58
+ elif [ "$actual" = "$expected" ]; then
59
+ ok "$s: parse-blueprint matches expected.json"
60
+ else
61
+ fail "$s: parse-blueprint mismatch (diff actual vs. expected.json)"
62
+ fi
63
+ done
64
+
65
+ # ---------------------------------------------------------------- 2
66
+ printf '\n[2/6] character.json descriptors verbatim in prompt.subject\n'
67
+ SILHOUETTE="$(jq -r '.characters[0].silhouette' "$PROJECT/character.json")"
68
+ PALETTE="$(jq -r '.characters[0].palette' "$PROJECT/character.json")"
69
+ WARDROBE="$(jq -r '.characters[0].wardrobe' "$PROJECT/character.json")"
70
+ PROP="$(jq -r '.characters[0].prop' "$PROJECT/character.json")"
71
+
72
+ for s in $SCENES; do
73
+ subj="$(jq -r '.prompt.subject' "$PROJECT/scenes/$s/expected.json")"
74
+ miss=""
75
+ case "$subj" in *"$SILHOUETTE"*) :;; *) miss="$miss silhouette";; esac
76
+ case "$subj" in *"$PALETTE"*) :;; *) miss="$miss palette";; esac
77
+ case "$subj" in *"$WARDROBE"*) :;; *) miss="$miss wardrobe";; esac
78
+ case "$subj" in *"$PROP"*) :;; *) miss="$miss prop";; esac
79
+ if [ -z "$miss" ]; then
80
+ ok "$s: silhouette + palette + wardrobe + prop verbatim in prompt.subject"
81
+ else
82
+ fail "$s: missing verbatim tokens in prompt.subject:$miss"
83
+ fi
84
+ done
85
+
86
+ # ---------------------------------------------------------------- 3
87
+ printf '\n[3/6] audio.* branching matches manifest\n'
88
+ for entry in 01-simple:false 02-dialogue-native-audio:true 03-edge-duration:false; do
89
+ s="${entry%%:*}"
90
+ want="${entry##*:}"
91
+ got="$(jq -r '.audio.enable_native_audio' "$PROJECT/scenes/$s/expected.json")"
92
+ manifest_got="$(jq -r --arg id "$s" '.scenes[] | select(.id==$id) | .audio_embedded' "$PROJECT/manifest.json")"
93
+ if [ "$got" = "$want" ] && [ "$manifest_got" = "$want" ]; then
94
+ ok "$s: enable_native_audio=$got, manifest.audio_embedded=$manifest_got"
95
+ else
96
+ fail "$s: audio branching drift (expected=$want, blueprint=$got, manifest=$manifest_got)"
97
+ fi
98
+ done
99
+
100
+ # ---------------------------------------------------------------- 4
101
+ printf '\n[4/6] adapter capability declarations\n'
102
+ declare_caps() {
103
+ local adapter="$1"; local expected="$2"
104
+ local out got
105
+ out="$(AIV_DRYRUN=true bash "$ROOT/scripts/ai-video/adapters/$adapter.sh" capability 2>/dev/null || true)"
106
+ got="$(printf '%s' "$out" | jq -r '.audio // empty' 2>/dev/null || true)"
107
+ if [ "$got" = "$expected" ]; then
108
+ ok "$adapter: capability.audio=$got"
109
+ else
110
+ fail "$adapter: capability mismatch (want audio=$expected, got: ${got:-<unparseable: $out>})"
111
+ fi
112
+ }
113
+ declare_caps gemini-veo "native"
114
+ declare_caps openai-images "none"
115
+ declare_caps sora "native"
116
+ declare_caps kling "none"
117
+
118
+ # ---------------------------------------------------------------- 5
119
+ printf '\n[5/6] stitch.sh dry-run returns manifest output path\n'
120
+ STITCH_OUT="$(jq -r '.stitch_output' "$PROJECT/manifest.json")"
121
+ stitch_log="$(AIV_DRYRUN=true bash "$ROOT/scripts/ai-video/stitch.sh" \
122
+ "$PROJECT/manifest.json" "$PROJECT/$STITCH_OUT" 2>&1 || true)"
123
+ case "$stitch_log" in
124
+ *"$STITCH_OUT"*) ok "stitch.sh dry-run referenced $STITCH_OUT";;
125
+ *) fail "stitch.sh dry-run did not reference $STITCH_OUT (log: $stitch_log)";;
126
+ esac
127
+
128
+ # ---------------------------------------------------------------- 6
129
+ printf '\n[6/6] visual regression (locked.png pairwise)\n'
130
+ PNG_MAGIC="$(printf '\x89PNG\r\n\x1a\n')"
131
+ prev=""
132
+ have_compare=0
133
+ command -v compare >/dev/null 2>&1 && have_compare=1
134
+ for s in $SCENES; do
135
+ f="$PROJECT/scenes/$s/fixtures/frames/locked.png"
136
+ if [ ! -s "$f" ]; then
137
+ fail "$s: locked.png missing or empty"
138
+ continue
139
+ fi
140
+ head -c 8 "$f" | od -An -c | tr -d ' \n' | grep -q '211PNG' \
141
+ && ok "$s: locked.png is a valid PNG ($(wc -c < "$f" | tr -d ' ') bytes)" \
142
+ || fail "$s: locked.png lacks PNG magic"
143
+ if [ -n "$prev" ]; then
144
+ if [ "$have_compare" -eq 1 ]; then
145
+ ncc="$(compare -metric NCC "$prev" "$f" null: 2>&1 || true)"
146
+ awk_ok="$(awk -v v="$ncc" 'BEGIN { exit !(v+0 >= 0.95) }' && echo yes || echo no)"
147
+ if [ "$awk_ok" = "yes" ]; then
148
+ ok "$s: NCC vs. previous = $ncc (≥ 0.95)"
149
+ else
150
+ fail "$s: NCC vs. previous = $ncc (< 0.95)"
151
+ fi
152
+ else
153
+ if cmp -s "$prev" "$f"; then
154
+ ok "$s: byte-identical to previous (compare unavailable; offline fallback)"
155
+ else
156
+ fail "$s: differs from previous frame and compare is unavailable"
157
+ fi
158
+ fi
159
+ fi
160
+ prev="$f"
161
+ done
162
+
163
+ # ----------------------------------------------------------------
164
+ printf '\n----------------\nresult: %d passed · %d failed\n' "$PASS" "$FAIL"
165
+ if [ "$FAIL" -gt 0 ]; then
166
+ printf '%b\n' "$FAILS"
167
+ exit 1
168
+ fi
169
+ exit 0
@@ -34,6 +34,14 @@
34
34
  "pattern": "^[a-z][a-z0-9-]*$"
35
35
  }
36
36
  },
37
+ "personas": {
38
+ "type": "array",
39
+ "items": {
40
+ "type": "string",
41
+ "pattern": "^[a-z][a-z0-9-]*$"
42
+ },
43
+ "description": "Personas this command invokes (see .agent-src.uncompressed/personas/). Mirrors `skills:`; each entry must match a persona slug."
44
+ },
37
45
  "cluster": {
38
46
  "type": "string",
39
47
  "pattern": "^[a-z][a-z0-9-]*$",