@event4u/agent-config 2.23.0 → 2.25.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.agent-src/commands/create-pr/description-only.md +39 -11
- package/.agent-src/commands/create-pr.md +59 -5
- package/.agent-src/commands/video/from-script.md +123 -0
- package/.agent-src/commands/video/scene.md +92 -0
- package/.agent-src/commands/video/stitch.md +83 -0
- package/.agent-src/commands/video/storyboard.md +95 -0
- package/.agent-src/commands/video.md +59 -0
- package/.agent-src/contexts/execution/roadmap-process-loop.md +69 -14
- package/.agent-src/personas/README.md +5 -1
- package/.agent-src/personas/ai-video-technical-director.md +81 -0
- package/.agent-src/personas/hollywood-director.md +99 -0
- package/.agent-src/profiles/content_creator.yml +5 -0
- package/.agent-src/rules/media-governance-routing.md +82 -0
- package/.agent-src/rules/persona-governance.md +90 -0
- package/.agent-src/rules/post-push-rewrite-discipline.md +70 -0
- package/.agent-src/rules/provider-lifecycle-discipline.md +75 -0
- package/.agent-src/rules/roadmap-ci-steps-policy.md +145 -0
- package/.agent-src/rules/roadmap-progress-sync.md +11 -5
- package/.agent-src/skills/character-consistency/SKILL.md +131 -0
- package/.agent-src/skills/git-workflow/SKILL.md +133 -0
- package/.agent-src/skills/motion-choreographer/SKILL.md +161 -0
- package/.agent-src/skills/pixar-storyteller/SKILL.md +120 -0
- package/.agent-src/skills/roadmap-writing/SKILL.md +10 -0
- package/.agent-src/skills/scene-expander/SKILL.md +137 -0
- package/.agent-src/skills/scene-expander/scene-blueprint.schema.yaml +108 -0
- package/.agent-src/skills/subagent-orchestration/SKILL.md +17 -15
- package/.agent-src/skills/video-director/SKILL.md +126 -0
- package/.agent-src/templates/agent-settings.md +19 -0
- package/.agent-src/templates/agents/agent-project-settings.example.yml +1 -1
- package/.agent-src/templates/roadmaps.md +16 -0
- package/.claude-plugin/marketplace.json +11 -1
- package/CHANGELOG.md +65 -0
- package/README.md +7 -5
- package/config/agent-settings.template.yml +54 -0
- package/docs/adrs/caveman/0001-default-off-until-bench.md +2 -2
- package/docs/adrs/cost/0001-hard-stop-hook.md +1 -1
- package/docs/adrs/smoke/0001-per-tier-smoke-scripts.md +2 -2
- package/docs/architecture.md +3 -3
- package/docs/catalog.md +18 -5
- package/docs/contracts/command-clusters.md +1 -0
- package/docs/contracts/compression-default-kill-criterion.md +1 -1
- package/docs/contracts/file-ownership-matrix.json +405 -0
- package/docs/contracts/provider-lifecycle.md +122 -0
- package/docs/decisions/ADR-011-domain-pack-readiness.md +213 -0
- package/docs/decisions/INDEX.md +1 -0
- package/docs/getting-started-by-role.md +10 -0
- package/docs/getting-started.md +2 -2
- package/docs/parity/ruflo.md +3 -3
- package/docs/personas.md +73 -26
- package/docs/profiles.md +9 -4
- package/package.json +1 -1
- package/scripts/_tmp_scan_framework_leakage.py +119 -0
- package/scripts/ai-video/adapters/gemini-veo.sh +62 -0
- package/scripts/ai-video/adapters/higgsfield.sh +88 -0
- package/scripts/ai-video/adapters/kling.sh +59 -0
- package/scripts/ai-video/adapters/openai-images.sh +57 -0
- package/scripts/ai-video/adapters/sora.sh +60 -0
- package/scripts/ai-video/lib/adapter-common.sh +116 -0
- package/scripts/ai-video/lib/adapter-contract.md +163 -0
- package/scripts/ai-video/lib/fixtures/gemini-veo/result.json +1 -0
- package/scripts/ai-video/lib/fixtures/gemini-veo/scene-0001.mp4 +1 -0
- package/scripts/ai-video/lib/fixtures/higgsfield/result.json +1 -0
- package/scripts/ai-video/lib/fixtures/higgsfield/scene-0001.mp4 +1 -0
- package/scripts/ai-video/lib/fixtures/kling/result.json +1 -0
- package/scripts/ai-video/lib/fixtures/kling/scene-0001.mp4 +1 -0
- package/scripts/ai-video/lib/fixtures/openai-images/result.json +1 -0
- package/scripts/ai-video/lib/fixtures/openai-images/scene-0001.png +3 -0
- package/scripts/ai-video/lib/fixtures/sora/result.json +1 -0
- package/scripts/ai-video/lib/fixtures/sora/scene-0001.mp4 +1 -0
- package/scripts/ai-video/lib/load-config.sh +140 -0
- package/scripts/ai-video/lib/operator-pick.sh +119 -0
- package/scripts/ai-video/lib/parse-blueprint.sh +122 -0
- package/scripts/ai-video/lib/redact.sh +85 -0
- package/scripts/ai-video/lib/validate-deps.sh +132 -0
- package/scripts/ai-video/stitch.sh +154 -0
- package/scripts/ai-video/test-pipeline.sh +169 -0
- package/scripts/check_portability.py +6 -0
- package/scripts/lint_media_policy_linkage.py +140 -0
- package/scripts/lint_persona_governance.py +164 -0
- package/scripts/lint_roadmap_ci_steps.py +182 -0
- package/scripts/schemas/command.schema.json +8 -0
- package/scripts/smoke/schema.sh +1 -1
|
@@ -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
|
|
@@ -319,6 +319,12 @@ _TASK_DETECTOR_SKIP = (
|
|
|
319
319
|
"rules/package-ci-checks.md",
|
|
320
320
|
"contexts/communication/rules-auto/package-ci-checks-mechanics.md",
|
|
321
321
|
"contexts/contracts/agents-md-anatomy.md",
|
|
322
|
+
# roadmap-ci-steps-policy defines the gate by listing the forbidden
|
|
323
|
+
# CI-shaped literals; its mechanics doc and the execution loop +
|
|
324
|
+
# authoring skill enumerate the same literals to detect them.
|
|
325
|
+
"rules/roadmap-ci-steps-policy.md",
|
|
326
|
+
"contexts/execution/roadmap-process-loop.md",
|
|
327
|
+
"skills/roadmap-writing/SKILL.md",
|
|
322
328
|
)
|
|
323
329
|
|
|
324
330
|
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Lint structural reachability of media governance policies.
|
|
3
|
+
|
|
4
|
+
Every policy file under `agents/policies/media/` (except README) must
|
|
5
|
+
be linked from at least one of:
|
|
6
|
+
|
|
7
|
+
* a skill SKILL.md (any .agent-src.uncompressed/skills/*/SKILL.md
|
|
8
|
+
or .claude/skills/*/SKILL.md),
|
|
9
|
+
* a routing rule under .agent-src.uncompressed/rules/, or
|
|
10
|
+
* a sibling policy file under agents/policies/media/.
|
|
11
|
+
|
|
12
|
+
A policy that no surface references is a silent policy and a silent
|
|
13
|
+
policy is a failed policy. This is the CI-side reachability guarantee
|
|
14
|
+
the agent-in-the-loop enforcement model rests on (see
|
|
15
|
+
agents/policies/media/README.md § Enforcement model).
|
|
16
|
+
|
|
17
|
+
Exit codes:
|
|
18
|
+
0 all policies linked
|
|
19
|
+
1 one or more orphan policies
|
|
20
|
+
"""
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
import sys
|
|
24
|
+
from pathlib import Path
|
|
25
|
+
|
|
26
|
+
QUIET = "--quiet" in sys.argv
|
|
27
|
+
|
|
28
|
+
REPO = Path(__file__).resolve().parents[1]
|
|
29
|
+
POLICY_DIR = REPO / "agents" / "policies" / "media"
|
|
30
|
+
EXEMPT_STEMS = frozenset({"README"})
|
|
31
|
+
|
|
32
|
+
# Surfaces scanned for inbound references to policy files.
|
|
33
|
+
SCAN_ROOTS: tuple[Path, ...] = (
|
|
34
|
+
REPO / ".agent-src.uncompressed" / "skills",
|
|
35
|
+
REPO / ".agent-src.uncompressed" / "rules",
|
|
36
|
+
REPO / ".agent-src.uncompressed" / "commands",
|
|
37
|
+
REPO / ".claude" / "skills",
|
|
38
|
+
REPO / "agents" / "policies" / "media",
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def emit(msg: str) -> None:
|
|
43
|
+
if not QUIET:
|
|
44
|
+
print(msg)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def collect_policies() -> list[Path]:
|
|
48
|
+
if not POLICY_DIR.exists():
|
|
49
|
+
return []
|
|
50
|
+
return sorted(
|
|
51
|
+
p
|
|
52
|
+
for p in POLICY_DIR.glob("*.md")
|
|
53
|
+
if p.stem not in EXEMPT_STEMS
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def collect_scan_files() -> list[Path]:
|
|
58
|
+
files: list[Path] = []
|
|
59
|
+
for root in SCAN_ROOTS:
|
|
60
|
+
if not root.exists():
|
|
61
|
+
continue
|
|
62
|
+
files.extend(root.rglob("*.md"))
|
|
63
|
+
return files
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def referrers_for(policy: Path, scan_files: list[Path]) -> list[Path]:
|
|
67
|
+
"""Return files that reference `policy` by its repo-relative name
|
|
68
|
+
or basename. We accept both the full path token
|
|
69
|
+
(`agents/policies/media/likeness.md`) and the bare basename
|
|
70
|
+
(`likeness.md`) inside a markdown link, because sibling policies
|
|
71
|
+
link via relative `[likeness.md](likeness.md)` form.
|
|
72
|
+
"""
|
|
73
|
+
needles = (
|
|
74
|
+
f"policies/media/{policy.name}",
|
|
75
|
+
f"]({policy.name})",
|
|
76
|
+
)
|
|
77
|
+
referrers: list[Path] = []
|
|
78
|
+
for scan_file in scan_files:
|
|
79
|
+
# A policy can't satisfy its own linkage requirement.
|
|
80
|
+
if scan_file.resolve() == policy.resolve():
|
|
81
|
+
continue
|
|
82
|
+
try:
|
|
83
|
+
text = scan_file.read_text(encoding="utf-8", errors="replace")
|
|
84
|
+
except OSError:
|
|
85
|
+
continue
|
|
86
|
+
if any(n in text for n in needles):
|
|
87
|
+
referrers.append(scan_file)
|
|
88
|
+
return referrers
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def main() -> int:
|
|
92
|
+
if not POLICY_DIR.exists():
|
|
93
|
+
emit(
|
|
94
|
+
"media-policy-linkage: agents/policies/media/ missing — "
|
|
95
|
+
"nothing to lint."
|
|
96
|
+
)
|
|
97
|
+
return 0
|
|
98
|
+
|
|
99
|
+
policies = collect_policies()
|
|
100
|
+
if not policies:
|
|
101
|
+
emit(
|
|
102
|
+
"media-policy-linkage: agents/policies/media/ has no policy "
|
|
103
|
+
"files — nothing to lint."
|
|
104
|
+
)
|
|
105
|
+
return 0
|
|
106
|
+
|
|
107
|
+
scan_files = collect_scan_files()
|
|
108
|
+
orphans: list[Path] = []
|
|
109
|
+
for policy in policies:
|
|
110
|
+
referrers = referrers_for(policy, scan_files)
|
|
111
|
+
rel = policy.relative_to(REPO)
|
|
112
|
+
if not referrers:
|
|
113
|
+
orphans.append(policy)
|
|
114
|
+
emit(f"❌ ORPHAN {rel}")
|
|
115
|
+
continue
|
|
116
|
+
emit(f"✅ {rel} ({len(referrers)} referrer(s))")
|
|
117
|
+
|
|
118
|
+
if orphans:
|
|
119
|
+
print(
|
|
120
|
+
f"\nmedia-policy-linkage: {len(orphans)} orphan policy "
|
|
121
|
+
f"file(s) — every policy must be linked from a skill, rule, "
|
|
122
|
+
f"or sibling policy.",
|
|
123
|
+
file=sys.stderr,
|
|
124
|
+
)
|
|
125
|
+
for o in orphans:
|
|
126
|
+
print(
|
|
127
|
+
f" - {o.relative_to(REPO)}",
|
|
128
|
+
file=sys.stderr,
|
|
129
|
+
)
|
|
130
|
+
return 1
|
|
131
|
+
|
|
132
|
+
emit(
|
|
133
|
+
f"media-policy-linkage: {len(policies)} policy file(s) — all "
|
|
134
|
+
f"linked."
|
|
135
|
+
)
|
|
136
|
+
return 0
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
if __name__ == "__main__":
|
|
140
|
+
sys.exit(main())
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Lint persona governance — per-domain cap (hard) + citation floor (warn).
|
|
3
|
+
|
|
4
|
+
Enforces the mechanical checks in
|
|
5
|
+
`.agent-src.uncompressed/rules/persona-governance.md`:
|
|
6
|
+
|
|
7
|
+
1. **Per-domain cap (HARD)** — ≤ 2 active specialist personas per
|
|
8
|
+
content domain. Core-tier personas are exempt. `status:
|
|
9
|
+
deprecated` rows (if any survive a transition window) are
|
|
10
|
+
excluded from the count; the canonical path is in-commit
|
|
11
|
+
deletion, no soak window.
|
|
12
|
+
2. **Skill citation floor (WARN)** — every active specialist
|
|
13
|
+
persona SHOULD be cited by `personas: [<id>]` in at least one
|
|
14
|
+
skill SKILL.md under `.agent-src.uncompressed/skills/` or
|
|
15
|
+
`.claude/skills/`. Surfaced as a warning, never blocks CI:
|
|
16
|
+
the citation floor is enforced at PR time per the rule (a new
|
|
17
|
+
specialist MUST land with a cite); pre-existing wiring debt is
|
|
18
|
+
tracked as `--citation-debt` for the maintainer dashboard.
|
|
19
|
+
|
|
20
|
+
Schema conformance (check 4) is delegated to `lint-skills`.
|
|
21
|
+
Deprecation path (check 3) is reviewed at PR time via the table in
|
|
22
|
+
`docs/personas.md`.
|
|
23
|
+
|
|
24
|
+
Domain inference: persona ids → content domain via `DOMAIN_MAP`,
|
|
25
|
+
mirroring `persona-governance.md § Per-domain cap`. Personas absent
|
|
26
|
+
from the map are cross-cutting (uncapped).
|
|
27
|
+
|
|
28
|
+
Exit codes:
|
|
29
|
+
0 per-domain cap clean (citation warnings non-blocking)
|
|
30
|
+
1 per-domain cap violated
|
|
31
|
+
"""
|
|
32
|
+
from __future__ import annotations
|
|
33
|
+
|
|
34
|
+
import re
|
|
35
|
+
import sys
|
|
36
|
+
from collections import defaultdict
|
|
37
|
+
from pathlib import Path
|
|
38
|
+
|
|
39
|
+
QUIET = "--quiet" in sys.argv
|
|
40
|
+
|
|
41
|
+
REPO = Path(__file__).resolve().parents[1]
|
|
42
|
+
PERSONA_DIR = REPO / ".agent-src.uncompressed" / "personas"
|
|
43
|
+
SKILL_ROOTS: tuple[Path, ...] = (
|
|
44
|
+
REPO / ".agent-src.uncompressed" / "skills",
|
|
45
|
+
REPO / ".claude" / "skills",
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
# Per-domain cap — mirrors persona-governance.md § Per-domain cap.
|
|
49
|
+
# Maps persona id → content-domain bucket. Personas absent from this
|
|
50
|
+
# map are cross-cutting (uncapped) — typically singleton specialists
|
|
51
|
+
# without a domain peer (e.g. `qa`, `tech-writer`).
|
|
52
|
+
DOMAIN_MAP: dict[str, str] = {
|
|
53
|
+
"hollywood-director": "ai-video",
|
|
54
|
+
"ai-video-technical-director": "ai-video",
|
|
55
|
+
"backend-architect": "backend",
|
|
56
|
+
"eloquent-tamer": "backend",
|
|
57
|
+
"cmo": "gtm",
|
|
58
|
+
"revops": "gtm",
|
|
59
|
+
"growth-pm": "growth",
|
|
60
|
+
"customer-success-lead": "customer",
|
|
61
|
+
"discovery-lead": "customer",
|
|
62
|
+
"engineering-manager": "people",
|
|
63
|
+
"people-strategist": "people",
|
|
64
|
+
"finance-partner": "money",
|
|
65
|
+
"strategist": "money",
|
|
66
|
+
}
|
|
67
|
+
PER_DOMAIN_CAP = 2
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def emit(msg: str) -> None:
|
|
71
|
+
if not QUIET:
|
|
72
|
+
print(msg)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def parse_frontmatter(path: Path) -> dict[str, str]:
|
|
76
|
+
text = path.read_text(encoding="utf-8", errors="replace")
|
|
77
|
+
if not text.startswith("---"):
|
|
78
|
+
return {}
|
|
79
|
+
end = text.find("\n---", 3)
|
|
80
|
+
if end == -1:
|
|
81
|
+
return {}
|
|
82
|
+
out: dict[str, str] = {}
|
|
83
|
+
for line in text[3:end].splitlines():
|
|
84
|
+
m = re.match(r"^([a-zA-Z_][\w-]*):\s*(.*)$", line)
|
|
85
|
+
if m:
|
|
86
|
+
out[m.group(1)] = m.group(2).strip().strip('"').strip("'")
|
|
87
|
+
return out
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def collect_personas() -> list[tuple[str, str, str, Path]]:
|
|
91
|
+
"""Return (id, tier, status, path) for every persona file."""
|
|
92
|
+
out: list[tuple[str, str, str, Path]] = []
|
|
93
|
+
if not PERSONA_DIR.exists():
|
|
94
|
+
return out
|
|
95
|
+
for path in sorted(PERSONA_DIR.glob("*.md")):
|
|
96
|
+
if path.stem == "README":
|
|
97
|
+
continue
|
|
98
|
+
fm = parse_frontmatter(path)
|
|
99
|
+
pid = fm.get("id") or path.stem
|
|
100
|
+
tier = fm.get("tier", "")
|
|
101
|
+
status = fm.get("status", "active") or "active"
|
|
102
|
+
out.append((pid, tier, status, path))
|
|
103
|
+
return out
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def citations_for(persona_id: str) -> list[Path]:
|
|
107
|
+
pattern = re.compile(rf"(^|[\s,\[]){re.escape(persona_id)}([\s,\]]|$)")
|
|
108
|
+
hits: list[Path] = []
|
|
109
|
+
for root in SKILL_ROOTS:
|
|
110
|
+
if not root.exists():
|
|
111
|
+
continue
|
|
112
|
+
for skill in root.rglob("SKILL.md"):
|
|
113
|
+
text = skill.read_text(encoding="utf-8", errors="replace")
|
|
114
|
+
if text.startswith("---"):
|
|
115
|
+
end = text.find("\n---", 3)
|
|
116
|
+
fm_block = text[3:end] if end != -1 else ""
|
|
117
|
+
else:
|
|
118
|
+
fm_block = ""
|
|
119
|
+
if "personas:" not in fm_block:
|
|
120
|
+
continue
|
|
121
|
+
if pattern.search(fm_block):
|
|
122
|
+
hits.append(skill)
|
|
123
|
+
return hits
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def main() -> int:
|
|
127
|
+
personas = collect_personas()
|
|
128
|
+
if not personas:
|
|
129
|
+
emit("persona-governance: no persona files found — nothing to lint.")
|
|
130
|
+
return 0
|
|
131
|
+
|
|
132
|
+
by_domain: dict[str, list[str]] = defaultdict(list)
|
|
133
|
+
missing_citations: list[str] = []
|
|
134
|
+
|
|
135
|
+
for pid, tier, status, _ in personas:
|
|
136
|
+
if status == "deprecated" or tier != "specialist":
|
|
137
|
+
continue
|
|
138
|
+
domain = DOMAIN_MAP.get(pid)
|
|
139
|
+
if domain:
|
|
140
|
+
by_domain[domain].append(pid)
|
|
141
|
+
if not citations_for(pid):
|
|
142
|
+
missing_citations.append(pid)
|
|
143
|
+
|
|
144
|
+
overflows = {d: ids for d, ids in by_domain.items() if len(ids) > PER_DOMAIN_CAP}
|
|
145
|
+
for d, ids in sorted(by_domain.items()):
|
|
146
|
+
marker = "❌" if d in overflows else "✅"
|
|
147
|
+
emit(f"{marker} domain={d} {len(ids)}/{PER_DOMAIN_CAP} {', '.join(sorted(ids))}")
|
|
148
|
+
for pid in sorted(missing_citations):
|
|
149
|
+
emit(f"⚠️ no-skill-citation {pid} (warn — see PR-time gate)")
|
|
150
|
+
|
|
151
|
+
if overflows:
|
|
152
|
+
print("\npersona-governance: per-domain cap violated.", file=sys.stderr)
|
|
153
|
+
for d, ids in sorted(overflows.items()):
|
|
154
|
+
print(f" - domain '{d}' has {len(ids)} specialists (cap {PER_DOMAIN_CAP}): {', '.join(sorted(ids))}", file=sys.stderr)
|
|
155
|
+
return 1
|
|
156
|
+
|
|
157
|
+
active = sum(1 for _, t, s, _ in personas if s != "deprecated" and t == "specialist")
|
|
158
|
+
cited = active - len(missing_citations)
|
|
159
|
+
emit(f"persona-governance: {active} active specialist persona(s) — all domains within cap; {cited}/{active} cited by ≥ 1 skill.")
|
|
160
|
+
return 0
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
if __name__ == "__main__":
|
|
164
|
+
sys.exit(main())
|