| name | powerpoint-slide-recorder-cli |
| description | Use this skill for service operations only. DO NOT use this skill for CLI implementation lifecycle work such as creating, testing, updating, troubleshooting, validating, removing, or documenting the CLI tool itself; delegate those tasks to cli-tool-expert. MANDATORY: Execute powerpoint-slide-recorder operations using the `powerpoint-slide-recorder` CLI tool. Record narrated PowerPoint slides. For CourseCraft slide recording, first use the project screen-recording skill; this skill is only the PowerPoint command adapter. Triggers: powerpoint-slide-recorder, powerpoint-slide-recorder cli, PowerPoint slide recorder, screencast recorder, PowerPoint screencast, record PowerPoint screencast, screen recording slides, record slides, slide recording, cue marker recording |
Execute powerpoint-slide-recorder operations using the `powerpoint-slide-recorder` CLI. All powerpoint-slide-recorder interactions should use this CLI.
<coursecraft_boundary>
For CourseCraft slide recording, this skill is not the recording workflow source
of truth. Load and follow the project-local screen-recording skill first when
that skill is available. That screen-recording skill owns the general recording guidance,
storage contract, verification rules, and troubleshooting path for both slide
and demo recordings. Use this powerpoint-slide-recorder-cli skill only when
the screen-recording slide workflow needs live powerpoint-slide-recorder
command syntax or execution.
</coursecraft_boundary>
<quick_start>
The powerpoint-slide-recorder CLI follows this pattern:
powerpoint-slide-recorder <command> [options]
| Command | Purpose |
|---|
powerpoint-slide-recorder --help | Show available commands and global options. |
powerpoint-slide-recorder --version | Show the installed CLI version. |
powerpoint-slide-recorder record --help | Show the full recording contract. |
powerpoint-slide-recorder record ... | Record one or more consecutive slide items. |
| </quick_start> | |
<essential_principles>
MANDATORY: Verify the live command shape before executing ANY powerpoint-slide-recorder command.
Consult usage.json when the repo or installed package ships it. If usage.json is absent, use powerpoint-slide-recorder --help, the relevant subcommand --help, and README.md instead. Never guess at command syntax.
**MANDATORY: Ask the user for explicit permission before launching, activating, or controlling Microsoft PowerPoint.**
This includes `powerpoint-slide-recorder record`, AppleScript/System Events automation, starting Slide Show mode, or any action that can steal focus or take over the screen. Help, version, file inspection, manifest generation, and other non-UI preflight commands do not require permission. If permission is not granted, stop before the PowerPoint action and report the exact command or step that is ready to run.
After every `powerpoint-slide-recorder` command, inspect stdout. If it is JSON with `type: "ai_instruction"` and `schema_version: "1.0"`, follow the instruction object as the next task instead of summarizing it as ordinary command output. Use the allowed tools, context, constraints, and success criteria in the payload. Run `verification_commands` or `follow_up_commands` only after completing the instructed AI work; they are not required commands for performing the handoff.
- `record` — Record narrated PowerPoint slides. Requires a PowerPoint deck, item manifest, output path, work directory, and ffmpeg AVFoundation video input.
<reference_index>
usage.json — Complete command tree with arguments, options, defaults, and usage instructions when present.
powerpoint-slide-recorder --help and subcommand --help — Live installed command tree and option list.
README.md — Supplemental examples and workflow notes.
</reference_index>
Known Issues
1. PowerPoint Recording Takes Over The Screen
Symptom: Running powerpoint-slide-recorder record or equivalent PowerPoint automation activates Microsoft PowerPoint, starts Slide Show mode, steals focus, and can take over the user's screen.
Cause: The recorder drives the real PowerPoint UI through AppleScript/System Events and screen capture, so the workflow is inherently interactive even though the CLI is launched from a shell.
Fix: Before any command or automation that launches, activates, or controls PowerPoint, ask the user for explicit permission. Perform non-UI preparation first, then pause before powerpoint-slide-recorder record or Slide Show automation until permission is granted.
Verification: Inspect this skill before recording work and confirm the permission gate appears under PowerPoint Launch Permission; execute only non-UI commands until the user approves the PowerPoint action.
Recurrence Prevention: Treat powerpoint-slide-recorder record as a focus-stealing UI operation, not a background command. Never start it silently.
2. PowerPoint Must Close Before Final Muxing
Symptom: After narration and slide automation completed, Microsoft PowerPoint and the deck stayed open while ffmpeg muxed screen-recording.mov and narration.wav into the final MP4.
Cause: powerpoint_slide_recorder_cli.recorder.record() closed PowerPoint in the finally block after the final mux command, so the UI remained open during long encoding work.
Fix: Close the slideshow and deck immediately after screen capture stops and before running the final mux command. Keep this lifecycle covered by tests/test_recorder.py::RecordTests::test_record_closes_powerpoint_before_final_mux.
Verification: Run uv run --project /Users/adam/Dropbox/GitRepos/cli-tools/powerpoint-slide-recorder --with pytest python -m pytest /Users/adam/Dropbox/GitRepos/cli-tools/powerpoint-slide-recorder/tests/test_recorder.py::RecordTests::test_record_closes_powerpoint_before_final_mux -q, uv run --project /Users/adam/Dropbox/GitRepos/cli-tools/powerpoint-slide-recorder --with pytest python -m pytest /Users/adam/Dropbox/GitRepos/cli-tools/powerpoint-slide-recorder/tests/test_recorder.py -q, powerpoint-slide-recorder --help, and powerpoint-slide-recorder record --help.
Recurrence Prevention: Treat screen capture completion as the boundary where PowerPoint must be released. Do not defer PowerPoint cleanup until final encoding or result formatting.
3. Output MP4 Has Black Bars From Aspect Ratio Mismatch
Symptom: A recorded slide MP4 has black bars above and below the slide content.
Cause: The recorder captured the selected AVFoundation source without validating its aspect ratio against the intended course output size, then muxed the raw recording without forcing a final output resolution.
Fix: Use powerpoint-slide-recorder record --resolution WIDTHxHEIGHT to define the final MP4 size. The default is 1920x1080, and both dimensions must be positive even integers because the MP4 is encoded with libx264. The recorder probes --video-input before PowerPoint launches, fails when the capture source aspect ratio differs or is smaller than the requested output, and muxes with -vf scale=<width>:<height> without padding or cropping. If the source does not match because the display is in the wrong mode, use --force-resolution to temporarily switch the main display to --resolution, re-probe the capture source, record, and restore the previous display mode during cleanup. For standard 1080p course output, prefer --force-aspect-ratio 16x9; this switches to the highest-usable-area source mode for 16:9 (an exact 16:9 mode when one exists, otherwise the best near-ratio mode whose centered 16:9 crop still covers 1920x1080) and scales the final MP4 down to the default 1920x1080 output. On a panel with no true 16:9 mode the centered 16:9 slide region is cropped from the captured frame before scaling — see Known Issue 5.
Verification: Run uv run --project /Users/adam/Dropbox/GitRepos/cli-tools/powerpoint-slide-recorder --with pytest python -m pytest /Users/adam/Dropbox/GitRepos/cli-tools/powerpoint-slide-recorder/tests/test_recorder.py -q, powerpoint-slide-recorder --help, and powerpoint-slide-recorder record --help; confirm the help output shows --resolution with default 1920x1080, --force-resolution, and --force-aspect-ratio.
Recurrence Prevention: Treat --resolution as part of the normal recording contract. Do not bypass the preflight probe or add pad/crop filters to make mismatched capture sources fit. Do not add an external display-resolution dependency; the recorder owns display switching through its embedded macOS CoreGraphics helper.
4. Slideshow Starts On The Wrong Slide So Reveals Never Line Up
Symptom: A multi-slide clip records with every slide desynchronized from its narration: the first slide is clicked away immediately, narration ends up on the second slide, and the click-build reveals never appear at the right time (they look like they fire on the wrong slides or not at all).
Cause: The recorder started the show with "Play from Start" (always slide 1) and then jumped to the clip's first deck slide by typing the slide number followed by Return. In a live PowerPoint slide show the typed digit is DROPPED — only the Return registers, as a single advance — so the show lands on slide 2 instead of the requested slide. The per-cue Space schedule built for the clip's real slides then fires against the wrong slides. The click-build animations themselves are fine; this is purely a navigation bug, not an animation bug.
Fix: Start the show on the clip's first slide through a custom slide-show range instead of "Play from Start" + a number jump. powerpoint_slide_recorder_cli.recorder.render_run_slideshow_range(start, end) sets range type to slide show range, sets starting slide/ending slide, forces advance mode to slide show advance manual advance, then runs run slide show. The show opens directly on the starting slide in build-pending state, so Space presses fire click-builds exactly like a normal linear run. jump_to_slide is removed.
Verification: Run uv run --project /Users/adam/Dropbox/GitRepos/cli-tools/powerpoint-slide-recorder --with pytest python -m pytest /Users/adam/Dropbox/GitRepos/cli-tools/powerpoint-slide-recorder/tests/test_recorder.py -q (see test_run_slideshow_range_starts_on_clip_first_slide_via_custom_range). For a live check, start a custom-range show at slide N and confirm current show position HOLDS across that slide's build-count Space presses before advancing (the builds consume the presses) instead of incrementing on every press.
Recurrence Prevention: Never navigate a running slide show by typing a slide number. Start on the intended slide with a custom slide-show range; the typed-number jump is unreliable.
5. --force-aspect-ratio 16x9 Failed On A 16:10-Only Built-In Display
Symptom: powerpoint-slide-recorder record ... --force-aspect-ratio 16x9 failed with Error: No display mode with aspect ratio 16x9 is available on the main display on a laptop whose only display is the built-in panel. --force-resolution 1920x1080 failed the same way. This blocked all slide recording on the laptop, which is the only host with PowerPoint installed.
Cause: The built-in Liquid Retina XDR panel is natively 16:10 and advertises NO true 16:9 pixel mode — its modes are ratio 1.6 (e.g. 3456x2160, 2560x1600, 1920x1200) or 1.547 (e.g. 3456x2234, active HiDPI 4112x2658). The old best_display_mode_for_aspect_ratio required an EXACT 16:9 pixel mode and hard-raised when none existed, aborting before the recorder's already-built letterbox-crop path could run. PowerPoint always presents the 16:9 slide centered and letterboxed inside the 16:10 screen, so the centered 16:9 sub-rectangle of the captured 16:10 frame IS the slide and can be cropped + scaled to 1920x1080.
Fix: best_display_mode_for_aspect_ratio(modes, aspect_w, aspect_h, output_w, output_h) now selects the highest-USABLE-area mode for the output AR: a mode qualifies when its centered output-AR crop is at least the requested output size. An exact-AR mode still wins when present (full pixel area usable, recorded uncropped — so an external true-16:9 monitor is unaffected); otherwise the best near-ratio mode is chosen and the existing centered crop (centered_crop_for_output_aspect/capture_crop_plan) extracts the slide before the scale mux. It raises only when no mode's usable region reaches the output. No new flag is needed; --force-aspect-ratio 16x9 now works on a 16:10-only panel.
Verification: Run uv run --project /Users/adam/Dropbox/GitRepos/cli-tools/powerpoint-slide-recorder --with pytest python -m pytest /Users/adam/Dropbox/GitRepos/cli-tools/powerpoint-slide-recorder/tests/test_recorder.py -q (see test_force_aspect_ratio_crops_centered_region_on_sixteen_ten_only_panel, test_best_display_mode_for_aspect_ratio_uses_centered_crop_on_sixteen_ten_only_panel, and test_best_display_mode_for_aspect_ratio_prefers_exact_over_larger_croppable). Live: record on the built-in panel with --force-aspect-ratio 16x9 and confirm the recorder logs Capture 4112x2658 != output AR 1920x1080; cropping centered 4112x2312 -> scaling to 1920x1080, exits 0, and the output MP4 is 1920x1080 with slide content filling the frame (no black bars).
Do not gate on an exact display-mode match. The output aspect ratio is delivered by the centered crop + scale, so any mode whose centered crop covers the output qualifies. Do not add an external 16:9-monitor requirement and do not add pad filters; the centered-crop path already removes the letterbox.
6. Best-Effort Dialog Probe Crashed The Recorder On A Bare false
Symptom: A live recording aborted at the PowerPoint-open step with Error: Unexpected PowerPoint dialog probe response: 'false'. The recorder added a System Events probe that detects a blocking PowerPoint startup dialog (sign-in, "What's New", Document Recovery, update, first-run) so it can auto-dismiss benign ones, but the probe itself killed the run.
Cause: powerpoint_slide_recorder_cli.recorder.parse_frontmost_dialog_payload split the probe output on \x1f and RAISED when it was not a 3-field payload. The not-running guard returns false\x1f\x1f (3 parts, parses fine), but the live-process path (tell process "Microsoft PowerPoint") can return the bare string false — most often because an Automation (AppleEvents) consent gate intercepted the event or AX was not ready in the moments right after PowerPoint launched. The 1-part false made the parser raise, and that exception propagated up and crashed the whole recorder. The auto-dismiss of benign startup dialogs is only a convenience, so it must never block or crash recording.
Fix: Make the dialog probe non-fatal end to end. parse_frontmost_dialog_payload now returns None ("no dialog detected") for ANY response that is not a well-formed 3-field payload — bare false, empty string, or an AppleScript error string — and never raises. frontmost_powerpoint_dialog wraps the probe so any failure (hard osascript subprocess timeout, System Events / AX / Automation-consent error, ill-formed output) is caught, retried a few times with short sleeps (DIALOG_PROBE_ATTEMPTS/DIALOG_PROBE_RETRY_SECONDS) to ride out the post-launch AX race, then logged via log_warning (so a genuine consent gate stays diagnosable) and treated as "no dialog." try_auto_dismiss_startup_dialog (formerly raised on an unknown dialog) now logs an unrecognized dialog and returns None instead of clicking blindly, and catches any dismiss failure. open_deck and clear_benign_dialogs_before_slideshow use this best-effort path and never raise from the probe; when the deck is already open the recorder proceeds to the slideshow even if dialog state is unreadable. The hard subprocess timeouts and the fail-fast gates on the ACTUAL recording steps (open_deck raising when the deck never opens, the slideshow drive) are kept — only the best-effort dialog probe is non-fatal.
Verification: Run uv run --project /Users/adam/Dropbox/GitRepos/cli-tools/powerpoint-slide-recorder --with pytest python -m pytest /Users/adam/Dropbox/GitRepos/cli-tools/powerpoint-slide-recorder/tests/test_recorder.py -q (see , , , , , , and ).
Treat the startup-dialog probe and auto-dismiss strictly as convenience: it may skip itself but must never raise, block, or decide whether recording succeeds. Keep fail-fast only on the real recording steps. Never reintroduce a for a non-3-field or error probe response; an unreadable probe means "no dialog detected, proceed."
7. PowerPoint Remained Wedged After AppleEvent Cleanup Failure
Symptom: After a failed recording run on adam-server, PowerPoint AppleScript calls hung or returned Connection is invalid. (-609), and a normal pkill -x "Microsoft PowerPoint" did not terminate the process.
Cause: Recorder cleanup relied on AppleScript quit/close actions. When the recorder-launched PowerPoint process was already wedged, the cleanup error was reported but the stale process could survive and poison the next run.
Fix: powerpoint_slide_recorder_cli.recorder.cleanup_powerpoint() now force-kills Microsoft PowerPoint with pkill -9 -x and verifies pgrep -x is empty when AppleScript cleanup fails with a PowerPoint connection/timeout error and the recorder launched PowerPoint itself. It does not force-kill when PowerPoint was already running before the recorder started.
Verification: Run uv run --project /Users/adam/Dropbox/GitRepos/cli-tools/powerpoint-slide-recorder --with pytest python -m pytest /Users/adam/Dropbox/GitRepos/cli-tools/powerpoint-slide-recorder/tests/test_recorder.py -q (see test_cleanup_force_kills_recorder_launched_powerpoint_on_connection_error, test_cleanup_does_not_force_kill_preexisting_powerpoint, and test_force_kill_powerpoint_uses_sigkill_and_verifies_exit).
Recurrence Prevention: Do not handle -609/-1712 cleanup failures with manual retry loops. Let recorder-owned sessions hard-reset the stale PowerPoint process; use host reset only for stale processes that predate the recorder run.
8. Pointer Near The Lower-Left Corner Exposed Slideshow Navigation Controls
Symptom: A recording showed PowerPoint's lower-left slide-show navigation controls even though no navigation controls were intentionally used.
Cause: PowerPoint reveals the navigation toolbar when pointer activity leaves its hover state near the lower-left corner. CGWarpMouseCursorPosition moved the cursor without delivering a mouse-moved event, and positioning an event at the exact screen boundary did not deliver a usable in-window hover transition. AVFoundation startup then retriggered the toolbar after the pre-capture settle, while the during-capture presence check repeatedly ran the mutating fullscreen action. This behavior is independent of PowerPoint Show Media Controls, which governs embedded-media playback controls.
Fix: park_slideshow_cursor() posts a Quartz kCGEventMouseMoved event 64 points inside the right-edge midpoint. The recorder restores PowerPoint's automatic pointer mode, parks after fullscreen, then parks again after AVFoundation starts and waits two seconds before audio/action timing. Final muxing trims the same two-second raw-video warm-up so narration and click cues remain aligned. The during-capture watchdog uses read-only assert_window, not fullscreen_window. The recorder does not globally hide the cursor and does not add another automation stack.
Verification: Run uv run --project /Users/adam/Dropbox/GitRepos/cli-tools/powerpoint-slide-recorder --with pytest python -m pytest /Users/adam/Dropbox/GitRepos/cli-tools/powerpoint-slide-recorder/tests/test_recorder.py -q (see test_macos_display_helper_moves_cursor_with_quartz, test_park_slideshow_cursor_uses_right_edge_midpoint, test_assert_slideshow_present_is_read_only, test_set_slideshow_pointer_automatic_uses_powerpoint_command_u_shortcut, test_start_slideshow_sets_pointer_automatic_then_parks_before_settle_delay, test_record_closes_powerpoint_before_final_mux, and test_final_mux_scales_to_requested_resolution_without_padding_or_crop). Live adam-server evidence: with the pointer 64 points inside the right edge and re-parked after AVFoundation startup, controls were present at raw t=0.00 and absent from every sampled frame from t=0.25 through t=9.50; confirm a fresh final MP4 starts after that warm-up and remains control-free.
Recurrence Prevention: Keep pointer parking both after fullscreen and after capture activation, keep the watchdog read-only, and keep the mux trim equal to the post-capture settle. Do not park on the exact screen boundary, treat as the control for this overlay, or replace the PowerPoint-local pointer lifecycle with global cursor hiding.
9. Exact Save Dialog Blocked Deck Opening
Symptom: Deck opening exhausted its polling budget while the dialog probe repeatedly reported title='Save', text='Save'.
Cause: The startup-dialog catalog had no entry for PowerPoint's exact-title Save sheet, so the safety policy correctly left it untouched as unknown and open_deck() could not progress.
Fix: Match only the exact case-insensitive title Save, then try the non-writing dismissal buttons Don't Save, Discard, and Cancel in that order; the existing Escape fallback remains the final safe action. Do not classify Save As or use a broad Save substring match.
Verification: Run the recorder test suite, including test_save_dialog_matches_exact_title_without_matching_save_as and test_auto_dismiss_save_dialog_uses_safe_discard_buttons, then complete a live adam-server recording from the previously blocked deck.
Recurrence Prevention: Keep destructive or ambiguous dialogs exact-match only. Unknown dialogs remain untouched; never broaden this policy to click a Save button.
10. Deck Click Steps Counted From OOXML Did Not Match The Live Slide Show
Symptom: A 24-slide, 777s module recording shipped desynchronized at rc=0. The deck ran AHEAD of the narration from slide 3 on, slide 10 over-held ~44s, slide 16 over-held ~54s, slides 9 and 17 flashed by in ~2s as the press backlog drained, and slide 24 was never shown. The recorder pressed exactly on schedule the whole time — a lateness guard cannot catch this.
Cause: slide_animation_counts() derived each slide's click count by counting p:cTn[@nodeType='clickEffect'] nodes under p:timing/p:tnLst on the slide and its layout. That authored node count is NOT the number of Space presses the running show consumes. Measured on adam-server against m1_vcp_approved.pptx by walking the show and reading the live slide index after each press: slide 3 XML=10 / live=8, slide 10 XML=2 / live=5, slide 16 XML=4 / live=8; the other 21 slides matched. On layout-inherited paragraph builds (<p:bldP build="p"> with a <p:tmplLst> template) PowerPoint expands the layout's build template against each SLIDE's own content at show time, so the authored nodes and the executed click steps diverge. The divergence is not recoverable from the parts: slides 16 and 24 have byte-identical animation XML for their animated placeholders (same clickEffect count, same pRg 0..0, same build="p", same layout placeholder structure, same 3-paragraph bodies) yet consume 3 and 1 click steps per shape respectively. PowerPoint's own AppleScript surface offers no shortcut either: print steps returned 1 for all 24 slides, and timeline's main sequence only exposes slide-OWN effects (10 on the one slide with its own p:timing, 0 on the other 23).
Fix: Stop deriving the count from OOXML. measure_slide_click_steps(config, slide_numbers) runs BEFORE any capture: it opens the deck, plays the requested range with manual advance, presses Space through the whole range, and reads slide index of slide of slide show view of slide show window 1 after each press. click_steps_from_slide_index_walk turns that index sequence into per-slide counts (index unchanged = a click step; index +1 = the advance; None = show ended). assert_cue_counts_match_click_steps then validates the authored cue markers against the measurement. slide_animation_count_from_xml, slide_layout_member_for_slide, slide_layout_animation_count, and slide_animation_counts are removed. additionally guards the drive: the live slide index is compared against the action's planned slide, and every advance opens a bounded transition via that must be observed to land, so this class of divergence aborts the recording instead of shipping. (The check runs on the presence-watcher thread, and the expectation must be opened before the advance press — see Known Issue 12.)
Run (see and , ). Live: walk the deck with Space, reading the live slide index after each press, and confirm the measured per-slide counts equal what the recorder plans.
Never infer a deck's click steps from its OOXML. The only authority for how many presses a slide consumes is the running slide show. Keep the pre-flight walk on the same PowerPoint session path as the recording, and keep the position guard — a schedule/lateness check cannot detect a deck that consumes presses differently than planned.
11. record Aborted With CourseCraft repo root not found When Launched Outside The Repo
Symptom: A live powerpoint-slide-recorder record run on adam-server aborted at startup with Error: CourseCraft repo root not found from: /Users/adam after the caller had already staged the deck, items manifest, and work directory. The error named a directory unrelated to any flag the caller passed.
Cause: powerpoint_slide_recorder_cli.recorder.resolve_coursecraft_repo_root() defaulted to Path.cwd() and walked upward for course-pipeline.json to locate the DemoEnvironmentAutomation module used by run_demo_environment_recording_prep(). That made the working directory an undocumented implicit input to a CLI whose every other input is an absolute --deck/--items/--output/--work-dir path, and neither record --help, the README, nor this skill said so.
Fix: record now takes --coursecraft-repo-root PATH, threaded through client.record() → build_config() → config["coursecraft_repo_root"] → run_demo_environment_recording_prep(). Omitting it keeps the upward cwd search, and the failure now names the requirement: ... (no course-pipeline.json in that directory or any parent). Pass --coursecraft-repo-root PATH, or run the command from inside the CourseCraft repo.
Verification: Run uv run --project /Users/adam/Dropbox/GitRepos/cli-tools/powerpoint-slide-recorder --with pytest python -m pytest /Users/adam/Dropbox/GitRepos/cli-tools/powerpoint-slide-recorder/tests -q (see test_demo_environment_manifest_resolves_from_explicit_repo_root_outside_cwd, test_coursecraft_repo_root_failure_names_the_explicit_option, test_record_forwards_configured_coursecraft_repo_root_to_demo_environment_prep, and test_public_cli_forwards_coursecraft_repo_root), and confirm powerpoint-slide-recorder record --help lists --coursecraft-repo-root.
Recurrence Prevention: Pass --coursecraft-repo-root explicitly for any run launched from outside the CourseCraft repo, including automation and remote-host runs. Do not add new implicit cwd-relative inputs to this CLI; every path the recorder needs belongs on the command line.
12. Position Guard Aborted A Correctly Driven Recording On A Slide Advance
Symptom: A 24-slide capture aborted mid-recording at slide 10 with PowerPoint slideshow was not present during capture: PowerPoint slide show is on slide 11 but the timing plan is driving slide 10; the deck consumed a different number of click steps than the slide-show probe measured. The recording was actually correct: the extracted capture frames show all five of slide 10's click steps firing on their cues (four bullet reveals plus the final de-emphasis) and the advance to slide 11 landing on schedule. Re-walking slide 10 at the real recording pacing confirmed it consumes exactly the five click steps the pre-flight probe measured, so neither the deck, the cue markers, nor the measurement was wrong.
Cause: Ordering race between the capture loop and the watcher thread. record() sent the advance keypress and only THEN registered the transition: press_space() followed by position_watcher.expect_slide(action["slide"] + 1). PowerPoint can land the advance while press_space()'s osascript round trip is still returning, and SlideshowPositionWatcher.check runs on SlideshowPresenceWatcher's thread — it snapshots _expected_slide/_pending_slide under the lock and then spends its own osascript round trip reading the live index. A poll that snapshotted expected=10, pending=None and read the index after the advance landed saw slide 11 with no pending transition open, which is exactly the "deck running ahead" signature, so it aborted. The bounded transition in expect_slide already handles the reverse order safely, so the window existed only because the expectation was opened too late. It is a sampling race (2.0s poll interval against a sub-second window per advance), which is why 23 other advances in the same run passed.
Fix: Open the transition BEFORE the key is sent — position_watcher.expect_slide(action["slide"] + 1) now precedes press_space() inside the SLIDE_ADVANCE_REASON branch. expect_slide tolerates the show sitting on the previous slide for SLIDE_ADVANCE_CONFIRM_SECONDS, so pre-registering cannot mask a genuinely eaten advance: an advance that never lands still trips the "did not advance" abort.
Verification: Run uv run --project /Users/adam/Dropbox/GitRepos/cli-tools/powerpoint-slide-recorder --with pytest python -m pytest /Users/adam/Dropbox/GitRepos/cli-tools/powerpoint-slide-recorder/tests -q (see test_advance_expectation_opens_before_the_press_is_sent, which asserts the recorded event order is then , and fails on the old ordering). Live: complete a full-module capture on without a position-guard abort.
Any state the watcher thread reads must be published before the action that changes the observed world, never after. When adding a guard that runs off-thread, write the expectation first and let the guard's bounded tolerance absorb the lag — do not order the mutation ahead of the bookkeeping.
Domain Knowledge
Navigating A Running PowerPoint Slide Show (AppleScript)
Context: Use this when the recorder (or any automation) must start or move a live PowerPoint slide show to a specific slide on macOS.
Key Facts: Start at a specific slide by setting a custom range on slide show settings of active presentation — range type = slide show range (the bare enum literal; raw code 0x00d60002), plus integer starting slide and ending slide — then run slide show <settings>. With a custom range, current show position is RANGE-relative (1 = the starting slide), which is fine for blind, cue-driven advancing. Force advance mode to slide show advance manual advance so a deck authored with automatic slide timings cannot self-advance ahead of narration. The slide-show window is named like PowerPoint Slide Show - [<deck>] (it contains "Slide Show", which window-matching can key on).
Gotchas: Typing a slide number + Return to jump in a live show DROPS the digit and lands on slide 2 — never navigate that way. current show position is READ-ONLY, so you cannot set it to jump. go to slide (slide show view ...) number N throws a runtime "Parameter error" on current PowerPoint for Mac. set show with animation ... to true errors (-10006); it is not settable, but animations play by default. The sdef CLI returns empty for PowerPoint (sandboxed) — read the dictionary from /Applications/Microsoft PowerPoint.app/Contents/Resources/PowerPoint.sdef instead. Pluralsight-templated decks define their click-build animations on the slide LAYOUT, not on slideN.xml, yet they still fire on a normal/linear or custom-range run — but do NOT count them from the XML: the authored clickEffect node count and the presses the running show consumes are different numbers (Known Issue 10). Read the live position with slide index of slide of slide show view of slide show window 1; it is the presentation-absolute slide number (not range-relative like current show position), it returns missing value on the post-final-slide black screen, and the slide show window disappears one press later.
Forced Resolution Uses Embedded macOS CoreGraphics
Context: Use this when a recording source fails the resolution guard and the user wants the CLI to fix the display mode automatically.
Key Facts: --force-resolution uses an embedded helper run with /usr/bin/python3 and macOS Quartz/CoreGraphics APIs. It targets the main display, switches to the requested --resolution, re-runs the AVFoundation probe, performs the normal recording, and restores the previous display mode in cleanup.
Gotchas: The requested mode must be available on the main display. If macOS does not advertise that mode, the command fails before PowerPoint launches instead of installing or shelling out to another display tool. On macOS, CGDisplaySetDisplayMode can return success without changing the active mode; use a CoreGraphics configuration transaction with CGBeginDisplayConfiguration, CGConfigureDisplayWithDisplayMode, and CGCompleteDisplayConfiguration.
Standard Course Recordings Use Forced 16:9 Source
Context: Use this when recording CourseCraft slide videos intended for final 1920x1080 delivery.
Key Facts: Run powerpoint-slide-recorder record with --force-aspect-ratio 16x9 for standard course recordings. Do not add --resolution 1920x1080; the default output resolution already applies. The recorder enumerates main-display modes and selects the highest-USABLE-area mode for 16:9 — an exact-16:9 pixel mode when one exists (recorded uncropped), otherwise the best near-ratio mode (e.g. a 16:10 mode on the built-in panel) whose centered 16:9 crop still covers 1920x1080. It switches to that mode, records, restores the previous display mode, crops the centered 16:9 slide region when the chosen mode is not exactly 16:9, and scales the final MP4 to 1920x1080.
Gotchas: --force-aspect-ratio and --force-resolution are mutually exclusive. The built-in Liquid Retina XDR panel is 16:10-only and advertises NO true 16:9 mode; --force-aspect-ratio 16x9 still works there via the centered crop (Known Issue 5) and needs no external monitor. The command fails before PowerPoint launches only when NO mode's centered 16:9 crop reaches 1920x1080 (every mode too small). Do not add pad filters; the centered crop already removes the letterbox.
<success_criteria>
- Command executes without error
- Output is displayed in requested format
- Correct command and flags used, verified against the live help output or
usage.json when present
</success_criteria>