ace-demo 0.23.3 → 0.25.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 19159fe383ba67afd9bb6735e290ceaac5e9750c2f70e71a4415d4def8c1a38d
4
- data.tar.gz: f13f657aef8df3cf47e21a817f897f86a5a2c807e3183223719130ef5d920095
3
+ metadata.gz: 3dc9853e3f70693cc0dc687ab61f645ed432d0afae57a7dd2d7e87c746da9236
4
+ data.tar.gz: 4f85e3e89c5efefdbab60d3776e4a2c6b92feb2067a1bdbdc7954723caada2c4
5
5
  SHA512:
6
- metadata.gz: 0ef7941567c83b88fccc3d7a2b32c152778d432332245ea59053883748787f450c4216ecadb2c02d5d0619a2eaa70b9ba83e6428392c418437a8825b5c587b72
7
- data.tar.gz: 9280cb9b75d343cd62ff9874a0de91b16f90793e37b9127118e7208a791baebdedcfbb6e0aca0275b509f6ef7e4d63f29037dcfb34e6c6e071c5af1c1735e969
6
+ metadata.gz: fff3491b92a232bbbfe39aaf01ddf6f0e3924ff0d9644609572248d4338b0ecceadbcbd5a19f0854398e43c482864d2336eea6a9aaceb97e4ba9a9dda6a654f2
7
+ data.tar.gz: dada165a45b5b184b95f5a49cf30775a8e4290530f13e4ea75337aacfcf2cb4cb4267da621a3311ac2620b82c6d8c71c22a680acc3cb89e0fdc78cb8f9ff342c
data/CHANGELOG.md CHANGED
@@ -6,6 +6,93 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
8
  ## [Unreleased]
9
+ ### Fixed
10
+ - Failed YAML tape verification by default when the final asciinema shell exit is non-zero, unless `verify.allow_nonzero_exit: true` opts out explicitly.
11
+ - Surfaced non-zero cast exit codes in `record` / `verify` output and verification reports so broken recordings are diagnosable without opening the raw cast.
12
+
13
+ ## [0.25.6] - 2026-04-16
14
+
15
+ ### Added
16
+ - Added additive YAML `tmux:` recorder-control directives for asciinema-backed demo tapes, covering shared-surface `attach`, `detach`, `wait`, `send`, and optional `capture` actions alongside existing visible `type:` commands.
17
+
18
+ ### Changed
19
+ - Updated `ace-demo` recording/docs to treat tmux orchestration as structured recorder control instead of canonical raw tmux shell glue.
20
+
21
+ ### Fixed
22
+ - Updated the `ace-tmux` runtime dependency constraint to `~> 0.17` so recorder-side tmux directives stay aligned with the new runtime inspection release line.
23
+ - Rejected unsupported YAML `tmux.action: capture` directives during parsing, forwarded recording env values into recorder-side tmux control resolution, and clarified that tmux directives are supported only for the asciinema backend.
24
+ - Rejected YAML `tmux:` directives when the resolved recording backend is `vhs` so unsupported backend/control-surface combinations fail fast instead of being silently ignored during VHS compilation.
25
+
26
+ ## [0.25.1] - 2026-04-16
27
+
28
+ ### Fixed
29
+ - Exported resolved browser executable variables for VHS runs and aligned non-browser E2E verification with explicit constrained-environment failure evidence.
30
+
31
+ ## [0.25.0] - 2026-04-14
32
+
33
+ ### Changed
34
+ - Rewrote `TS-DEMO-001` smoke E2E coverage around public-surface goal contracts by adding non-dry-run preset recording success coverage and stabilizing dry-run and missing-`--pr` verification assertions.
35
+
36
+ ### Technical
37
+ - Added `TC-005-record-preset-success-artifact` runner/verifier assets and updated scenario/aggregate E2E manifests to execute and verify 5 goals.
38
+ - Updated scenario setup to use `${ACE_E2E_SOURCE_ROOT:-$PROJECT_ROOT_PATH}` for resilient `mise.toml` bootstrap in package sandbox runs.
39
+
40
+ ## [0.24.6] - 2026-04-15
41
+
42
+ ### Fixed
43
+ - Made `ace-demo record` print verification summaries safely when optional failure detail arrays are absent, so reruns surface the real verification outcome instead of crashing in CLI output formatting.
44
+
45
+ ## [0.24.5] - 2026-04-15
46
+
47
+ ### Added
48
+ - Added `ace-demo verify` plus a dedicated cast-analysis workflow so failed asciinema recordings can be re-verified and routed to scenario, product, or verifier fixes before re-recording.
49
+
50
+ ### Changed
51
+ - Expanded YAML demo verification with required visible output and ordered output-transition checks, renamed scenario-side failures to `scenario_defect`, and preserved failed recording sandboxes/manifests for follow-up triage.
52
+
53
+ ## [0.24.4] - 2026-04-15
54
+
55
+ ### Changed
56
+ - Tightened demo planning and recording workflows so tmux/UI demos define the visible transition up front and validate the operator viewpoint before recording.
57
+
58
+ ### Technical
59
+ - Refreshed the handbook demo workflow instructions to reject recordings that miss the visible `before -> trigger -> effect -> after` contract for state-transition demos.
60
+
61
+ ## [0.24.3] - 2026-04-13
62
+
63
+ ### Changed
64
+ - Completed the batch i05 migration follow-through for this package and aligned it with the restarted `fast` / `feat` / `e2e` verification model.
65
+
66
+ ### Technical
67
+ - Included in the coordinated assignment-driven patch release for batch i05 package updates.
68
+
69
+
70
+ ## [0.24.2] - 2026-04-12
71
+
72
+ ### Changed
73
+ - Migrated deterministic `ace-demo` coverage to `test/fast`, kept scenario assets in `test/e2e`, and aligned E2E metadata references to the new fast test paths.
74
+ - Added package-level testing contract guidance for `ace-test ace-demo`, `ace-test ace-demo all`, and `ace-test-e2e ace-demo`.
75
+
76
+ ## [0.24.1] - 2026-04-07
77
+
78
+ ### Added
79
+ - Added fail-closed verification behavior for recording workflows with richer report classification when verification fails.
80
+
81
+ ### Changed
82
+ - Updated demo CLI/recording paths and parser logic to preserve typed failure semantics through verification and recorder execution.
83
+
84
+ ## [0.24.0] - 2026-04-07
85
+
86
+ ### Added
87
+ - Added semantic `verify:` support for YAML/asciinema demo tapes, including required exported variables, forbidden output signatures, and final-state assertion commands.
88
+ - Added structured demo verification reports under `.ace-local/demo/` so failed recordings preserve actionable evidence for retry or bug triage.
89
+
90
+ ### Changed
91
+ - Made `ace-demo record` fail closed on verification errors instead of treating cast mismatches as warning-only output.
92
+ - Classified recording failures as `instruction_defect`, `product_bug`, or `verification_error`, and blocked PR upload/comment when verification does not pass.
93
+
94
+ ### Technical
95
+ - Expanded parser, verifier, recorder, and CLI coverage for semantic verification, report writing, and fail-closed record behavior.
9
96
 
10
97
  ## [0.23.3] - 2026-03-29
11
98
 
data/README.md CHANGED
@@ -18,7 +18,7 @@
18
18
 
19
19
  ![ace-demo demo](docs/demo/ace-demo-getting-started.gif)
20
20
 
21
- `ace-demo` records terminal sessions as proof-of-work evidence for agent-driven workflows. Tapes define what to capture — either as simple [VHS](https://github.com/charmbracelet/vhs) scripts (`.tape`) or as YAML specs (`.tape.yml`) with sandbox setup, scenes, and teardown.
21
+ `ace-demo` records terminal sessions as proof-of-work evidence for agent-driven workflows. Tapes define what to capture — either as simple [VHS](https://github.com/charmbracelet/vhs) scripts (`.tape`) or as YAML specs (`.tape.yml`) with sandbox setup, scenes, teardown, and optional tmux recorder-control directives.
22
22
 
23
23
  Recordings attach directly to GitHub pull requests as reviewable evidence. Requires `vhs`, `chromium`, and `ttyd` for deterministic rendering (see [setup requirements](docs/setup.md)).
24
24
 
@@ -43,6 +43,11 @@ scenes:
43
43
  commands:
44
44
  - type: ace-demo list
45
45
  sleep: 4s
46
+ - tmux:
47
+ action: wait
48
+ for: window-active
49
+ session: fork-demo
50
+ window: work
46
51
  - type: ace-demo record hello
47
52
  sleep: 6s
48
53
 
@@ -51,10 +56,12 @@ teardown:
51
56
  ```
52
57
 
53
58
  - **setup** — sandbox isolation, git init, fixture copying, or arbitrary shell via `run: <cmd>`
54
- - **scenes** — named command sequences compiled to VHS directives (`Type`, `Enter`, `Sleep`)
59
+ - **scenes** — named command sequences with visible `type:` shell commands and optional `tmux:` recorder-control directives for asciinema-backed recordings
55
60
  - **teardown** — cleanup directives that always run (even on failure)
56
61
  - **settings** — optional `font_size`, `width`, `height`, `format` overrides
57
62
 
63
+ Use `type:` for on-camera shell commands that should appear in the recording. Use `tmux:` only with the asciinema backend for recorder-control actions such as `attach`, `detach`, `wait`, and `send` when the demo needs deterministic tmux choreography without raw shell glue. YAML tapes recorded with the `vhs` backend must not include `tmux:` directives.
64
+
58
65
  Legacy `.tape` files use raw VHS syntax directly. See the [Usage Guide](docs/usage.md) for the full tape specification.
59
66
 
60
67
  ## Use Cases
@@ -65,5 +72,26 @@ Legacy `.tape` files use raw VHS syntax directly. See the [Usage Guide](docs/usa
65
72
 
66
73
  *Future: web interaction recording is planned alongside terminal capture.*
67
74
 
75
+ ## Testing Contract
76
+
77
+ Run deterministic package coverage with:
78
+
79
+ ```bash
80
+ ace-test ace-demo
81
+ ace-test ace-demo all
82
+ ```
83
+
84
+ Run deterministic feature coverage only when `test/feat/` exists:
85
+
86
+ ```bash
87
+ ace-test ace-demo feat
88
+ ```
89
+
90
+ Run retained workflow scenarios in E2E:
91
+
92
+ ```bash
93
+ ace-test-e2e ace-demo
94
+ ```
95
+
68
96
  ---
69
97
  [Getting Started](docs/getting-started.md) | [Usage Guide](docs/usage.md) | [Handbook - Skills, Agents, Templates](docs/handbook.md) | Part of [ACE](https://github.com/cs3b/ace)
@@ -0,0 +1,27 @@
1
+ ---
2
+ name: as-demo-analyze-cast
3
+ description: Analyze failed demo casts and route them to scenario, product, or verifier fixes
4
+ # bundle: wfi://demo/analyze-cast
5
+ # agent: general-purpose
6
+ user-invocable: true
7
+ allowed-tools:
8
+ - Bash(ace-demo:*)
9
+ - Bash(ace-bundle:*)
10
+ - Read
11
+ argument-hint: "<cast-file> --tape <tape-ref-or-path> [--sandbox-path <path>]"
12
+ last_modified: 2026-04-15
13
+ source: ace-demo
14
+ integration:
15
+ targets:
16
+ - claude
17
+ - codex
18
+ - gemini
19
+ - opencode
20
+ - pi
21
+ skill:
22
+ kind: workflow
23
+ execution:
24
+ workflow: wfi://demo/analyze-cast
25
+ ---
26
+
27
+ Load and run `ace-bundle wfi://demo/analyze-cast` in the current project, then follow the loaded workflow as the source of truth and execute it end-to-end instead of only summarizing it.
@@ -0,0 +1,53 @@
1
+ ---
2
+ doc-type: workflow
3
+ title: Analyze Demo Cast Workflow
4
+ purpose: cast verification triage workflow instruction
5
+ ace-docs:
6
+ last-updated: 2026-04-15
7
+ last-checked: 2026-04-15
8
+ ---
9
+
10
+ # Analyze Demo Cast Workflow
11
+
12
+ ## Purpose
13
+
14
+ Analyze a recorded `.cast` after demo verification fails, decide whether the problem is the recording scenario, the product, or the verification tooling, and route to the correct next workflow before re-recording.
15
+
16
+ ## Instructions
17
+
18
+ 1. Run cast verification:
19
+
20
+ ```bash
21
+ ace-demo verify <cast-file> --tape <tape-ref-or-path>
22
+ ```
23
+
24
+ If the failed recording preserved a sandbox and the tape uses `assert_commands`, include it:
25
+
26
+ ```bash
27
+ ace-demo verify <cast-file> --tape <tape-ref-or-path> --sandbox-path <sandbox-path>
28
+ ```
29
+
30
+ 2. Read the generated report under `.ace-local/demo/` or the selected `--report-dir`.
31
+
32
+ 3. Route by classification:
33
+
34
+ - `scenario_defect`:
35
+ - fix the tape, camera/viewpoint, setup, or `verify:` contract
36
+ - keep the product code unchanged
37
+ - re-record after updating the demo scenario
38
+ - `product_bug`:
39
+ - treat the cast as a real product/runtime failure
40
+ - run the normal bug analysis/fix workflow for the affected package
41
+ - re-record only after the product fix lands
42
+ - `verification_error`:
43
+ - fix the verifier, cast parser, tape metadata, or recorder/tooling problem
44
+ - re-run verification
45
+ - re-record only if the original cast is unusable or no longer representative
46
+
47
+ 4. Do not upload or comment on a PR until `ace-demo verify` returns `pass`.
48
+
49
+ ## Success Criteria
50
+
51
+ - Every failed demo recording is triaged through `ace-demo verify`
52
+ - The next action is unambiguous: scenario fix, product bug fix, or verifier/tooling fix
53
+ - Re-recording happens only after the correct upstream issue is fixed
@@ -29,7 +29,31 @@ Tapes are VHS script files that define terminal sessions: commands to type, timi
29
29
 
30
30
  ## Instructions
31
31
 
32
- 1. **Preview the tape** before writing:
32
+ 1. **Define the demo contract before scripting anything**:
33
+
34
+ Capture these decisions in notes or the task/PR before you create a tape:
35
+
36
+ - **Viewpoint**: what exact terminal or tmux client is being recorded
37
+ - **Starting state**: what must already be visible on screen before the trigger action
38
+ - **Trigger action**: the concrete command or interaction that causes the behavior
39
+ - **Visible reaction**: the system state change that must be seen in the recording
40
+ - **End state**: what the reviewer should understand after the recording finishes
41
+
42
+ For demos about tmux, panes, windows, or session routing, the contract must explicitly say which tmux client view is being recorded. A shell transcript alone is not sufficient if the feature is about visible tmux behavior.
43
+
44
+ 2. **Keep setup off-camera unless setup is the feature**:
45
+
46
+ Pre-stage long or distracting setup before recording whenever possible:
47
+
48
+ - create fixtures, assignments, and helper scripts before the recording starts
49
+ - start from the state the viewer must recognize
50
+ - only leave setup on camera when the setup step itself is what the demo is proving
51
+
52
+ For tmux fork/window demos, prefer starting in the operator's normal work window and let the recording capture the visible transition into the fork window. Do not begin already attached to the fork window unless the feature is specifically about that attach flow.
53
+
54
+ If the demo is intended to be durable evidence for docs, review, or PR proof, prefer a committed `.tape.yml` / asciinema flow over ad-hoc inline/VHS capture so the resulting `.cast` can be re-verified later.
55
+
56
+ 3. **Preview the tape** before writing:
33
57
 
34
58
  ```bash
35
59
  ace-demo create <name> --dry-run -- "cmd1" "cmd2"
@@ -37,7 +61,7 @@ Tapes are VHS script files that define terminal sessions: commands to type, timi
37
61
 
38
62
  This prints the generated tape content without writing any file.
39
63
 
40
- 2. **Create the tape**:
64
+ 4. **Create the tape**:
41
65
 
42
66
  ```bash
43
67
  ace-demo create <name> -- "cmd1" "cmd2"
@@ -48,13 +72,13 @@ Tapes are VHS script files that define terminal sessions: commands to type, timi
48
72
  ace-demo create <name> --desc "What this demo shows" --tags "feature,setup" -- "cmd1" "cmd2"
49
73
  ```
50
74
 
51
- 3. **Update an existing tape** (overwrite):
75
+ 5. **Update an existing tape** (overwrite):
52
76
 
53
77
  ```bash
54
78
  ace-demo create <name> --force -- "cmd1" "cmd2"
55
79
  ```
56
80
 
57
- 4. **Verify** the created tape:
81
+ 6. **Verify** the created tape:
58
82
 
59
83
  ```bash
60
84
  ace-demo show <name>
@@ -62,7 +86,7 @@ Tapes are VHS script files that define terminal sessions: commands to type, timi
62
86
 
63
87
  This displays metadata and full tape contents.
64
88
 
65
- 5. **List all available tapes** to confirm visibility:
89
+ 7. **List all available tapes** to confirm visibility:
66
90
 
67
91
  ```bash
68
92
  ace-demo list
@@ -86,4 +110,6 @@ Tapes are VHS script files that define terminal sessions: commands to type, timi
86
110
 
87
111
  - Tape file created at `.ace/demo/tapes/<name>.tape`
88
112
  - `ace-demo show <name>` displays correct metadata and commands
89
- - `ace-demo list` shows the new tape
113
+ - `ace-demo list` shows the new tape
114
+ - The tape starts from the intended viewer-recognizable state instead of rebuilding irrelevant setup on camera
115
+ - For state-transition demos, the tape visibly shows `before -> trigger -> visible effect -> after`
@@ -29,10 +29,27 @@ Record terminal demos using `ace-demo record`. Supports two modes: tape-based re
29
29
 
30
30
  ## Instructions
31
31
 
32
+ ### Validate the Recording Contract First
33
+
34
+ Before choosing tape mode vs inline mode, lock these points:
35
+
36
+ - **What is the user-visible behavior?**
37
+ - **What screen or tmux client must the reviewer be looking at to see it?**
38
+ - **What setup can happen off-camera so the recording starts at the meaningful baseline?**
39
+
40
+ If the feature is about visible tmux behavior such as opening a pane, switching windows, or showing a running fork in the current session:
41
+
42
+ - record the tmux client view that will actually show that transition
43
+ - do not treat a shell transcript as sufficient proof
44
+ - start from the operator's normal work window unless the feature is specifically about attach/reattach behavior
45
+ - prefer showing the transition directly over showing commands that reconstruct the state later
46
+
32
47
  ### Record from Existing Tape
33
48
 
34
49
  The tape argument accepts a **preset name** (from `ace-demo list`) or a **direct file path** to a `.tape` or `.tape.yml` file.
35
50
 
51
+ For demos that serve as durable evidence, prefer a committed `.tape.yml` so the generated `.cast` can be verified again with `ace-demo verify`.
52
+
36
53
  1. **Find available tapes**:
37
54
 
38
55
  ```bash
@@ -62,6 +79,15 @@ The tape argument accepts a **preset name** (from `ace-demo list`) or a **direct
62
79
  ace-demo record path/to/tape.tape.yml --output path/to/output.gif
63
80
  ```
64
81
 
82
+ 5. **Preflight the camera contract for state-transition demos**:
83
+
84
+ Ask these questions before recording:
85
+
86
+ - Does the first part of the recording show the baseline state the reviewer needs?
87
+ - Will the trigger action happen on camera?
88
+ - Will the visible system reaction happen on camera?
89
+ - If the feature is tmux-related, will the recording clearly show the tmux window/pane/session change rather than only a later shell prompt?
90
+
65
91
  ### Record Inline (Ad-Hoc Commands)
66
92
 
67
93
  1. **Preview** generated tape content:
@@ -139,8 +165,44 @@ TEST_PATH=ace-bundle ace-demo record test
139
165
  | `--font-size <n>` | Font size — inline mode (default: 16) |
140
166
  | `--playback-speed <speed>` | Postprocess speed: `1x`, `2x`, `4x`, `8x` |
141
167
 
168
+ ## Verification and Recovery
169
+
170
+ For YAML/asciinema demos, recording is not complete when the GIF exists. The cast must pass semantic verification.
171
+
172
+ Use tape `verify:` rules to express:
173
+ - required exported variables (`require_vars`)
174
+ - forbidden output/error signatures (`forbid_output`)
175
+ - final-state assertions (`assert_commands`)
176
+
177
+ After recording:
178
+
179
+ 1. If verification passes, continue normally.
180
+ 2. If verification fails with `scenario_defect`:
181
+ - inspect the generated report in `.ace-local/demo/`
182
+ - run `ace-demo verify <cast> --tape <tape>` again if needed
183
+ - fix the tape instructions, viewpoint, or setup contract
184
+ - retry recording once
185
+ 3. If verification fails with `product_bug`:
186
+ - stop
187
+ - keep the generated report in `.ace-local/demo/`
188
+ - run the cast-analysis workflow and treat it as a real code/runtime bug
189
+ 4. Never upload or comment on a PR when verification is not a true pass.
190
+ 5. For any non-pass result, branch through `wfi://demo/analyze-cast` before re-recording.
191
+
192
+ ### Additional Validation for UX / Tmux Demos
193
+
194
+ Even when the recorder succeeds technically, reject the demo and re-record if any of these are true:
195
+
196
+ - the recording starts after the important state change already happened
197
+ - the recording shows only setup and end state, not the visible transition
198
+ - a tmux/window/pane feature is represented only by status files or textual explanation
199
+ - the viewer cannot identify the operator window, trigger action, and resulting fork/window/pane state from the recording alone
200
+
142
201
  ## Success Criteria
143
202
 
144
203
  - Recording file produced in `.ace-local/demo/` (plus optional retimed artifact)
145
- - If `--pr` used: demo uploaded to `demo-assets` release and comment posted on PR
146
- - If `--dry-run`: preview printed, no side effects
204
+ - YAML/asciinema recordings pass semantic verification
205
+ - If `--pr` used: demo uploaded to `demo-assets` release and comment posted on PR only after verification passes
206
+ - If verification fails: error report written to `.ace-local/demo/`
207
+ - If `--dry-run`: preview printed, no side effects
208
+ - For UI/state-transition demos, the recording visibly shows `before -> trigger -> visible effect -> after` from the correct viewer perspective
@@ -27,6 +27,8 @@ module Ace
27
27
  lines << "# Scene: #{scene_name}" unless scene_name.to_s.strip.empty?
28
28
 
29
29
  scene.fetch("commands", []).each do |command|
30
+ next unless command["type"]
31
+
30
32
  lines << command.fetch("type")
31
33
  sleep_value = validate_sleep!(command["sleep"] || default_timeout)
32
34
  lines << "sleep #{sleep_value}"
@@ -6,7 +6,7 @@ module Ace
6
6
  module Demo
7
7
  module Atoms
8
8
  module DemoYamlParser
9
- ALLOWED_ROOT_KEYS = %w[description tags settings setup scenes teardown].freeze
9
+ ALLOWED_ROOT_KEYS = %w[description tags settings setup scenes verify teardown].freeze
10
10
 
11
11
  module_function
12
12
 
@@ -35,6 +35,7 @@ module Ace
35
35
  "settings" => normalize_settings(data["settings"], source_path: source_path),
36
36
  "setup" => normalize_directives(data["setup"], "setup", source_path: source_path),
37
37
  "scenes" => normalize_scenes(data["scenes"], source_path: source_path),
38
+ "verify" => normalize_verify(data["verify"], source_path: source_path),
38
39
  "teardown" => normalize_directives(data["teardown"], "teardown", source_path: source_path)
39
40
  }
40
41
 
@@ -164,24 +165,142 @@ module Ace
164
165
  end
165
166
  private_class_method :normalize_scenes
166
167
 
168
+ def normalize_verify(verify, source_path:)
169
+ return {} if verify.nil?
170
+ raise DemoYamlParseError, "verify must be a map in #{source_path}" unless verify.is_a?(Hash)
171
+
172
+ normalized = {}
173
+ if verify.key?("allow_nonzero_exit")
174
+ normalized["allow_nonzero_exit"] = normalize_boolean(
175
+ verify["allow_nonzero_exit"],
176
+ "verify.allow_nonzero_exit",
177
+ source_path
178
+ )
179
+ end
180
+ normalized["require_vars"] = normalize_string_list(verify["require_vars"], "verify.require_vars", source_path) if verify.key?("require_vars")
181
+ normalized["require_output"] = normalize_string_list(verify["require_output"], "verify.require_output", source_path) if verify.key?("require_output")
182
+ if verify.key?("require_output_sequence")
183
+ normalized["require_output_sequence"] = normalize_string_list(
184
+ verify["require_output_sequence"],
185
+ "verify.require_output_sequence",
186
+ source_path
187
+ )
188
+ end
189
+ normalized["forbid_output"] = normalize_string_list(verify["forbid_output"], "verify.forbid_output", source_path) if verify.key?("forbid_output")
190
+ normalized["assert_commands"] = normalize_string_list(verify["assert_commands"], "verify.assert_commands", source_path) if verify.key?("assert_commands")
191
+ normalized
192
+ end
193
+ private_class_method :normalize_verify
194
+
195
+ def normalize_boolean(value, field, source_path)
196
+ return value if value == true || value == false
197
+
198
+ raise DemoYamlParseError, "#{field} must be a boolean in #{source_path}"
199
+ end
200
+ private_class_method :normalize_boolean
201
+
202
+ def normalize_string_list(value, field, source_path)
203
+ raise DemoYamlParseError, "#{field} must be an array in #{source_path}" unless value.is_a?(Array)
204
+
205
+ value.map.with_index do |item, index|
206
+ text = item&.to_s
207
+ if text.nil? || text.strip.empty?
208
+ raise DemoYamlParseError, "#{field}[#{index}] must be a non-empty string in #{source_path}"
209
+ end
210
+
211
+ text
212
+ end
213
+ end
214
+ private_class_method :normalize_string_list
215
+
167
216
  def normalize_command(command, scene_index, command_index, source_path:)
168
217
  unless command.is_a?(Hash)
169
218
  raise DemoYamlParseError,
170
219
  "scenes[#{scene_index}].commands[#{command_index}] must be a map in #{source_path}"
171
220
  end
172
221
 
173
- type = command["type"]&.to_s
174
- if type.nil? || type.strip.empty?
222
+ normalized = {"sleep" => command["sleep"]&.to_s}.compact
223
+ has_type = !command["type"].to_s.strip.empty?
224
+ has_tmux = command["tmux"].is_a?(Hash)
225
+
226
+ if has_type && has_tmux
175
227
  raise DemoYamlParseError,
176
- "scenes[#{scene_index}].commands[#{command_index}].type is required in #{source_path}"
228
+ "scenes[#{scene_index}].commands[#{command_index}] cannot define both type and tmux in #{source_path}"
177
229
  end
178
230
 
179
- {
180
- "type" => type,
181
- "sleep" => command["sleep"]&.to_s
182
- }
231
+ if has_type
232
+ normalized["type"] = command["type"].to_s
233
+ return normalized
234
+ end
235
+
236
+ if has_tmux
237
+ normalized["tmux"] = normalize_tmux_command(
238
+ command["tmux"],
239
+ scene_index,
240
+ command_index,
241
+ source_path: source_path
242
+ )
243
+ return normalized
244
+ end
245
+
246
+ raise DemoYamlParseError,
247
+ "scenes[#{scene_index}].commands[#{command_index}] must define either type or tmux in #{source_path}"
183
248
  end
184
249
  private_class_method :normalize_command
250
+
251
+ def normalize_tmux_command(tmux, scene_index, command_index, source_path:)
252
+ directive = tmux.transform_keys(&:to_s)
253
+ action = directive["action"]&.to_s&.strip
254
+ if action.nil? || action.empty?
255
+ raise DemoYamlParseError,
256
+ "scenes[#{scene_index}].commands[#{command_index}].tmux.action is required in #{source_path}"
257
+ end
258
+
259
+ allowed = %w[attach detach wait send]
260
+ unless allowed.include?(action)
261
+ raise DemoYamlParseError,
262
+ "Unknown tmux action '#{action}' in #{source_path}. Allowed: #{allowed.join(', ')}"
263
+ end
264
+
265
+ normalized = {"action" => action}
266
+ %w[session window pane pattern].each do |field|
267
+ normalized[field] = directive[field].to_s if directive.key?(field)
268
+ end
269
+
270
+ case action
271
+ when "attach", "detach"
272
+ normalized["session"] = required_string!(directive, "session", scene_index, command_index, source_path)
273
+ when "wait"
274
+ normalized["for"] = required_string!(directive, "for", scene_index, command_index, source_path)
275
+ normalized["pattern"] = normalized["pattern"] if normalized["pattern"]
276
+ normalized["timeout"] = Float(directive["timeout"]) if directive.key?("timeout")
277
+ when "send"
278
+ command_text = directive["command"]&.to_s&.strip
279
+ key_text = directive["key"]&.to_s&.strip
280
+ if command_text.to_s.empty? == key_text.to_s.empty?
281
+ raise DemoYamlParseError,
282
+ "tmux send must define exactly one of command or key in #{source_path}"
283
+ end
284
+ normalized["command"] = command_text unless command_text.to_s.empty?
285
+ normalized["key"] = key_text unless key_text.to_s.empty?
286
+ end
287
+
288
+ normalized
289
+ rescue ArgumentError, TypeError => e
290
+ raise DemoYamlParseError, "#{e.message} (#{source_path})"
291
+ end
292
+ private_class_method :normalize_tmux_command
293
+
294
+ def required_string!(directive, field, scene_index, command_index, source_path)
295
+ value = directive[field]&.to_s&.strip
296
+ if value.nil? || value.empty?
297
+ raise DemoYamlParseError,
298
+ "scenes[#{scene_index}].commands[#{command_index}].tmux.#{field} is required in #{source_path}"
299
+ end
300
+
301
+ value
302
+ end
303
+ private_class_method :required_string!
185
304
  end
186
305
  end
187
306
  end
@@ -34,11 +34,25 @@ module Ace
34
34
  raise ArgumentError, "Format 'webm' requires --backend vhs when recording YAML tapes"
35
35
  end
36
36
 
37
+ def validate_yaml_backend_capabilities!(backend:, spec:)
38
+ return unless backend == "vhs"
39
+ return unless yaml_uses_tmux_directives?(spec)
40
+
41
+ raise ArgumentError, "Backend 'vhs' does not support tmux directives in YAML tapes; use backend 'asciinema' or remove tmux commands"
42
+ end
43
+
37
44
  def validate_raw_tape_backend!(backend:)
38
45
  return if backend.nil? || backend == "vhs"
39
46
 
40
47
  raise ArgumentError, "Raw .tape recordings support backend 'vhs' only"
41
48
  end
49
+
50
+ def yaml_uses_tmux_directives?(spec)
51
+ Array(spec["scenes"]).any? do |scene|
52
+ Array(scene["commands"]).any? { |command| command.is_a?(Hash) && command["tmux"].is_a?(Hash) }
53
+ end
54
+ end
55
+ private_class_method :yaml_uses_tmux_directives?
42
56
  end
43
57
  end
44
58
  end
@@ -26,6 +26,8 @@ module Ace
26
26
  lines << "# Scene: #{scene_name}" unless scene_name.to_s.strip.empty?
27
27
 
28
28
  scene.fetch("commands", []).each do |command|
29
+ next unless command["type"]
30
+
29
31
  type_text = command.fetch("type")
30
32
  if type_text.include?('"') || type_text.include?("$") || type_text.include?("\\")
31
33
  lines << "Type `#{type_text}`"
@@ -28,6 +28,7 @@ module Ace
28
28
  allow_nil: false
29
29
  )
30
30
  RecordOptionValidator.validate_yaml_backend_format!(backend: selected_backend, format: selected_format)
31
+ RecordOptionValidator.validate_yaml_backend_capabilities!(backend: selected_backend, spec: spec)
31
32
 
32
33
  selected_speed = playback_speed.nil? ? settings["playback_speed"] : playback_speed
33
34
  selected_speed = Atoms::PlaybackSpeedParser.parse(selected_speed)