Context
The protocol is strictly request/response; agents cannot react to session changes (playback position, tempo, clip states) without hand-rolled polling. src/ableton_cli/session_diff.py already implements a recursive snapshot diff (compute_session_diff). This issue adds a polling watch command emitting one JSON line per detected change. Push-based LOM listeners are explicitly deferred (would require protocol changes); polling + diff is the right first step.
Scope
- New CLI command
session watch:
- Options:
--interval-ms (default 500, min 50), --scope song|tracks|transport|all (default all), --count N (stop after N emitted diffs; required for testability, default unlimited), --include-position/--no-include-position (playback position changes every poll while playing; default excluded to avoid noise).
- Behavior: take snapshot via existing read commands for the scope → sleep interval → snapshot →
compute_session_diff(prev, cur) → if non-empty, print one compact JSON line {"ts": <iso8601>, "diff": {...}} and flush → repeat. SIGINT exits with code 0.
- Snapshot assembly lives in a pure function
build_watch_snapshot(client, scope) -> dict in a new src/ableton_cli/watch.py; filtering of position fields is a pure function strip_volatile(snapshot, include_position: bool).
- Read-only: side_effect metadata
kind: "read". No new remote commands.
Non-goals
- LOM listener push events, subscriptions over the TCP protocol, or protocol_version changes.
- Sub-50ms latency (use
batch stream for low-latency command loops instead; see SKILL.md).
TDD plan
- RED:
tests/test_watch.py — fake client returning a scripted sequence of snapshots; assert: no output when consecutive snapshots equal; one JSONL line with correct diff shape when tempo changes; --count 2 stops after two diffs; position excluded by default and included with the flag.
- RED: interval validation (
--interval-ms 10 → INVALID_ARGUMENT).
- GREEN, REFACTOR; regenerate snapshot/skill docs for the new CLI command.
Acceptance criteria
- Deterministic tests with zero sleeping (inject a fake clock/sleep function).
- Output is valid JSONL (one object per line, flushed), suitable for
jq and agent consumption.
- Harness layering clean (
watch.py in core imports only client protocol types, not commands).
Context
The protocol is strictly request/response; agents cannot react to session changes (playback position, tempo, clip states) without hand-rolled polling.
src/ableton_cli/session_diff.pyalready implements a recursive snapshot diff (compute_session_diff). This issue adds a polling watch command emitting one JSON line per detected change. Push-based LOM listeners are explicitly deferred (would require protocol changes); polling + diff is the right first step.Scope
session watch:--interval-ms(default 500, min 50),--scope song|tracks|transport|all(defaultall),--count N(stop after N emitted diffs; required for testability, default unlimited),--include-position/--no-include-position(playback position changes every poll while playing; default excluded to avoid noise).compute_session_diff(prev, cur)→ if non-empty, print one compact JSON line{"ts": <iso8601>, "diff": {...}}and flush → repeat. SIGINT exits with code 0.build_watch_snapshot(client, scope) -> dictin a newsrc/ableton_cli/watch.py; filtering of position fields is a pure functionstrip_volatile(snapshot, include_position: bool).kind: "read". No new remote commands.Non-goals
batch streamfor low-latency command loops instead; see SKILL.md).TDD plan
tests/test_watch.py— fake client returning a scripted sequence of snapshots; assert: no output when consecutive snapshots equal; one JSONL line with correct diff shape when tempo changes;--count 2stops after two diffs; position excluded by default and included with the flag.--interval-ms 10→ INVALID_ARGUMENT).Acceptance criteria
jqand agent consumption.watch.pyin core imports only client protocol types, not commands).