← all scripts

demo/record_autopilot.sh

195 of 195 statements covered (100.00%).

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

111#!/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, ``codex`` or ``opencode``) 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.
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# OpenCode: demo/autopilot-opencode.cast + demo/autopilot-opencode.gif
26#
27# Prerequisites:
28# - asciinema: brew install asciinema (or pip install asciinema)
29# - agg: brew install agg (or cargo install agg)
30# - python3 with the repo's dependencies importable (dev container, or
31# an environment where `python3 -m cli.main --help` works)
32#
33# Usage:
34# bash demo/record_autopilot.sh
35#
36# Options (via environment variables):
37# DEMO_ENGINE=claude-code "claude-code" (default), "codex" or "opencode"
38# DEMO_MODE=live "live" (real selected-engine Bedrock session, default)
39# or "plan" (credential-free engine launch plan)
40# DEMO_COLS=110 Terminal width for recording (default: 110)
41# DEMO_ROWS=30 Terminal height for recording (default: 30)
42# DEMO_SPEED=1.6 Playback speed multiplier for GIF (default: 1.6)
43# DEMO_THEME=monokai agg color theme (default: monokai)
44# DEMO_FONT_FAMILY agg font fallback chain (default: see lib_demo.sh)
45# SKIP_GIF=1 Only produce the .cast file, skip GIF conversion
46# SKIP_SANITIZE=1 Skip AWS-account-ID redaction (debugging only)
47# SKIP_EMOJI_STRIP=1 Skip emoji substitution (debugging only)
48#
49# The recorded .cast is post-processed exactly like the other demo
50# recordings before the GIF is rendered: sanitize_cast redacts anything
51# shaped like an AWS account ID or access-key ID (verified afterwards by
52# verify_cast_sanitized), and strip_emoji_from_cast rewrites codepoints
53# agg's text engine can't render. See demo/lib_demo.sh.
54# ─────────────────────────────────────────────────────────────────────────────
55
5616set -euo pipefail
57
58# ── Configuration ────────────────────────────────────────────────────────────
59
6064SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
61# The checkout being recorded: normally the one this script lives in. The BATS
62# suite points GCO_RECORDING_REPO_ROOT at a disposable fixture repository so
63# the tracked recorder runs in place against it; left unset, every path below
64# is the same as before the override existed.
6548REPO_ROOT="$(cd "${GCO_RECORDING_REPO_ROOT:-$SCRIPT_DIR/..}" && pwd)"
6616DEMO_DIR="${REPO_ROOT}/demo"
67
68# shellcheck source=demo/lib_demo.sh
6916source "${SCRIPT_DIR}/lib_demo.sh"
7016setup_colors
71
7216DEMO_ENGINE="${DEMO_ENGINE:-claude-code}"
7316DEMO_MODE="${DEMO_MODE:-live}"
74
7516case "$DEMO_ENGINE" in
76 claude-code)
777 CAST_FILE="${DEMO_DIR}/autopilot-claude-code.cast"
787 GIF_FILE="${DEMO_DIR}/autopilot-claude-code.gif"
79 ;;
80 codex)
814 CAST_FILE="${DEMO_DIR}/autopilot-codex.cast"
824 GIF_FILE="${DEMO_DIR}/autopilot-codex.gif"
83 ;;
84 opencode)
854 CAST_FILE="${DEMO_DIR}/autopilot-opencode.cast"
864 GIF_FILE="${DEMO_DIR}/autopilot-opencode.gif"
87 ;;
882 *) echo "error: DEMO_ENGINE must be 'claude-code', 'codex' or 'opencode', got '$DEMO_ENGINE'" >&2; exit 1 ;;
89esac
90
9115COLS="${DEMO_COLS:-110}"
9215ROWS="${DEMO_ROWS:-30}"
9315SPEED="${DEMO_SPEED:-1.6}"
9415THEME="${DEMO_THEME:-monokai}"
95
9615case "$DEMO_MODE" in
9714 live|plan) : ;;
982 *) echo "error: DEMO_MODE must be 'live' or 'plan', got '$DEMO_MODE'" >&2; exit 1 ;;
99esac
100
101# ── Preflight Checks ────────────────────────────────────────────────────────
102
10314PREFLIGHT_FAIL=0
104
105preflight_pass() {
10686 echo " ${GREEN}${BOLD}✓${RESET} $1"
107}
108
109preflight_fail() {
11011 echo " ${RED}${BOLD}✗${RESET} $1"
11111 echo " ${DIM}Fix: $2${RESET}"
11211 PREFLIGHT_FAIL=$((PREFLIGHT_FAIL + 1))
113}
114
11514echo "=== GCO Autopilot Demo Recorder (${DEMO_ENGINE}, ${DEMO_MODE}) ==="
11614echo ""
117
11814if command -v asciinema &>/dev/null; then
11939 preflight_pass "asciinema installed ($(asciinema --version 2>&1 | head -1))"
120else
1211 preflight_fail "asciinema not installed" \
122 "brew install asciinema (macOS) or pip install asciinema (Linux)"
123fi
124
12514if [ "${SKIP_GIF:-}" != "1" ]; then
12612 if command -v agg &>/dev/null; then
12733 preflight_pass "agg installed ($(agg --version 2>&1 | head -1))"
128 else
1291 preflight_fail "agg not installed" \
130 "brew install agg (macOS) or cargo install agg (Rust), or set SKIP_GIF=1"
131 fi
132fi
133
13428if (cd "$REPO_ROOT" && python3 -m cli.main --version &>/dev/null); then
13513 preflight_pass "GCO CLI importable (python3 -m cli.main)"
136else
1371 preflight_fail "GCO CLI not importable from this python3" \
138 "Run inside the dev container, or install the repo's deps (pip install -e .)"
139fi
140
14128if [ -f "${SCRIPT_DIR}/lib_demo.sh" ] && [ -f "${REPO_ROOT}/cdk.json" ]; then
14213 preflight_pass "Repository layout looks right"
143else
1441 preflight_fail "Repository layout unexpected" "Run from a full GCO checkout"
145fi
146
14714if [ "$DEMO_MODE" = "live" ]; then
14811 case "$DEMO_ENGINE" in
149 codex)
1503 ENGINE_BINARY="codex"
1513 ENGINE_LABEL="Codex"
1523 INSTALL_HINT="gco autopilot --engine codex -y"
153 ;;
154 opencode)
1553 ENGINE_BINARY="opencode"
1563 ENGINE_LABEL="OpenCode"
1573 INSTALL_HINT="gco autopilot --engine opencode -y"
158 ;;
159 *)
1605 ENGINE_BINARY="claude"
1615 ENGINE_LABEL="Claude Code"
1625 INSTALL_HINT="gco autopilot -y"
163 ;;
164 esac
16511 if command -v "$ENGINE_BINARY" &>/dev/null; then
16624 preflight_pass "$ENGINE_LABEL installed ($("$ENGINE_BINARY" --version 2>&1 | head -1))"
167 else
1683 preflight_fail "$ENGINE_LABEL not installed (live mode launches a real session)" \
169 "Run '$INSTALL_HINT' once to install the pin, or set DEMO_MODE=plan"
170 fi
171 # Only the Claude recording starts the companion servers; the Codex and
172 # OpenCode recordings run --no-companions (GCO's documentation tools only).
17311 if [ "$DEMO_ENGINE" = "claude-code" ]; then
17410 for companion_runtime in uvx npx; do
17510 if command -v "$companion_runtime" &>/dev/null; then
1768 preflight_pass "$companion_runtime installed"
177 else
1782 preflight_fail "$companion_runtime not installed (Claude live mode starts companions)" \
179 "Use gco-dev, install the missing runtime, or set DEMO_MODE=plan"
180 fi
181 done
182 fi
18311 if command -v expect &>/dev/null; then
18410 preflight_pass "expect installed (drives the interactive TUI)"
185 else
1861 preflight_fail "expect not installed (live mode scripts the TUI)" \
187 "macOS ships /usr/bin/expect; on Linux: apt install expect. Or set DEMO_MODE=plan"
188 fi
18911 if aws sts get-caller-identity &>/dev/null; then
19010 preflight_pass "AWS credentials resolve (Bedrock access is exercised by the recording)"
191 else
1921 preflight_fail "No AWS credentials (live mode makes a real Bedrock call)" \
193 "Configure credentials with Bedrock model access, or set DEMO_MODE=plan"
194 fi
195fi
196
19714if [ "$PREFLIGHT_FAIL" -gt 0 ]; then
1983 echo ""
1993 echo " ${RED}${BOLD}${PREFLIGHT_FAIL} check(s) failed. Fix the issues above before recording.${RESET}"
2003 exit 1
201fi
202
20311echo ""
204
205# ── Build the demo driver ───────────────────────────────────────────────────
206# A `gco` PATH shim keeps the on-screen command honest (`$ gco autopilot …`)
207# while guaranteeing the recording exercises this checkout's code.
208
20922SHIM_DIR="$(mktemp -d)"
21022DRIVER="$(mktemp)"
21111EXPECT_SCRIPT=""
21211trap 'rm -rf "$SHIM_DIR" "$DRIVER" ${EXPECT_SCRIPT:+"$EXPECT_SCRIPT"}' EXIT
213
21411cat > "${SHIM_DIR}/gco" <<'GCO_SHIM'
215#!/usr/bin/env bash
216exec python3 -m cli.main "$@"
217GCO_SHIM
21811chmod +x "${SHIM_DIR}/gco"
219
22019if [ "$DEMO_MODE" = "live" ] && [ "$DEMO_ENGINE" = "codex" ]; then
221 # Mirror the Claude recording with Codex's real inline TUI. Read-only,
222 # never-approve, and run-scoped trust for this reviewed checkout are hidden
223 # recording plumbing: the prompt explicitly uses GCO MCP documentation
224 # tools, no workspace mutation is needed, and no trust setting is persisted.
2254 EXPECT_SCRIPT="$(mktemp)"
2262 cat > "$EXPECT_SCRIPT" <<'EXPECT_DRIVER'
227#!/usr/bin/expect -f
228set timeout 420
229set stty_init "rows 30 columns 110"
230
231# Codex enables CSI-u keyboard reporting; this emits a physical Enter only
232# after a positively identified composer redraw below.
233proc press_enter {} {
234 send -- "\033\[13u"
235}
236
237# The recording uses only the local GCO MCP server and exposes exactly the two
238# documentation tools needed on camera. GCO startup is required, both tools are
239# explicitly preapproved, and Codex's built-in shell is removed entirely.
240set gco_required {mcp_servers.gco.required=true}
241set gco_tools {mcp_servers.gco.enabled_tools=["find_docs","read_resource"]}
242set find_docs_approval {mcp_servers.gco.tools.find_docs.approval_mode="approve"}
243set read_resource_approval {mcp_servers.gco.tools.read_resource.approval_mode="approve"}
244set 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}
245log_user 0
246spawn 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
247log_user 1
248
249# Codex accepts the initial prompt as prefilled composer text. Monitor startup
250# for dialogs, then append one harmless space to force a current composer redraw;
251# only a positive match of that redraw is allowed to submit the turn.
252set submitted 0
253set timeout 20
254expect {
255 -nocase -re {trust the contents|trust the files|do you trust} { exit 6 }
256 -nocase -re {do you want to (proceed|allow)|allow this tool} { exit 7 }
257 -re {Working|Calling gco\.find_docs} { set submitted 1 }
258 timeout {}
259 eof { exit 3 }
260}
261if {!$submitted} {
262 send -- "."
263 set timeout 10
264 expect {
265 -re {Begin when ready.*\.} { press_enter }
266 -nocase -re {trust the contents|trust the files|do you trust} { exit 6 }
267 -nocase -re {do you want to (proceed|allow)|allow this tool} { exit 7 }
268 timeout { exit 8 }
269 eof { exit 3 }
270 }
271}
272# Wait for the explanatory final answer, not an intermediate tool query that
273# happens to mention the command. Trust and approval dialogs remain explicit
274# failures for the entire turn; never send an unqualified keypress on timeout.
275set timeout 420
276expect {
277 -nocase -re {trust the contents|trust the files|do you trust} { exit 6 }
278 -nocase -re {do you want to (proceed|allow)|allow this tool} { exit 7 }
279 -nocase -re {recommended (for production )?because|resilient production|durable, asynchronous} {}
280 timeout { exit 4 }
281 eof { exit 5 }
282}
283# Leave the complete short answer on screen before exiting. The matched phrase
284# can appear just before the final line finishes rendering.
285sleep 35
286
287# Codex uses Ctrl+C for a clean TUI exit; unlike Claude Code, `/exit` is not
288# a terminating slash command in this version.
289send "\003"
290expect eof
291EXPECT_DRIVER
2922
2932 cat > "$DRIVER" <<DRIVER_SCRIPT
294#!/usr/bin/env bash
295set -euo pipefail
296# Bash 5 enables checkwinsize by default and would overwrite the exported
297# COLUMNS with the outer terminal's width after the first external command,
298# wrapping every later banner in the COLS-wide render.
299shopt -u checkwinsize
300cd "\$REPO_ROOT"
301export PATH="\${SHIM_DIR}:\${PATH}"
302export COLUMNS="\${COLS}" LINES="\${ROWS}"
303
304# shellcheck source=demo/lib_demo.sh
305source "\${REPO_ROOT}/demo/lib_demo.sh"
306setup_colors
307
308banner "GCO Autopilot — Codex"
309narrate "A live Codex session scoped to GCO documentation tools:"
310narrate "Amazon Bedrock + required GCO MCP; no shell or companion servers."
311sleep 3
312
313echo ""
314echo " \${MAGENTA}\\\$ \${WHITE}\${BOLD}gco autopilot --engine codex --no-companions\${RESET}"
315sleep 1
316
317expect -f "$EXPECT_SCRIPT"
318
319printf '\033[2J\033[H'
320banner "GCO Autopilot — Codex"
321spacer
322highlight "A real session: Codex used only GCO's approved documentation tools."
323narrate "Default Autopilot can include companions; this recording is least-privilege."
324narrate "Get started: gco autopilot --engine codex"
325sleep 4
326DRIVER_SCRIPT
32715elif [ "$DEMO_MODE" = "live" ] && [ "$DEMO_ENGINE" = "opencode" ]; then
3282 # Mirror the Codex recording with OpenCode's real full-screen TUI. Every
3292 # built-in tool (shell, file read/edit/write, search, web, subagents,
3302 # todo, skills, the interactive question dialog) is denied through
3312 # OpenCode's own config override (OPENCODE_CONFIG_CONTENT, merged last),
3322 # so the answer can only come from GCO's MCP documentation tools. It has
3332 # to be the permission map: in OpenCode 1.18 `permission` governs the
3342 # bash/edit/read tools and a bare `tools: {bash: false}` is overridden by
3352 # the permission-derived default. That is hidden recording plumbing —
3362 # nothing is persisted, and an interactive user running the plain command
3372 # gets OpenCode's ordinary tool set with the ask-first permission floor.
3384 EXPECT_SCRIPT="$(mktemp)"
3392 cat > "$EXPECT_SCRIPT" <<'EXPECT_DRIVER'
340#!/usr/bin/expect -f
341set timeout 420
342set stty_init "rows 30 columns 110"
343
344set env(OPENCODE_CONFIG_CONTENT) {{"permission":{"bash":"deny","edit":"deny","read":"deny","glob":"deny","grep":"deny","list":"deny","webfetch":"deny","websearch":"deny","codesearch":"deny","task":"deny","todowrite":"deny","skill":"deny","question":"deny","lsp":"deny"}}}
345set 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.}
346log_user 0
347spawn gco autopilot --engine opencode --no-companions
348log_user 1
349
350# Wait for the composer (its "Ask anything…" placeholder). A permission
351# prompt at startup is a failure, never something the driver answers.
352set timeout 90
353expect {
354 -nocase -re {permission required|allow once|allow always} { exit 7 }
355 -re {Ask anything} {}
356 timeout { exit 8 }
357 eof { exit 3 }
358}
359sleep 3
360# OpenCode's composer takes the whole prompt as pasted text; a plain
361# carriage return submits it.
362send -- $prompt
363sleep 1
364send -- "\r"
365
366# Wait for the grounded answer (it inevitably names submit-sqs; a
367# find_docs query may name it first, which is fine because the quiet-wait
368# below runs until the turn is really over). A permission prompt means the
369# model reached for a tool this recording does not allow — fail rather
370# than approve.
371set timeout 420
372expect {
373 -nocase -re {permission required|allow once|allow always} { exit 7 }
374 -re {submit-sqs} {}
375 timeout { exit 4 }
376 eof { exit 5 }
377}
378# OpenCode redraws its spinner continuously while a turn is running and goes
379# completely quiet once the answer is on screen, so six seconds without
380# output is the end-of-turn signal. The deadline keeps a runaway tool loop
381# from recording forever.
382set deadline [expr {[clock seconds] + 300}]
383expect {
384 -nocase -re {permission required|allow once|allow always} { exit 7 }
385 -timeout 6 -re {.+} {
386 if {[clock seconds] < $deadline} { exp_continue }
387 exit 4
388 }
389 timeout {}
390 eof { exit 5 }
391}
392# Leave the complete two-line answer on screen before exiting.
393sleep 30
394
395# /exit is OpenCode's terminating slash command: type it, let the command
396# palette resolve it, confirm with Enter.
397send -- "/exit"
398sleep 1
399send -- "\r"
400expect eof
401EXPECT_DRIVER
4022
4032 cat > "$DRIVER" <<DRIVER_SCRIPT
404#!/usr/bin/env bash
405set -euo pipefail
406# Bash 5 enables checkwinsize by default and would overwrite the exported
407# COLUMNS with the outer terminal's width after the first external command,
408# wrapping every later banner in the COLS-wide render.
409shopt -u checkwinsize
410cd "\$REPO_ROOT"
411export PATH="\${SHIM_DIR}:\${PATH}"
412export COLUMNS="\${COLS}" LINES="\${ROWS}"
413
414# shellcheck source=demo/lib_demo.sh
415source "\${REPO_ROOT}/demo/lib_demo.sh"
416setup_colors
417
418banner "GCO Autopilot — OpenCode"
419narrate "A live OpenCode session scoped to GCO documentation tools:"
420narrate "Amazon Bedrock + the GCO MCP server; no shell or companion servers."
421sleep 3
422
423echo ""
424echo " \${MAGENTA}\\\$ \${WHITE}\${BOLD}gco autopilot --engine opencode --no-companions\${RESET}"
425sleep 1
426
427expect -f "$EXPECT_SCRIPT"
428
429printf '\033[2J\033[H'
430banner "GCO Autopilot — OpenCode"
431spacer
432highlight "A real session: OpenCode used only GCO's documentation tools."
433narrate "Default Autopilot can include companions; this recording is least-privilege."
434narrate "Get started: gco autopilot --engine opencode"
435sleep 4
436DRIVER_SCRIPT
4377elif [ "$DEMO_MODE" = "live" ]; then
4382 # A real interactive session, driven end-to-end: expect(1) spawns the
4392 # actual `gco autopilot` TUI, types a question with human-ish pacing,
4402 # approves the GCO MCP tool-permission dialog on camera (the security
4412 # model is part of the demo), waits for the grounded answer, and exits
4422 # with /exit. Timing-based matches keep it robust to cosmetic TUI
4432 # changes; the post-recording check below verifies the answer actually
4442 # landed before the GIF is rendered.
4458 EXPECT_SCRIPT="$(mktemp)"
4464 cat > "$EXPECT_SCRIPT" <<'EXPECT_DRIVER'
447#!/usr/bin/expect -f
448set timeout 300
449set stty_init "rows 30 columns 110"
450# Human-ish typing: avg 80ms/char, 400ms max — visible but not sluggish.
451set send_human {0.08 0.12 1 0.02 0.4}
452
453# Type with Expect's humanized per-character timing. Claude Code's native TUI
454# processes terminal key events individually; word-sized writes can be discarded
455# while its keyboard protocol is active.
456proc type_words {text} {
457 send -h -- "$text"
458}
459
460# Claude Code 2.1.235 enables CSI-u keyboard reporting after startup. A raw
461# carriage return is text input in that mode; CSI 13 u is the physical Enter
462# key. The resume prompt appears before CSI-u is enabled and still uses \r.
463proc press_enter {} {
464 send -- "\033\[13u"
465}
466
467# The GCO MCP server's tools are pre-approved for the session with
468# claude's own --allowedTools flag (through autopilot's passthrough), so
469# the recording is deterministic — no version-specific permission-dialog
470# text to script against. The driver deliberately displays plain
471# `gco autopilot`: the flag is recording plumbing, not part of the user
472# journey — an interactive user running the plain command gets the
473# identical session, with claude's ordinary one-click permission prompt
474# standing in for the pre-approval.
475# log_user is toggled off around spawn so expect's own echo of the spawn
476# line doesn't appear in the recording (the driver already printed the
477# pretty prompt line).
478log_user 0
479spawn gco autopilot -- --allowedTools mcp__gco
480log_user 1
481
482# Autopilot's own resume prompt (when this workspace has previous
483# sessions), first-run dialogs if any (workspace trust, theme picker),
484# then wait for the input prompt. Every match keeps consuming until the
485# composer is up; a quiet timeout just falls through to the settle sleep.
486expect {
487 -re {Resume your previous Claude Code session} { sleep 2; send "n\r"; exp_continue }
488 -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 }
489 -nocase -re {choose the text style|select theme} { press_enter; exp_continue }
490 -re {Welcome|\? for shortcuts|Try "} {}
491 timeout {}
492}
493sleep 4
494
495type_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."
496sleep 1
497press_enter
498
499# Wait for the grounded answer itself (it inevitably names submit-sqs),
500# then let the complete two-line answer finish rendering. Recorded idle is
501# capped, so the generous hold improves reliability without bloating the GIF.
502expect {
503 -nocase -re {do you want to (proceed|allow)|allow this tool} { sleep 2; send "2"; press_enter; exp_continue }
504 -timeout 240 -re {submit-sqs} {}
505 timeout {}
506}
507sleep 60
508
509send -- "/exit"
510press_enter
511expect eof
512EXPECT_DRIVER
513
5144 cat > "$DRIVER" <<DRIVER_SCRIPT
515#!/usr/bin/env bash
516set -euo pipefail
517# Bash 5 enables checkwinsize by default and would overwrite the exported
518# COLUMNS with the outer terminal's width after the first external command,
519# wrapping every later banner in the COLS-wide render.
520shopt -u checkwinsize
521cd "\$REPO_ROOT"
522export PATH="\${SHIM_DIR}:\${PATH}"
523# tput cols runs inside command substitutions in lib_demo.sh, where stdout
524# is a pipe rather than the recording PTY, so it falls back to 80 unless
525# COLUMNS is exported. Without this the banner renders 80 wide on a
526# ${COLS}-column recording and sits awkwardly off-center.
527export COLUMNS="\${COLS}" LINES="\${ROWS}"
528
529# shellcheck source=demo/lib_demo.sh
530source "\${REPO_ROOT}/demo/lib_demo.sh"
531setup_colors
532
533banner "GCO Autopilot"
534narrate "One command turns your terminal into a working Claude Code setup:"
535narrate "Claude Code on Amazon Bedrock + the GCO MCP server + companion MCPs."
536sleep 3
537
538echo ""
539echo " \${MAGENTA}\\\$ \${WHITE}\${BOLD}gco autopilot\${RESET}"
540sleep 1
541
542expect -f "$EXPECT_SCRIPT"
543
544# The TUI leaves residual chrome behind on exit; give the outro its own
545# clean screen instead of printing into the leftovers.
546printf '\033[2J\033[H'
547banner "GCO Autopilot"
548spacer
549highlight "A real session: the model grounded its answer in GCO's MCP server."
550narrate "Sessions resume next launch; import your own skills with --skills."
551narrate "Get started: gco autopilot"
552sleep 4
553DRIVER_SCRIPT
5543elif [ "$DEMO_ENGINE" = "codex" ]; then
5554 cat > "$DRIVER" <<'DRIVER_SCRIPT'
556#!/usr/bin/env bash
557set -euo pipefail
558# Bash 5 enables checkwinsize by default and would overwrite the exported
559# COLUMNS with the outer terminal's width after the first external command,
560# wrapping every later banner in the COLS-wide render.
561shopt -u checkwinsize
562cd "$REPO_ROOT"
563export PATH="${SHIM_DIR}:${PATH}"
564export COLUMNS="${COLS}" LINES="${ROWS}"
565
566# shellcheck source=demo/lib_demo.sh
567source "${REPO_ROOT}/demo/lib_demo.sh"
568setup_colors
569
570banner "GCO Autopilot — Codex"
571narrate "Choose Codex without giving up GCO's one-command setup:"
572narrate "OpenAI Codex + the GCO MCP server + companion MCPs on Amazon Bedrock."
573sleep 3
574
575run_cmd "gco autopilot --engine codex --dry-run"
576sleep 5
577
578spacer
579highlight "Launch it for real with: gco autopilot --engine codex"
580narrate "The exact Codex pin and isolated CODEX_HOME persist in gco-dev."
581sleep 4
582DRIVER_SCRIPT
5832elif [ "$DEMO_ENGINE" = "opencode" ]; then
5844 cat > "$DRIVER" <<'DRIVER_SCRIPT'
585#!/usr/bin/env bash
586set -euo pipefail
587# Bash 5 enables checkwinsize by default and would overwrite the exported
588# COLUMNS with the outer terminal's width after the first external command,
589# wrapping every later banner in the COLS-wide render.
590shopt -u checkwinsize
591cd "$REPO_ROOT"
592export PATH="${SHIM_DIR}:${PATH}"
593export COLUMNS="${COLS}" LINES="${ROWS}"
594
595# shellcheck source=demo/lib_demo.sh
596source "${REPO_ROOT}/demo/lib_demo.sh"
597setup_colors
598
599banner "GCO Autopilot — OpenCode"
600narrate "Choose OpenCode without giving up GCO's one-command setup:"
601narrate "OpenCode + the GCO MCP server + companion MCPs on Amazon Bedrock."
602sleep 3
603
604run_cmd "gco autopilot --engine opencode --dry-run"
605sleep 5
606
607spacer
608highlight "Launch it for real with: gco autopilot --engine opencode"
609narrate "The exact OpenCode pin and isolated OPENCODE_CONFIG persist in gco-dev."
610sleep 4
611DRIVER_SCRIPT
6124else
6134 cat > "$DRIVER" <<'DRIVER_SCRIPT'
614#!/usr/bin/env bash
615set -euo pipefail
616# Bash 5 enables checkwinsize by default and would overwrite the exported
617# COLUMNS with the outer terminal's width after the first external command,
618# wrapping every later banner in the COLS-wide render.
619shopt -u checkwinsize
620cd "$REPO_ROOT"
621export PATH="${SHIM_DIR}:${PATH}"
622# See the live driver: COLUMNS keeps tput-in-substitution honest so the
623# banner spans the full recording width.
624export COLUMNS="${COLS}" LINES="${ROWS}"
625
626# shellcheck source=demo/lib_demo.sh
627source "${REPO_ROOT}/demo/lib_demo.sh"
628setup_colors
629
630banner "GCO Autopilot"
631narrate "One command from a plain terminal to a working Claude Code setup:"
632narrate "Claude Code + the GCO MCP server + the recommended companion MCPs,"
633narrate "on Amazon Bedrock with GCO's default Claude Code model."
634sleep 3
635
636run_cmd "gco autopilot --dry-run"
637sleep 4
638
639spacer
640highlight "That's the whole setup. Launch it for real with: gco autopilot"
641narrate "Missing Claude Code? Autopilot offers the exact pinned install first."
642sleep 3
643DRIVER_SCRIPT
644fi
64511chmod +x "$DRIVER"
646
647# ── Record ───────────────────────────────────────────────────────────────────
648
64911echo "Recording autopilot demo (${COLS}x${ROWS})..."
65011echo "Output: ${CAST_FILE}"
65111echo ""
652
65311rm -f "$CAST_FILE"
654
655# --idle-time-limit caps recorded pauses (model thinking time in live mode)
656# so the GIF stays short without editing the cast by hand.
65711export REPO_ROOT SHIM_DIR COLS ROWS
65811asciinema rec \
659 --return \
660 --cols "$COLS" \
661 --rows "$ROWS" \
662 --idle-time-limit 1.5 \
663 --overwrite \
664 --command "bash --norc --noprofile $DRIVER" \
665 "$CAST_FILE"
666
66710echo ""
66810echo "✓ Recording saved: ${CAST_FILE}"
669
670# In live mode, prove the answer actually landed before rendering: the
671# TUI drive is timing-based, so a slow model or a changed dialog could
672# produce a cast that cuts off early. Fail loudly instead of publishing it.
67310if [ "$DEMO_MODE" = "live" ]; then
674 # TUI redraws interleave ANSI escapes and can split words across output
675 # events, so checks join the rendered stream and normalize to alphanumerics.
676 # Codex and OpenCode have a stricter contract: successful calls to both
677 # approved GCO docs tools, no shell/companions/prompts, and no live
678 # credential values.
6797 if python3 - "$CAST_FILE" "$DEMO_ENGINE" <<'PYEOF'
680import json
681import os
682import re
683import sys
684from pathlib import Path
685
686documents = []
687stream = []
688for line in Path(sys.argv[1]).read_text(encoding="utf-8").splitlines():
689 if not line.strip():
690 continue
691 doc = json.loads(line)
692 documents.append(doc)
693 if isinstance(doc, list) and len(doc) >= 3 and doc[1] == "o":
694 stream.append(doc[2])
695exit_events = [
696 doc
697 for doc in documents
698 if isinstance(doc, list) and len(doc) >= 3 and doc[1] == "x"
699]
700header_version = documents[0].get("version") if isinstance(documents[0], dict) else None
701valid_exit = header_version != 3 or (
702 bool(exit_events) and str(exit_events[-1][2]) == "0"
703)
704joined = "".join(stream)
705plain = re.sub(
706 r"\x1b\[[0-9;?]*[a-zA-Z]|\x1b\][^\x07\x1b]*(\x07|\x1b\\\\)|\x1b[P^_].*?\x1b\\\\|\x1b.",
707 "",
708 joined,
709)
710normalized = re.sub(r"[^a-z0-9]", "", plain.lower())
711required = ["submitsqs"]
712forbidden = []
713companions = (
714 "awsdocs",
715 "awspricing",
716 "ddgsearch",
717 "deepwiki",
718 "filesystem",
719 "innermonologue",
720 "mcptasks",
721 "memorymcp",
722 "playwright",
723 "sequentialthinking",
724)
725if sys.argv[2] == "codex":
726 required.extend(("calledgcofinddocs", "calledgcoreadresource"))
727 forbidden.extend(
728 (
729 "callingshell",
730 "calledshell",
731 "trustthecontents",
732 "trustthefiles",
733 "doyoutrust",
734 "doyouwanttoproceed",
735 "doyouwanttoallow",
736 "allowthistool",
737 *companions,
738 )
739 )
740elif sys.argv[2] == "opencode":
741 # OpenCode renders MCP tool calls as <server>_<tool>; the prompt text
742 # itself only ever says "GCO MCP find_docs", so these spellings are
743 # evidence of calls, not of the question.
744 required.extend(("gcofinddocs", "gcoreadresource"))
745 forbidden.extend(
746 (
747 "permissionrequired",
748 "allowonce",
749 "allowalways",
750 *companions,
751 )
752 )
753credential_names = (
754 "AWS_ACCESS_KEY_ID",
755 "AWS_SECRET_ACCESS_KEY",
756 "AWS_SESSION_TOKEN",
757 "AWS_WEB_IDENTITY_TOKEN_FILE",
758)
759credential_leak = any(
760 value and len(value) >= 8 and value in joined
761 for name in credential_names
762 if (value := os.environ.get(name))
763)
764raise SystemExit(
765 0
766 if valid_exit
767 and all(marker in normalized for marker in required)
768 and not any(marker in normalized for marker in forbidden)
769 and not credential_leak
770 else 1
771)
772PYEOF
7737 then
7743 case "$DEMO_ENGINE" in
7751 codex) echo "✓ Live Codex recording verified (GCO docs tools only; no credentials/prompts)" ;;
7761 opencode) echo "✓ Live OpenCode recording verified (GCO docs tools only; no credentials/prompts)" ;;
7771 *) echo "✓ Live answer verified in the recording (mentions submit-sqs)" ;;
7787 esac
7797 else
7804 echo "✗ The recording failed its required answer/tool/security contract." >&2
7814 echo " The session may have stalled, used another tool, prompted, or exposed credentials." >&2
7824 exit 1
7837 fi
7847fi
7857
7867# ── Sanitize and verify ─────────────────────────────────────────────────────
7877
7886sanitize_cast "$CAST_FILE"
7896verify_cast_sanitized "$CAST_FILE"
7906echo "✓ Cast sanitized and verified (AWS account IDs → 000000000000)"
7917
7926strip_emoji_from_cast "$CAST_FILE"
7936echo "✓ Tofu-triggering codepoints stripped"
7947
7957# ── Strip terminal query/response artifacts and TUI tofu glyphs ─────────────
7967# Two engine-TUI cleanups on top of lib_demo.sh's shared passes:
7977#
7987# 1. The TUIs probe the terminal (focus tracking, OSC 11 background color,
7997# device attributes, XTVERSION), and pieces of those query/response
8007# exchanges land in the recorded output stream. agg's renderer doesn't
8017# understand them and paints fragments like ``^[[O`` or ``^[]11;rgb:...``
8027# literally. They carry no visual content, so they are removed outright.
8037#
8047# 2. The TUIs emit codepoints Menlo has no glyph for, and agg's
8057# first-family-wins renderer paints them as tofu boxes (same root cause
8067# strip_emoji_from_cast documents). Verified against Menlo.ttc's cmap:
8077# Claude Code
8087# ⏺ U+23FA BLACK CIRCLE FOR RECORD → ● U+25CF (in Menlo, same intent)
8097# ⏸ U+23F8 DOUBLE VERTICAL BAR → ║ U+2551 (in Menlo, same width)
8107# ⎿ U+23BF DENTISTRY SYMBOL ... → └ U+2514 (in Menlo, same elbow)
8117# OpenCode
8127# ⬝ U+2B1D BLACK VERY SMALL SQUARE → ▪ U+25AA (in Menlo; the idle cell
8137# of its ■/⬝ block spinner)
8147# ⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏ Braille spinner → | / - \ (Menlo has no Braille
8157# block at all; the ASCII cycle
8167# keeps the rotation legible)
8177# Everything else the TUIs use (box drawing, quadrant/half blocks, ■ ●
8187# ⚙ ⊙, the Claude spinner asterisks ✻✶✳✢✽, ❯, arrows) is covered by Menlo.
8197python3 - "$CAST_FILE" <<'PYEOF'
820import json
821import re
822import sys
823from pathlib import Path
824
825ARTIFACTS = re.compile(
826 r"\x1b\[[IO]" # focus in/out events
827 r"|\x1b\]1[01];[^\x07\x1b]*(?:\x07|\x1b\\)" # OSC 10/11 color query/response
828 r"|\x1b\[\?\d+(?:;\d+)*c" # device-attribute responses
829 r"|\x1b\[>\d+(?:;\d+)*c" # secondary DA responses
830 r"|\x1bP>\|[^\x1b]*\x1b\\" # XTVERSION response
831)
832
833# Single-codepoint substitutions (str.translate); multi-char targets are
834# fine as translate values.
835TUI_TOFU = {
836 0x23FA: "\u25cf", # ⏺ → ●
837 0x23F8: "\u2551", # ⏸ → ║
838 0x23BF: "\u2514", # ⎿ → └
839 0x2B1D: "\u25aa", # ⬝ → ▪
840}
841# The whole Braille block is tofu in Menlo; OpenCode's ten spinner frames get
842# the matching ASCII spinner phase, anything else in the block a plain dot.
843TUI_TOFU.update({codepoint: "\u00b7" for codepoint in range(0x2800, 0x2900)})
844TUI_TOFU.update(
845 zip(
846 (0x280B, 0x2819, 0x2839, 0x2838, 0x283C, 0x2834, 0x2826, 0x2827, 0x2807, 0x280F),
847 "|/-\\|/-\\|/",
848 )
849)
850
851path = Path(sys.argv[1])
852documents = []
853for line in path.read_text(encoding="utf-8").splitlines():
854 if not line.strip():
855 continue
856 documents.append(json.loads(line))
857
858for document in documents:
859 if isinstance(document, list) and len(document) >= 3 and document[1] == "o":
860 document[2] = ARTIFACTS.sub("", document[2]).translate(TUI_TOFU)
861
862# Static GIF previews show frame zero without advancing the animation. Make the
863# first rendered frame the recorder banner rather than the empty PTY that
864# precedes it. Asciicast v2 uses absolute timestamps; v3 uses per-event delays.
865events = [
866 document
867 for document in documents
868 if isinstance(document, list) and len(document) >= 3
869]
870banner_event = next(
871 (
872 event
873 for event in events
874 if event[1] == "o"
875 and isinstance(event[2], str)
876 and "GCO Autopilot" in event[2]
877 ),
878 None,
879)
880header_version = documents[0].get("version") if isinstance(documents[0], dict) else None
881if banner_event is not None and header_version == 2:
882 banner_time = float(banner_event[0])
883 for event in events:
884 event[0] = round(max(0.0, float(event[0]) - banner_time), 6)
885elif banner_event is not None and header_version == 3:
886 for event in events:
887 event[0] = 0
888 if event is banner_event:
889 break
890
891path.write_text(
892 "\n".join(json.dumps(d, ensure_ascii=False, separators=(",", ":")) for d in documents)
893 + "\n",
894 encoding="utf-8",
895)
896PYEOF
8976echo "✓ Terminal query/response artifacts stripped, TUI tofu glyphs substituted"
898
899# ── Convert to GIF ──────────────────────────────────────────────────────────
900
9016if [ "${SKIP_GIF:-}" != "1" ]; then
9025 echo ""
9035 echo "Converting to GIF (speed=${SPEED}x, theme=${THEME})..."
9045 render_gif "$CAST_FILE" "$GIF_FILE" "$SPEED" "$THEME" "$COLS" "$ROWS"
9055 echo "✓ GIF saved: ${GIF_FILE}"
90620 GIF_SIZE=$(du -h "$GIF_FILE" | cut -f1); echo " Size: $GIF_SIZE"
907fi
908
909# ── Summary ──────────────────────────────────────────────────────────────────
910
9116echo ""
9126echo "=== Done ==="
9136echo ""
9146echo "Files:"
9156echo " ${CAST_FILE}"
91611[ "${SKIP_GIF:-}" != "1" ] && echo " ${GIF_FILE}"
9176echo ""
9186echo "Any new GIF must satisfy the reviewed policy in"
9196echo ".github/scripts/validate_demo_gifs.py (size/dimensions/frames)."
9206echo ""
9216echo "Embed in README:"
92212GIF_BASENAME="$(basename "$GIF_FILE")"
9236echo " ![GCO Autopilot](demo/${GIF_BASENAME})"