This was written agentically; verify its assertions and edit accordingly:
What happened
A real python sync.py --apply run aborted:
dep [0F5b] +1 dependency(ies)
dep [0F5c] +1 dependency(ies)
AgilePlace PATCH /card/2494062382 failed: HTTP 428 {"statusCode":428,...,"data":{"operations":[{"op":"test","path":"/version","value":"1","fromHeader":"x-lk-resource-version","error":"Test operation failed","actualValue":"2"}]}}
Root cause
patch_card already has a one-shot refetch-validate-retry for exactly this (409/428, issue #72). It could not fire.
_conflict_retry gates the retry on _changed_patch_paths, which resolves each queued op path through _card_value_for_patch_path. That function has no case for /description — it raises ValueError, which _changed_patch_paths deliberately counts as "changed". So the retry is refused and the original 428 re-raises out of sync.main(), aborting the run.
description_sync.sync_description queues /description on essentially every card whose text differs from its issue, so the conflict-recovery net is structurally dead for the ordinary sync flush path — any version bump on such a card aborts the whole run instead of retrying.
/externalLink has the same gap. It is already documented (intake._card_for_link_write, docs/API-VALIDATION.md "Reverse intake") and locally worked around with an explicit refetch — but the gap itself was never closed, so the main flush inherited it.
The version bump itself came from the run: step 4 (sync_dependencies -> POST /card/dependency) writes to cards before step 5 flushes their queued PATCH with the pre-POST snapshot version. Whether that POST is what bumps the version is unconfirmed against the live API — tracked separately; it does not change the fix here.
Fix
Teach _card_value_for_patch_path the two missing paths so the guard can actually compare them:
/description — the run snapshot already carries the observed value (board_reads.hydrate_run_reads hydrates card["description"] in place; created cards get it from _created_card_snapshot), so comparing snapshot vs. refetch is sound. A card whose description hydration failed carries no key and keeps failing closed, unchanged.
/externalLink — compare the singular field directly.
Unknown paths must keep failing closed. Add a test that enumerates every op-builder path in agilesync/ and asserts the guard understands it, so the next new op path cannot silently kill the retry net again.
Testing
🤖 Co-authored by Claude Opus 5.
This was written agentically; verify its assertions and edit accordingly:
What happened
A real
python sync.py --applyrun aborted:Root cause
patch_cardalready has a one-shot refetch-validate-retry for exactly this (409/428, issue #72). It could not fire._conflict_retrygates the retry on_changed_patch_paths, which resolves each queued op path through_card_value_for_patch_path. That function has no case for/description— it raisesValueError, which_changed_patch_pathsdeliberately counts as "changed". So the retry is refused and the original 428 re-raises out ofsync.main(), aborting the run.description_sync.sync_descriptionqueues/descriptionon essentially every card whose text differs from its issue, so the conflict-recovery net is structurally dead for the ordinary sync flush path — any version bump on such a card aborts the whole run instead of retrying./externalLinkhas the same gap. It is already documented (intake._card_for_link_write,docs/API-VALIDATION.md"Reverse intake") and locally worked around with an explicit refetch — but the gap itself was never closed, so the main flush inherited it.The version bump itself came from the run: step 4 (
sync_dependencies->POST /card/dependency) writes to cards before step 5 flushes their queued PATCH with the pre-POST snapshot version. Whether that POST is what bumps the version is unconfirmed against the live API — tracked separately; it does not change the fix here.Fix
Teach
_card_value_for_patch_paththe two missing paths so the guard can actually compare them:/description— the run snapshot already carries the observed value (board_reads.hydrate_run_readshydratescard["description"]in place; created cards get it from_created_card_snapshot), so comparing snapshot vs. refetch is sound. A card whose description hydration failed carries no key and keeps failing closed, unchanged./externalLink— compare the singular field directly.Unknown paths must keep failing closed. Add a test that enumerates every op-builder path in
agilesync/and asserts the guard understands it, so the next new op path cannot silently kill the retry net again.Testing
patch_cardwith a/descriptionop + a version bump retries with the fresh version (currently aborts)agilesync/python -m agilesync.tools.smokenew step passes on the real boardpython sync.py --applyrun completes🤖 Co-authored by Claude Opus 5.