Add automations diff to preview a spec against the live automation - #12
Merged
jbedient-kizen merged 2 commits intoAug 17, 2026
Conversation
`automations update` was all-or-nothing: you either trusted the spec or eyeballed two full JSON trees. `diff` shows what the update would change — trigger/step additions, removals, reparenting, and field changes. The hard part is identity. GET and PUT are different dialects, so `key` is resynthesized from live order on one side and hand-authored on the other; comparing them literally reports every step as changed. Steps and triggers are matched by `id`, with `key`/`parent_key`/`prefix` excluded as per-side naming rather than content. Reparenting and `go_to_automation_step` references are resolved to the matched identity of their target, so they still surface a real change without tripping on a cosmetic rekey. A spec item carrying an `id` that matches no live step is treated as an addition, and the live step it would have displaced as a removal, rather than merged into it by position — that is what the PUT would actually do, and the previous position-merge silently swallowed the unknown id. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`diff_wire_payloads`'s docstring said its return value was "the same shape `roundtrip_automation`'s `drift` field already uses." The three keys match; the `path` convention does not. `drift` comes from `semantic_diff`, which identifies steps positionally (`steps[3].field`) and carries no id anywhere, so it is not a precedent for these id-octet-labelled paths. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
marshall-kizen
approved these changes
Aug 17, 2026
jbedient-kizen
merged commit Aug 17, 2026
6c10431
into
bug/automation-update-deactivates-without-asking
4 checks passed
Merged
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
automations updatewas all-or-nothing. To know what a spec would do to a liveautomation you either trusted it and wrote, or diffed two full JSON trees by
eye. Neither scales past a handful of steps, and the failure mode is silent:
an unintended reparenting or a dropped step looks like any other successful
write.
Solution
kizen automations diff <api_name> --spec-file <path>(stdin also accepted)builds the payload an
updatewould PUT and compares it to the liveautomation, writing nothing.
The hard part is identity, not comparison. GET and PUT are different dialects,
so
keyis resynthesized from live order on one side and hand-authored on theother — compared literally, every step reads as changed and the output is
worthless. So:
idfirst, regardless ofkeyor order.Position is a fallback only for a spec carrying no
ids at all.key/parent_key/prefixare excluded from the field comparison asper-side synthetic naming rather than automation content.
go_to_automation_stepreferences are resolved to theirtarget's matched identity, so a genuine retarget still surfaces while a
cosmetic rekey does not.
Each line is labelled with the first octet of the step/trigger's
id, whichmatches what the UI shows and is unique within one automation.
One behaviour decision worth pushing on: a spec item carrying an
idthatmatches no live step is now treated as an addition, and the live step it would
have displaced as a removal — rather than merged into that step by position.
This is what the PUT would actually do, and the previous position-merge pinned
both sides'
idto the live value, silently swallowing the unknown one andrendering it as "these fields changed."
Testing
check.sh builder-clifully green:1128 passed, 4 skipped, 64 deselected;lint, format, typecheck, and the CLI-tree
extra_checksall pass.Both behavioural fixes were proven by disabling the fix and watching the new
test fail, then restoring it:
test_diff_wire_payloads_ignores_go_to_key_resynthesisand
test_diff_wire_payloads_reports_go_to_retargetboth fail.test_diff_wire_payloads_dangling_spec_id_is_addition_not_editfails with a
KeyErroron the expected removal, while the other nine wiretests pass under both behaviours, confirming the change is narrowly scoped.
Also sanity-checked read-only against a live automation in
cli-testing:rebuilding a spec from
automations show --jsonand diffing it returned anempty diff, with the automation's
revisionunchanged before and after.Design notes / tradeoffs
activeresolution is duplicated rather than shared withplan_update_automation, deliberately: sharing it would putdiffon thatfunction's single-fetch path and cost a second live fetch. Worth extracting
once both call sites have settled.
--jsonentries carry only the id octet, not a separate fullidkey. Additions and removals carry the whole step/trigger including itsfull
id; every other command's--json(automations get,show) carriesfull ids throughout, so this is scoped to
diff's changed-field entries.An earlier revision of this note justified that by "keeping the shape
identical to
roundtrip's existingdriftfield" — the three keys do match,but
driftnumbers steps positionally and carries no id at all, so it is noprecedent for octet-only identity. What this actually rests on is that the
octet is unique within one automation, plus not wanting an
idkey that ispresent on step/trigger entries and absent on top-level automation-field
entries like
{"path": "name"}. That is a weaker guarantee than simplyincluding the id — say so and it gets added.
--output csvhas noidcolumn. Out of scope here; worth a follow-up.🤖 Generated with Claude Code