← all scripts

demo/record_autopilot.sh

166 of 166 statements covered (100.00%).

coveredmissednever traced by Bash (not counted)A line ending in … continues the statement above it and shares its fate.

18#!/usr/bin/env bash
2# ─────────────────────────────────────────────────────────────────────────────
3# Record the GCO Autopilot demo as an animated GIF
4# ─────────────────────────────────────────────────────────────────────────────
5# Records a short terminal session of `gco autopilot` and converts it to a
6# GIF with agg. Select the engine with DEMO_ENGINE (``claude-code`` by
7# default, or ``codex``) and the scenario with DEMO_MODE:
8#
9# live (default) A real interactive session on Amazon Bedrock. The recorder
10# launches the selected TUI, types a GCO question, waits for
11# an MCP-grounded answer, then exits. Requires the selected
12# engine binary, expect(1), and Bedrock-enabled credentials.
13# plan Credential-free recording of the selected engine's
14# `--dry-run` plan: model, reasoning, MCP set, config path,
15# and exact lazy-install pin. This is the Codex demo mode.
16#
17# The recording drives the *checked-out* CLI through a `gco` PATH shim
18# (`python3 -m cli.main`), never a globally installed gco, so the GIF always
19# reflects the code in this working tree. Model latency is compressed by
20# asciinema's --idle-time-limit, so the live mode stays a short GIF.
21#
22# Output files (deposited in demo/):
23# Claude: demo/autopilot-claude-code.cast + demo/autopilot-claude-code.gif
24# Codex: demo/autopilot-codex.cast + demo/autopilot-codex.gif
25#
26# Prerequisites:
27# - asciinema: brew install asciinema (or pip install asciinema)
28# - agg: brew install agg (or cargo install agg)
29# - python3 with the repo's dependencies importable (dev container, or
30# an environment where `python3 -m cli.main --help` works)
31#
32# Usage:
33# bash demo/record_autopilot.sh
34#
35# Options (via environment variables):
36# DEMO_ENGINE=claude-code "claude-code" (default) or "codex"
37# DEMO_MODE=live "live" (real selected-engine Bedrock session, default)
38# or "plan" (credential-free engine launch plan)
39# DEMO_COLS=110 Terminal width for recording (default: 110)
40# DEMO_ROWS=30 Terminal height for recording (default: 30)
41# DEMO_SPEED=1.6 Playback speed multiplier for GIF (default: 1.6)
42# DEMO_THEME=monokai agg color theme (default: monokai)
43# DEMO_FONT_FAMILY agg font fallback chain (default: see lib_demo.sh)
44# SKIP_GIF=1 Only produce the .cast file, skip GIF conversion
45# SKIP_SANITIZE=1 Skip AWS-account-ID redaction (debugging only)
46# SKIP_EMOJI_STRIP=1 Skip emoji substitution (debugging only)
47#
48# The recorded .cast is post-processed exactly like the other demo
49# recordings before the GIF is rendered: sanitize_cast redacts anything
50# shaped like an AWS account ID or access-key ID (verified afterwards by
51# verify_cast_sanitized), and strip_emoji_from_cast rewrites codepoints
52# agg's text engine can't render. See demo/lib_demo.sh.
53# ─────────────────────────────────────────────────────────────────────────────
54
5512set -euo pipefail
56
57# ── Configuration ────────────────────────────────────────────────────────────
58
5948SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
60# The checkout being recorded: normally the one this script lives in. The BATS
61# suite points GCO_RECORDING_REPO_ROOT at a disposable fixture repository so
62# the tracked recorder runs in place against it; left unset, every path below
63# is the same as before the override existed.
6436REPO_ROOT="$(cd "${GCO_RECORDING_REPO_ROOT:-$SCRIPT_DIR/..}" && pwd)"
6512DEMO_DIR="${REPO_ROOT}/demo"
66
67# shellcheck source=demo/lib_demo.sh
6812source "${SCRIPT_DIR}/lib_demo.sh"
6912setup_colors
70
7112DEMO_ENGINE="${DEMO_ENGINE:-claude-code}"
7212DEMO_MODE="${DEMO_MODE:-live}"
73
7412case "$DEMO_ENGINE" in
75 claude-code)
767 CAST_FILE="${DEMO_DIR}/autopilot-claude-code.cast"
777 GIF_FILE="${DEMO_DIR}/autopilot-claude-code.gif"
78 ;;
79 codex)
804 CAST_FILE="${DEMO_DIR}/autopilot-codex.cast"
814 GIF_FILE="${DEMO_DIR}/autopilot-codex.gif"
82 ;;
832 *) echo "error: DEMO_ENGINE must be 'claude-code' or 'codex', got '$DEMO_ENGINE'" >&2; exit 1 ;;
84esac
85
8611COLS="${DEMO_COLS:-110}"
8711ROWS="${DEMO_ROWS:-30}"
8811SPEED="${DEMO_SPEED:-1.6}"
8911THEME="${DEMO_THEME:-monokai}"
90
9111case "$DEMO_MODE" in
9210 live|plan) : ;;
932 *) echo "error: DEMO_MODE must be 'live' or 'plan', got '$DEMO_MODE'" >&2; exit 1 ;;
94esac
95
96# ── Preflight Checks ────────────────────────────────────────────────────────
97
9810PREFLIGHT_FAIL=0
99
100preflight_pass() {
10162 echo " ${GREEN}${BOLD}✓${RESET} $1"
102}
103
104preflight_fail() {
10510 echo " ${RED}${BOLD}✗${RESET} $1"
10610 echo " ${DIM}Fix: $2${RESET}"
10710 PREFLIGHT_FAIL=$((PREFLIGHT_FAIL + 1))
108}
109
11010echo "=== GCO Autopilot Demo Recorder (${DEMO_ENGINE}, ${DEMO_MODE}) ==="
11110echo ""
112
11310if command -v asciinema &>/dev/null; then
11427 preflight_pass "asciinema installed ($(asciinema --version 2>&1 | head -1))"
115else
1161 preflight_fail "asciinema not installed" \
117 "brew install asciinema (macOS) or pip install asciinema (Linux)"
118fi
119
12010if [ "${SKIP_GIF:-}" != "1" ]; then
1218 if command -v agg &>/dev/null; then
12221 preflight_pass "agg installed ($(agg --version 2>&1 | head -1))"
123 else
1241 preflight_fail "agg not installed" \
125 "brew install agg (macOS) or cargo install agg (Rust), or set SKIP_GIF=1"
126 fi
127fi
128
12920if (cd "$REPO_ROOT" && python3 -m cli.main --version &>/dev/null); then
1309 preflight_pass "GCO CLI importable (python3 -m cli.main)"
131else
1321 preflight_fail "GCO CLI not importable from this python3" \
133 "Run inside the dev container, or install the repo's deps (pip install -e .)"
134fi
135
13620if [ -f "${SCRIPT_DIR}/lib_demo.sh" ] && [ -f "${REPO_ROOT}/cdk.json" ]; then
1379 preflight_pass "Repository layout looks right"
138else
1391 preflight_fail "Repository layout unexpected" "Run from a full GCO checkout"
140fi
141
14210if [ "$DEMO_MODE" = "live" ]; then
1438 if [ "$DEMO_ENGINE" = "codex" ]; then
1443 ENGINE_BINARY="codex"
1453 ENGINE_LABEL="Codex"
1463 INSTALL_HINT="gco autopilot --engine codex -y"
147 else
1485 ENGINE_BINARY="claude"
1495 ENGINE_LABEL="Claude Code"
1505 INSTALL_HINT="gco autopilot -y"
151 fi
1528 if command -v "$ENGINE_BINARY" &>/dev/null; then
15318 preflight_pass "$ENGINE_LABEL installed ($("$ENGINE_BINARY" --version 2>&1 | head -1))"
154 else
1552 preflight_fail "$ENGINE_LABEL not installed (live mode launches a real session)" \
156 "Run '$INSTALL_HINT' once to install the pin, or set DEMO_MODE=plan"
157 fi
1588 if [ "$DEMO_ENGINE" != "codex" ]; then
15910 for companion_runtime in uvx npx; do
16010 if command -v "$companion_runtime" &>/dev/null; then
1618 preflight_pass "$companion_runtime installed"
162 else
1632 preflight_fail "$companion_runtime not installed (Claude live mode starts companions)" \
164 "Use gco-dev, install the missing runtime, or set DEMO_MODE=plan"
165 fi
166 done
167 fi
1688 if command -v expect &>/dev/null; then
1697 preflight_pass "expect installed (drives the interactive TUI)"
170 else
1711 preflight_fail "expect not installed (live mode scripts the TUI)" \
172 "macOS ships /usr/bin/expect; on Linux: apt install expect. Or set DEMO_MODE=plan"
173 fi
1748 if aws sts get-caller-identity &>/dev/null; then
1757 preflight_pass "AWS credentials resolve (Bedrock access is exercised by the recording)"
176 else
1771 preflight_fail "No AWS credentials (live mode makes a real Bedrock call)" \
178 "Configure credentials with Bedrock model access, or set DEMO_MODE=plan"
179 fi
180fi
181
18210if [ "$PREFLIGHT_FAIL" -gt 0 ]; then
1832 echo ""
1842 echo " ${RED}${BOLD}${PREFLIGHT_FAIL} check(s) failed. Fix the issues above before recording.${RESET}"
1852 exit 1
186fi
187
1888echo ""
189
190# ── Build the demo driver ───────────────────────────────────────────────────
191# A `gco` PATH shim keeps the on-screen command honest (`$ gco autopilot …`)
192# while guaranteeing the recording exercises this checkout's code.
193
19416SHIM_DIR="$(mktemp -d)"
19516DRIVER="$(mktemp)"
1968EXPECT_SCRIPT=""
1978trap 'rm -rf "$SHIM_DIR" "$DRIVER" ${EXPECT_SCRIPT:+"$EXPECT_SCRIPT"}' EXIT
198
1998cat > "${SHIM_DIR}/gco" <<'GCO_SHIM'
200#!/usr/bin/env bash
201exec python3 -m cli.main "$@"
202GCO_SHIM
2038chmod +x "${SHIM_DIR}/gco"
204
20514if [ "$DEMO_MODE" = "live" ] && [ "$DEMO_ENGINE" = "codex" ]; then
206 # Mirror the Claude recording with Codex's real inline TUI. Read-only,
207 # never-approve, and run-scoped trust for this reviewed checkout are hidden
208 # recording plumbing: the prompt explicitly uses GCO MCP documentation
209 # tools, no workspace mutation is needed, and no trust setting is persisted.
2104 EXPECT_SCRIPT="$(mktemp)"
2112 cat > "$EXPECT_SCRIPT" <<'EXPECT_DRIVER'
212#!/usr/bin/expect -f
213set timeout 420
214set stty_init "rows 30 columns 110"
215
216# Codex enables CSI-u keyboard reporting; this emits a physical Enter only
217# after a positively identified composer redraw below.
218proc press_enter {} {
219 send -- "\033\[13u"
220}
221
222# The recording uses only the local GCO MCP server and exposes exactly the two
223# documentation tools needed on camera. GCO startup is required, both tools are
224# explicitly preapproved, and Codex's built-in shell is removed entirely.
225set gco_required {mcp_servers.gco.required=true}
226set gco_tools {mcp_servers.gco.enabled_tools=["find_docs","read_resource"]}
227set find_docs_approval {mcp_servers.gco.tools.find_docs.approval_mode="approve"}
228set read_resource_approval {mcp_servers.gco.tools.read_resource.approval_mode="approve"}
229set prompt {Use only the GCO MCP find_docs tool, then read_resource on a returned documentation URI; do not use shell commands or other tools. Which gco command submits a job through SQS, and why is that recommended? Answer in two short lines. Begin when ready}
230log_user 0
231spawn gco autopilot --engine codex --no-companions -- -c $gco_required -c $gco_tools -c $find_docs_approval -c $read_resource_approval --disable shell_tool --sandbox read-only --ask-for-approval never --no-alt-screen -- $prompt
232log_user 1
233
234# Codex accepts the initial prompt as prefilled composer text. Monitor startup
235# for dialogs, then append one harmless space to force a current composer redraw;
236# only a positive match of that redraw is allowed to submit the turn.
237set submitted 0
238set timeout 20
239expect {
240 -nocase -re {trust the contents|trust the files|do you trust} { exit 6 }
241 -nocase -re {do you want to (proceed|allow)|allow this tool} { exit 7 }
242 -re {Working|Calling gco\.find_docs} { set submitted 1 }
243 timeout {}
244 eof { exit 3 }
245}
246if {!$submitted} {
247 send -- "."
248 set timeout 10
249 expect {
250 -re {Begin when ready.*\.} { press_enter }
251 -nocase -re {trust the contents|trust the files|do you trust} { exit 6 }
252 -nocase -re {do you want to (proceed|allow)|allow this tool} { exit 7 }
253 timeout { exit 8 }
254 eof { exit 3 }
255 }
256}
257# Wait for the explanatory final answer, not an intermediate tool query that
258# happens to mention the command. Trust and approval dialogs remain explicit
259# failures for the entire turn; never send an unqualified keypress on timeout.
260set timeout 420
261expect {
262 -nocase -re {trust the contents|trust the files|do you trust} { exit 6 }
263 -nocase -re {do you want to (proceed|allow)|allow this tool} { exit 7 }
264 -nocase -re {recommended (for production )?because|resilient production|durable, asynchronous} {}
265 timeout { exit 4 }
266 eof { exit 5 }
267}
268# Leave the complete short answer on screen before exiting. The matched phrase
269# can appear just before the final line finishes rendering.
270sleep 35
271
272# Codex uses Ctrl+C for a clean TUI exit; unlike Claude Code, `/exit` is not
273# a terminating slash command in this version.
274send "\003"
275expect eof
276EXPECT_DRIVER
2772
2782 cat > "$DRIVER" <<DRIVER_SCRIPT
279#!/usr/bin/env bash
280set -euo pipefail
281cd "\$REPO_ROOT"
282export PATH="\${SHIM_DIR}:\${PATH}"
283export COLUMNS="\${COLS}" LINES="\${ROWS}"
284
285# shellcheck source=demo/lib_demo.sh
286source "\${REPO_ROOT}/demo/lib_demo.sh"
287setup_colors
288
289banner "GCO Autopilot — Codex"
290narrate "A live Codex session scoped to GCO documentation tools:"
291narrate "Amazon Bedrock + required GCO MCP; no shell or companion servers."
292sleep 3
293
294echo ""
295echo " \${MAGENTA}\\\$ \${WHITE}\${BOLD}gco autopilot --engine codex --no-companions\${RESET}"
296sleep 1
297
298expect -f "$EXPECT_SCRIPT"
299
300printf '\033[2J\033[H'
301banner "GCO Autopilot — Codex"
302spacer
303highlight "A real session: Codex used only GCO's approved documentation tools."
304narrate "Default Autopilot can include companions; this recording is least-privilege."
305narrate "Get started: gco autopilot --engine codex"
306sleep 4
307DRIVER_SCRIPT
3086elif [ "$DEMO_MODE" = "live" ]; then
3092 # A real interactive session, driven end-to-end: expect(1) spawns the
3102 # actual `gco autopilot` TUI, types a question with human-ish pacing,
3112 # approves the GCO MCP tool-permission dialog on camera (the security
3122 # model is part of the demo), waits for the grounded answer, and exits
3132 # with /exit. Timing-based matches keep it robust to cosmetic TUI
3142 # changes; the post-recording check below verifies the answer actually
3152 # landed before the GIF is rendered.
3168 EXPECT_SCRIPT="$(mktemp)"
3174 cat > "$EXPECT_SCRIPT" <<'EXPECT_DRIVER'
318#!/usr/bin/expect -f
319set timeout 300
320set stty_init "rows 30 columns 110"
321# Human-ish typing: avg 80ms/char, 400ms max — visible but not sluggish.
322set send_human {0.08 0.12 1 0.02 0.4}
323
324# Type with Expect's humanized per-character timing. Claude Code's native TUI
325# processes terminal key events individually; word-sized writes can be discarded
326# while its keyboard protocol is active.
327proc type_words {text} {
328 send -h -- "$text"
329}
330
331# Claude Code 2.1.235 enables CSI-u keyboard reporting after startup. A raw
332# carriage return is text input in that mode; CSI 13 u is the physical Enter
333# key. The resume prompt appears before CSI-u is enabled and still uses \r.
334proc press_enter {} {
335 send -- "\033\[13u"
336}
337
338# The GCO MCP server's tools are pre-approved for the session with
339# claude's own --allowedTools flag (through autopilot's passthrough), so
340# the recording is deterministic — no version-specific permission-dialog
341# text to script against. The driver deliberately displays plain
342# `gco autopilot`: the flag is recording plumbing, not part of the user
343# journey — an interactive user running the plain command gets the
344# identical session, with claude's ordinary one-click permission prompt
345# standing in for the pre-approval.
346# log_user is toggled off around spawn so expect's own echo of the spawn
347# line doesn't appear in the recording (the driver already printed the
348# pretty prompt line).
349log_user 0
350spawn gco autopilot -- --allowedTools mcp__gco
351log_user 1
352
353# Autopilot's own resume prompt (when this workspace has previous
354# sessions), first-run dialogs if any (workspace trust, theme picker),
355# then wait for the input prompt. Every match keeps consuming until the
356# composer is up; a quiet timeout just falls through to the settle sleep.
357expect {
358 -re {Resume your previous Claude Code session} { sleep 2; send "n\r"; exp_continue }
359 -nocase -re {trust the files|do you trust|project you created or one you trust|yes, i trust this folder|enter.*confirm} { press_enter; exp_continue }
360 -nocase -re {choose the text style|select theme} { press_enter; exp_continue }
361 -re {Welcome|\? for shortcuts|Try "} {}
362 timeout {}
363}
364sleep 4
365
366type_words "Which gco command submits a job via SQS, and why is that recommended? Check the GCO MCP docs (no shell commands). Answer in exactly two short lines."
367sleep 1
368press_enter
369
370# Wait for the grounded answer itself (it inevitably names submit-sqs),
371# then let the complete two-line answer finish rendering. Recorded idle is
372# capped, so the generous hold improves reliability without bloating the GIF.
373expect {
374 -nocase -re {do you want to (proceed|allow)|allow this tool} { sleep 2; send "2"; press_enter; exp_continue }
375 -timeout 240 -re {submit-sqs} {}
376 timeout {}
377}
378sleep 60
379
380send -- "/exit"
381press_enter
382expect eof
383EXPECT_DRIVER
384
3854 cat > "$DRIVER" <<DRIVER_SCRIPT
386#!/usr/bin/env bash
387set -euo pipefail
388cd "\$REPO_ROOT"
389export PATH="\${SHIM_DIR}:\${PATH}"
390# tput cols runs inside command substitutions in lib_demo.sh, where stdout
391# is a pipe rather than the recording PTY, so it falls back to 80 unless
392# COLUMNS is exported. Without this the banner renders 80 wide on a
393# ${COLS}-column recording and sits awkwardly off-center.
394export COLUMNS="\${COLS}" LINES="\${ROWS}"
395
396# shellcheck source=demo/lib_demo.sh
397source "\${REPO_ROOT}/demo/lib_demo.sh"
398setup_colors
399
400banner "GCO Autopilot"
401narrate "One command turns your terminal into a working Claude Code setup:"
402narrate "Claude Code on Amazon Bedrock + the GCO MCP server + companion MCPs."
403sleep 3
404
405echo ""
406echo " \${MAGENTA}\\\$ \${WHITE}\${BOLD}gco autopilot\${RESET}"
407sleep 1
408
409expect -f "$EXPECT_SCRIPT"
410
411# The TUI leaves residual chrome behind on exit; give the outro its own
412# clean screen instead of printing into the leftovers.
413printf '\033[2J\033[H'
414banner "GCO Autopilot"
415spacer
416highlight "A real session: the model grounded its answer in GCO's MCP server."
417narrate "Sessions resume next launch; import your own skills with --skills."
418narrate "Get started: gco autopilot"
419sleep 4
420DRIVER_SCRIPT
4212elif [ "$DEMO_ENGINE" = "codex" ]; then
4224 cat > "$DRIVER" <<'DRIVER_SCRIPT'
423#!/usr/bin/env bash
424set -euo pipefail
425cd "$REPO_ROOT"
426export PATH="${SHIM_DIR}:${PATH}"
427export COLUMNS="${COLS}" LINES="${ROWS}"
428
429# shellcheck source=demo/lib_demo.sh
430source "${REPO_ROOT}/demo/lib_demo.sh"
431setup_colors
432
433banner "GCO Autopilot — Codex"
434narrate "Choose Codex without giving up GCO's one-command setup:"
435narrate "OpenAI Codex + the GCO MCP server + companion MCPs on Amazon Bedrock."
436sleep 3
437
438run_cmd "gco autopilot --engine codex --dry-run"
439sleep 5
440
441spacer
442highlight "Launch it for real with: gco autopilot --engine codex"
443narrate "The exact Codex pin and isolated CODEX_HOME persist in gco-dev."
444sleep 4
445DRIVER_SCRIPT
4464else
4474 cat > "$DRIVER" <<'DRIVER_SCRIPT'
448#!/usr/bin/env bash
449set -euo pipefail
450cd "$REPO_ROOT"
451export PATH="${SHIM_DIR}:${PATH}"
452# See the live driver: COLUMNS keeps tput-in-substitution honest so the
453# banner spans the full recording width.
454export COLUMNS="${COLS}" LINES="${ROWS}"
455
456# shellcheck source=demo/lib_demo.sh
457source "${REPO_ROOT}/demo/lib_demo.sh"
458setup_colors
459
460banner "GCO Autopilot"
461narrate "One command from a plain terminal to a working Claude Code setup:"
462narrate "Claude Code + the GCO MCP server + the recommended companion MCPs,"
463narrate "on Amazon Bedrock with GCO's default Claude Code model."
464sleep 3
465
466run_cmd "gco autopilot --dry-run"
467sleep 4
468
469spacer
470highlight "That's the whole setup. Launch it for real with: gco autopilot"
471narrate "Missing Claude Code? Autopilot offers the exact pinned install first."
472sleep 3
473DRIVER_SCRIPT
474fi
4758chmod +x "$DRIVER"
476
477# ── Record ───────────────────────────────────────────────────────────────────
478
4798echo "Recording autopilot demo (${COLS}x${ROWS})..."
4808echo "Output: ${CAST_FILE}"
4818echo ""
482
4838rm -f "$CAST_FILE"
484
485# --idle-time-limit caps recorded pauses (model thinking time in live mode)
486# so the GIF stays short without editing the cast by hand.
4878export REPO_ROOT SHIM_DIR COLS ROWS
4888asciinema rec \
489 --return \
490 --cols "$COLS" \
491 --rows "$ROWS" \
492 --idle-time-limit 1.5 \
493 --overwrite \
494 --command "bash --norc --noprofile $DRIVER" \
495 "$CAST_FILE"
496
4977echo ""
4987echo "✓ Recording saved: ${CAST_FILE}"
499
500# In live mode, prove the answer actually landed before rendering: the
501# TUI drive is timing-based, so a slow model or a changed dialog could
502# produce a cast that cuts off early. Fail loudly instead of publishing it.
5037if [ "$DEMO_MODE" = "live" ]; then
504 # TUI redraws interleave ANSI escapes and can split words across output
505 # events, so checks join the rendered stream and normalize to alphanumerics.
506 # Codex has a stricter contract: successful calls to both approved GCO docs
507 # tools, no shell/companions/prompts, and no live credential values.
5085 if python3 - "$CAST_FILE" "$DEMO_ENGINE" <<'PYEOF'
509import json
510import os
511import re
512import sys
513from pathlib import Path
514
515documents = []
516stream = []
517for line in Path(sys.argv[1]).read_text(encoding="utf-8").splitlines():
518 if not line.strip():
519 continue
520 doc = json.loads(line)
521 documents.append(doc)
522 if isinstance(doc, list) and len(doc) >= 3 and doc[1] == "o":
523 stream.append(doc[2])
524exit_events = [
525 doc
526 for doc in documents
527 if isinstance(doc, list) and len(doc) >= 3 and doc[1] == "x"
528]
529header_version = documents[0].get("version") if isinstance(documents[0], dict) else None
530valid_exit = header_version != 3 or (
531 bool(exit_events) and str(exit_events[-1][2]) == "0"
532)
533joined = "".join(stream)
534plain = re.sub(
535 r"\x1b\[[0-9;?]*[a-zA-Z]|\x1b\][^\x07\x1b]*(\x07|\x1b\\\\)|\x1b[P^_].*?\x1b\\\\|\x1b.",
536 "",
537 joined,
538)
539normalized = re.sub(r"[^a-z0-9]", "", plain.lower())
540required = ["submitsqs"]
541forbidden = []
542if sys.argv[2] == "codex":
543 required.extend(("calledgcofinddocs", "calledgcoreadresource"))
544 forbidden.extend(
545 (
546 "callingshell",
547 "calledshell",
548 "trustthecontents",
549 "trustthefiles",
550 "doyoutrust",
551 "doyouwanttoproceed",
552 "doyouwanttoallow",
553 "allowthistool",
554 "awsdocs",
555 "awspricing",
556 "ddgsearch",
557 "deepwiki",
558 "filesystem",
559 "innermonologue",
560 "mcptasks",
561 "memorymcp",
562 "playwright",
563 "sequentialthinking",
564 )
565 )
566credential_names = (
567 "AWS_ACCESS_KEY_ID",
568 "AWS_SECRET_ACCESS_KEY",
569 "AWS_SESSION_TOKEN",
570 "AWS_WEB_IDENTITY_TOKEN_FILE",
571)
572credential_leak = any(
573 value and len(value) >= 8 and value in joined
574 for name in credential_names
575 if (value := os.environ.get(name))
576)
577raise SystemExit(
578 0
579 if valid_exit
580 and all(marker in normalized for marker in required)
581 and not any(marker in normalized for marker in forbidden)
582 and not credential_leak
583 else 1
584)
585PYEOF
5865 then
5872 if [ "$DEMO_ENGINE" = "codex" ]; then
5881 echo "✓ Live Codex recording verified (GCO docs tools only; no credentials/prompts)"
5895 else
5901 echo "✓ Live answer verified in the recording (mentions submit-sqs)"
5915 fi
5925 else
5933 echo "✗ The recording failed its required answer/tool/security contract." >&2
5943 echo " The session may have stalled, used another tool, prompted, or exposed credentials." >&2
5953 exit 1
5965 fi
5975fi
5985
5995# ── Sanitize and verify ─────────────────────────────────────────────────────
6005
6014sanitize_cast "$CAST_FILE"
6024verify_cast_sanitized "$CAST_FILE"
6034echo "✓ Cast sanitized and verified (AWS account IDs → 000000000000)"
6045
6054strip_emoji_from_cast "$CAST_FILE"
6064echo "✓ Tofu-triggering codepoints stripped"
6075
6085# ── Strip terminal query/response artifacts and TUI tofu glyphs ─────────────
6095# Two Claude-Code-specific cleanups on top of lib_demo.sh's shared passes:
6105#
6115# 1. The TUI probes the terminal (focus tracking, OSC 11 background color,
6125# device attributes, XTVERSION), and pieces of those query/response
6135# exchanges land in the recorded output stream. agg's renderer doesn't
6145# understand them and paints fragments like ``^[[O`` or ``^[]11;rgb:...``
6155# literally. They carry no visual content, so they are removed outright.
6165#
6175# 2. The TUI emits three codepoints Menlo has no glyph for, and agg's
6185# first-family-wins renderer paints them as tofu boxes (same root cause
6195# strip_emoji_from_cast documents). Verified against Menlo.ttc's cmap:
6205# ⏺ U+23FA BLACK CIRCLE FOR RECORD → ● U+25CF (in Menlo, same intent)
6215# ⏸ U+23F8 DOUBLE VERTICAL BAR → ║ U+2551 (in Menlo, same width)
6225# ⎿ U+23BF DENTISTRY SYMBOL ... → └ U+2514 (in Menlo, same elbow)
6235# Everything else the TUI uses (box drawing, quadrant blocks, the
6245# spinner asterisks ✻✶✳✢✽, ❯, arrows) is covered by Menlo.
6255python3 - "$CAST_FILE" <<'PYEOF'
626import json
627import re
628import sys
629from pathlib import Path
630
631ARTIFACTS = re.compile(
632 r"\x1b\[[IO]" # focus in/out events
633 r"|\x1b\]1[01];[^\x07\x1b]*(?:\x07|\x1b\\)" # OSC 10/11 color query/response
634 r"|\x1b\[\?\d+(?:;\d+)*c" # device-attribute responses
635 r"|\x1b\[>\d+(?:;\d+)*c" # secondary DA responses
636 r"|\x1bP>\|[^\x1b]*\x1b\\" # XTVERSION response
637)
638
639# Single-codepoint substitutions (str.translate); multi-char targets are
640# fine as translate values.
641TUI_TOFU = {
642 0x23FA: "\u25cf", # ⏺ → ●
643 0x23F8: "\u2551", # ⏸ → ║
644 0x23BF: "\u2514", # ⎿ → └
645}
646
647path = Path(sys.argv[1])
648documents = []
649for line in path.read_text(encoding="utf-8").splitlines():
650 if not line.strip():
651 continue
652 documents.append(json.loads(line))
653
654for document in documents:
655 if isinstance(document, list) and len(document) >= 3 and document[1] == "o":
656 document[2] = ARTIFACTS.sub("", document[2]).translate(TUI_TOFU)
657
658# Static GIF previews show frame zero without advancing the animation. Make the
659# first rendered frame the recorder banner rather than the empty PTY that
660# precedes it. Asciicast v2 uses absolute timestamps; v3 uses per-event delays.
661events = [
662 document
663 for document in documents
664 if isinstance(document, list) and len(document) >= 3
665]
666banner_event = next(
667 (
668 event
669 for event in events
670 if event[1] == "o"
671 and isinstance(event[2], str)
672 and "GCO Autopilot" in event[2]
673 ),
674 None,
675)
676header_version = documents[0].get("version") if isinstance(documents[0], dict) else None
677if banner_event is not None and header_version == 2:
678 banner_time = float(banner_event[0])
679 for event in events:
680 event[0] = round(max(0.0, float(event[0]) - banner_time), 6)
681elif banner_event is not None and header_version == 3:
682 for event in events:
683 event[0] = 0
684 if event is banner_event:
685 break
686
687path.write_text(
688 "\n".join(json.dumps(d, ensure_ascii=False, separators=(",", ":")) for d in documents)
689 + "\n",
690 encoding="utf-8",
691)
692PYEOF
6934echo "✓ Terminal query/response artifacts stripped, TUI tofu glyphs substituted"
694
695# ── Convert to GIF ──────────────────────────────────────────────────────────
696
6974if [ "${SKIP_GIF:-}" != "1" ]; then
6983 echo ""
6993 echo "Converting to GIF (speed=${SPEED}x, theme=${THEME})..."
7003 render_gif "$CAST_FILE" "$GIF_FILE" "$SPEED" "$THEME" "$COLS" "$ROWS"
7013 echo "✓ GIF saved: ${GIF_FILE}"
70212 GIF_SIZE=$(du -h "$GIF_FILE" | cut -f1); echo " Size: $GIF_SIZE"
703fi
704
705# ── Summary ──────────────────────────────────────────────────────────────────
706
7074echo ""
7084echo "=== Done ==="
7094echo ""
7104echo "Files:"
7114echo " ${CAST_FILE}"
7127[ "${SKIP_GIF:-}" != "1" ] && echo " ${GIF_FILE}"
7134echo ""
7144echo "Any new GIF must satisfy the reviewed policy in"
7154echo ".github/scripts/validate_demo_gifs.py (size/dimensions/frames)."
7164echo ""
7174echo "Embed in README:"
7188GIF_BASENAME="$(basename "$GIF_FILE")"
7194echo " ![GCO Autopilot](demo/${GIF_BASENAME})"