#!/usr/bin/env bash # Ideation lane: run outside models on a design brief, one lane per provider, # inside the same contained container the security lane uses. The container # sees a disposable git-archive copy of HEAD and can reach only the model APIs. # # The brief is yours, not the runner's. Pass the directory holding it; it must # contain DESIGNER.md (how the model works and what it must produce), # CONTEXT.md (the surface and its constraints), SCREENS.md (what exists today) # and brief.md (the deliverable). Override any one with the matching variable. # # Usage: # ./prepare.sh # once: export HEAD, build, start proxy # ./run-ux.sh # default four models # MODELS="zai-coding-plan/glm-5.3" ./run-ux.sh # a subset # TIMEOUT=3600 VARIANT=high ./run-ux.sh # budget, reasoning effort # OUT= ./run-ux.sh # findings dir; default /out # EXTRA= INCLUDE=" ..." ./run-ux.sh set -euo pipefail HERE="$(cd "$(dirname "$0")" && pwd)" REPO="$(git -C "$HERE" rev-parse --show-toplevel)" BRIEF_DIR="${1:?usage: OUT= ./run-ux.sh }" BRIEF_DIR="$(cd "$BRIEF_DIR" && pwd)" [[ -d "$HERE/.code" ]] || { echo "No $HERE/.code: run $HERE/prepare.sh first." >&2; exit 1; } DESIGNER="${DESIGNER:-$BRIEF_DIR/DESIGNER.md}"; CONTEXT="${CONTEXT:-$BRIEF_DIR/CONTEXT.md}" SCREENS="${SCREENS:-$BRIEF_DIR/SCREENS.md}"; BRIEF="${BRIEF:-$BRIEF_DIR/brief.md}" for f in "$DESIGNER" "$CONTEXT" "$SCREENS" "$BRIEF"; do [[ -f "$f" ]] || { echo "missing $f" >&2; exit 1; } done # Findings land where the caller says; never in this directory, which is tracked. OUT="${OUT:-$BRIEF_DIR/out}"; mkdir -p "$OUT"; OUT="$(cd "$OUT" && pwd)"; export OUT DEFAULT_MODELS="deepseek/deepseek-v4-pro google/gemini-3.8-flash zai-coding-plan/glm-5.3 kimi-for-coding/k3-256k" MODELS="${MODELS:-$DEFAULT_MODELS}" TIMEOUT="${TIMEOUT:-2700}" VARIANT="${VARIANT-max}" VARIANT_FLAG=""; [[ -n "$VARIANT" ]] && VARIANT_FLAG="--variant $VARIANT" RUN_DIR="$OUT/$(date +%Y-%m-%d_%H%M%S)" mkdir -p "$RUN_DIR/_static" # bootstrap.css from the snapshot; app.css from the working tree, so a mockup # renders against the stylesheet the change is being made under. cp "$HERE/.code/internal/embeds/static/bootstrap.css" "$REPO/internal/embeds/static/app.css" "$RUN_DIR/_static/" # The brief travels inside the disposable copy, in a per-run directory so two # runs with different briefs can overlap. EXTRA (a file) is appended to the # prompt after brief.md; INCLUDE (space-separated files) travel alongside for # the model to read, and are named at the end of the prompt. UX="ux-$(basename "$RUN_DIR")" mkdir -p "$HERE/.code/$UX" cp "$DESIGNER" "$HERE/.code/$UX/DESIGNER.md"; cp "$CONTEXT" "$HERE/.code/$UX/CONTEXT.md"; cp "$SCREENS" "$HERE/.code/$UX/SCREENS.md"; cp "$BRIEF" "$HERE/.code/$UX/brief.md" EXTRA_CAT=""; INCLUDE_LINE="" if [[ -n "${EXTRA:-}" ]]; then cp "$EXTRA" "$HERE/.code/$UX/extra.md"; EXTRA_CAT="; echo; echo '---'; cat $UX/extra.md"; fi if [[ -n "${INCLUDE:-}" ]]; then mkdir -p "$HERE/.code/$UX/include"; names="" for f in $INCLUDE; do cp "$f" "$HERE/.code/$UX/include/"; names="$names $UX/include/$(basename "$f")"; done INCLUDE_LINE="; echo; echo 'Files to read first, in this copy:$names'" fi { echo "commit $(cat "$HERE/.code/AUDIT_COMMIT.txt")" echo "models $MODELS" echo "brief $(basename "$BRIEF") context $(basename "$CONTEXT") screens $(basename "$SCREENS")${EXTRA:+ + extra $(basename "$EXTRA")}${INCLUDE:+ + include $INCLUDE}" echo "variant ${VARIANT:-none}" echo "timeout ${TIMEOUT}s per lane" echo "started $(date -Is)" } > "$RUN_DIR/manifest.txt" compose() { docker compose -f "$HERE/compose.yaml" --project-directory "$HERE" "$@"; } compose up -d egress-proxy >/dev/null run_lane() { local model="$1" safe="${1//\//__}" local outdir="$RUN_DIR/$safe"; mkdir -p "$outdir" echo "==> [$model] started $(date +%H:%M:%S)" # Fresh --rm container per model. --no-deps keeps the proxy as it is. -T: no # TTY. stdin closed (opencode headless quirk). The prompt is the four brief # files plus one line naming this lane's output directory. if ! timeout "$TIMEOUT" docker compose -f "$HERE/compose.yaml" --project-directory "$HERE" run --rm --no-deps -T -v "$RUN_DIR:/ux-out" auditor \ bash -c "opencode run --model '$model' $VARIANT_FLAG \"\$(cat $UX/DESIGNER.md; echo; echo '---'; cat $UX/CONTEXT.md; echo; echo '---'; cat $UX/SCREENS.md; echo; echo '---'; cat $UX/brief.md$EXTRA_CAT$INCLUDE_LINE; echo; echo 'Write the mockup to /ux-out/$safe/mockup.html (the directory exists; use that absolute path).')\" "$outdir/proposal.md" 2>"$outdir/proposal.err"; then echo "exit non-zero or timeout at $(date -Is); opencode exit codes are unreliable, judge by proposal.md" >>"$outdir/_lane.log" fi if [[ -s "$outdir/proposal.md" ]] && grep -q '^COMPLETE' "$outdir/proposal.md"; then echo " ok [$model] $(wc -l <"$outdir/proposal.md") lines; mockup: $([[ -f "$outdir/mockup.html" ]] && echo yes || echo NO)" else echo " SUSPECT [$model] empty or no COMPLETE line; see $outdir/proposal.err" fi } pids=() for m in $MODELS; do run_lane "$m" & pids+=("$!"); done echo "==> ${#pids[@]} lanes running (pids ${pids[*]}); one provider per lane" wait "${pids[@]}" || true # The container runs as root; hand the files back to the invoking user. compose run --rm --no-deps -T -v "$RUN_DIR:/ux-out" auditor chown -R "$(id -u):$(id -g)" /ux-out >/dev/null 2>&1 || true echo "finished $(date -Is)" >> "$RUN_DIR/manifest.txt" echo "Proposals under: $RUN_DIR"