feat(prompts): modular agent packs and builder

- Add COMMON prompt modules + manifests (base, CTO, COO)
- Add scripts/prompts builder (runs in CI container with host uid/gid)
- Make targets: prompts, prompts-check
This commit is contained in:
2025-09-10 17:34:28 -05:00
parent 56aa2a1522
commit d2eb0e1f79
12 changed files with 245 additions and 0 deletions

130
scripts/prompts Normal file
View File

@@ -0,0 +1,130 @@
#!/usr/bin/env bash
set -euo pipefail
usage() {
cat >&2 <<'USAGE'
Usage: scripts/prompts <command> [args]
Commands:
build <manifest> <output> Build a flattened prompt from a manifest
pack <area> Build known area pack (cto|coo) into dist/prompts
all Build all known area packs
lint Lint prompts (budgets and includes)
USAGE
exit 2
}
repo_root() { git rev-parse --show-toplevel 2>/dev/null || pwd; }
ci_run() {
local root; root="$(repo_root)"
# Ensure ci image is available by invoking a no-op build via scripts/ci
# Use compose to run with current uid:gid to avoid file ownership issues
docker compose -f "$root/docker/ci.compose.yml" run --rm \
--user "$(id -u):$(id -g)" \
-e IN_CI_CONTAINER=1 ci bash -lc "$1" </dev/null
}
build_manifest() {
local manifest=$1 out=$2 root
root="$(repo_root)"
mkdir -p "$root/dist/prompts"
local cmd
cmd=$(cat <<'PY'
python3 - << 'EOF'
import os, sys, yaml
manifest_path = sys.argv[1]
out_path = sys.argv[2]
seen = []
def load_manifest(path):
with open(path, 'r', encoding='utf-8') as f:
return yaml.safe_load(f)
def collect(mod, acc):
if mod not in acc:
acc.append(mod)
def resolve(path):
m = load_manifest(path)
includes = m.get('include', []) or []
modules = m.get('modules', []) or []
for inc in includes:
for x in resolve(inc):
collect(x, seen)
for mod in modules:
collect(mod, seen)
return seen
mods = resolve(manifest_path)
if not mods:
print(f"No modules resolved from {manifest_path}", file=sys.stderr)
sys.exit(1)
os.makedirs(os.path.dirname(out_path), exist_ok=True)
def read(p):
with open(p, 'r', encoding='utf-8') as f:
return f.read().strip() + "\n\n"
with open(out_path, 'w', encoding='utf-8') as out:
out.write("Generated Prompt Pack\n\n")
for m in mods:
out.write(f"--- {m} ---\n")
out.write(read(m))
# Budgets (approximate by word count)
def words(s):
return len(s.split())
with open(out_path, 'r', encoding='utf-8') as f:
content = f.read()
total_words = words(content)
BASE_BUDGET = 1200
if total_words > BASE_BUDGET:
print(f"ERROR: Pack exceeds budget: {total_words} > {BASE_BUDGET}", file=sys.stderr)
sys.exit(3)
# Per-module budget check
ERRORS = 0
MOD_BUDGET = 400
for m in mods:
with open(m, 'r', encoding='utf-8') as f:
wc = words(f.read())
if wc > MOD_BUDGET:
print(f"ERROR: Module {m} exceeds budget: {wc} > {MOD_BUDGET}", file=sys.stderr)
ERRORS += 1
if ERRORS:
sys.exit(4)
print(f"Built {out_path} with {total_words} words across {len(mods)} modules.")
EOF
PY
)
ci_run "$cmd" <<<"$manifest $out"
}
cmd=${1:-}
case "$cmd" in
build)
shift; [[ $# -eq 2 ]] || usage
build_manifest "$1" "$2" ;;
pack)
shift; area=${1:-}; root="$(repo_root)"
case "$area" in
cto) build_manifest "$root/COMMON/prompt/manifests/cto.yaml" "$root/dist/prompts/cto.md" ;;
coo) build_manifest "$root/COMMON/prompt/manifests/coo.yaml" "$root/dist/prompts/coo.md" ;;
*) echo "Unknown area: $area" >&2; exit 2 ;;
esac ;;
all)
root="$(repo_root)"; mkdir -p "$root/dist/prompts"
"$0" pack cto
"$0" pack coo ;;
lint)
# Rebuild and rely on budget checks to fail if over
"$0" all ;;
*) usage ;;
esac