booking-agent-cli 0.5.0__tar.gz → 0.6.0__tar.gz

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.
Files changed (223) hide show
  1. {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/PKG-INFO +2 -2
  2. {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/README.md +1 -1
  3. {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/pyproject.toml +1 -1
  4. {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/src/booking_agent/__init__.py +1 -1
  5. {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/src/booking_agent/defaults.env +3 -0
  6. {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/src/booking_agent_cli/__init__.py +1 -1
  7. {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/src/booking_agent_cli/bundled_skill.md +21 -21
  8. {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/src/booking_agent_cli/main.py +9 -0
  9. {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/src/booking_agent_cli/runtime_prepare.py +180 -16
  10. {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/tests/integration/test_regression_index.py +8 -1
  11. {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/tests/integration/test_runtime_prepare.py +103 -9
  12. {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/.gitignore +0 -0
  13. {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/src/booking_agent/authorization.py +0 -0
  14. {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/src/booking_agent/backend/__init__.py +0 -0
  15. {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/src/booking_agent/backend/live.py +0 -0
  16. {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/src/booking_agent/calendar/__init__.py +0 -0
  17. {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/src/booking_agent/calendar/filter.py +0 -0
  18. {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/src/booking_agent/calendar/google.py +0 -0
  19. {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/src/booking_agent/calendar/oauth_listener.py +0 -0
  20. {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/src/booking_agent/calendar/scope.py +0 -0
  21. {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/src/booking_agent/circuit_breaker.py +0 -0
  22. {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/src/booking_agent/config.py +0 -0
  23. {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/src/booking_agent/discovery.py +0 -0
  24. {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/src/booking_agent/headless.py +0 -0
  25. {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/src/booking_agent/http_deadlines.py +0 -0
  26. {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/src/booking_agent/http_retry.py +0 -0
  27. {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/src/booking_agent/http_trace.py +0 -0
  28. {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/src/booking_agent/merchant_pacing.py +0 -0
  29. {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/src/booking_agent/models/__init__.py +0 -0
  30. {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/src/booking_agent/models/platform.py +0 -0
  31. {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/src/booking_agent/models/wire.py +0 -0
  32. {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/src/booking_agent/namespace.py +0 -0
  33. {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/src/booking_agent/observability.py +0 -0
  34. {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/src/booking_agent/platform_profile.py +0 -0
  35. {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/src/booking_agent/pop.py +0 -0
  36. {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/src/booking_agent/profile_cache.py +0 -0
  37. {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/src/booking_agent/registry_url.py +0 -0
  38. {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/src/booking_agent/rest_client.py +0 -0
  39. {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/src/booking_agent/secure_file.py +0 -0
  40. {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/src/booking_agent/settings.py +0 -0
  41. {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/src/booking_agent/signals.py +0 -0
  42. {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/src/booking_agent/step_timing.py +0 -0
  43. {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/src/booking_agent/ucp_capabilities.py +0 -0
  44. {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/src/booking_agent/ucp_checkout.py +0 -0
  45. {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/src/booking_agent/ucp_negotiation.py +0 -0
  46. {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/src/booking_agent/ucp_profile.py +0 -0
  47. {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/src/booking_agent/usp_methods.py +0 -0
  48. {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/src/booking_agent_cli/activity_log.py +0 -0
  49. {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/src/booking_agent_cli/booking_poll.py +0 -0
  50. {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/src/booking_agent_cli/booking_summary.py +0 -0
  51. {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/src/booking_agent_cli/calendar_connection.py +0 -0
  52. {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/src/booking_agent_cli/calendar_event.py +0 -0
  53. {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/src/booking_agent_cli/calendar_preference.py +0 -0
  54. {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/src/booking_agent_cli/catalog.py +0 -0
  55. {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/src/booking_agent_cli/checkout_prereq.py +0 -0
  56. {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/src/booking_agent_cli/commands/__init__.py +0 -0
  57. {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/src/booking_agent_cli/commands/config.py +0 -0
  58. {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/src/booking_agent_cli/commands/flow.py +0 -0
  59. {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/src/booking_agent_cli/commands/lowlevel.py +0 -0
  60. {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/src/booking_agent_cli/commands/runtime.py +0 -0
  61. {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/src/booking_agent_cli/commands/update.py +0 -0
  62. {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/src/booking_agent_cli/config_loader.py +0 -0
  63. {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/src/booking_agent_cli/engine.py +0 -0
  64. {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/src/booking_agent_cli/flow_timings.py +0 -0
  65. {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/src/booking_agent_cli/geo_eligibility.py +0 -0
  66. {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/src/booking_agent_cli/geocode.py +0 -0
  67. {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/src/booking_agent_cli/identity.py +0 -0
  68. {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/src/booking_agent_cli/install_integrity.py +0 -0
  69. {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/src/booking_agent_cli/link_cli.py +0 -0
  70. {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/src/booking_agent_cli/output.py +0 -0
  71. {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/src/booking_agent_cli/payment_helpers.py +0 -0
  72. {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/src/booking_agent_cli/payment_token.py +0 -0
  73. {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/src/booking_agent_cli/provenance.py +0 -0
  74. {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/src/booking_agent_cli/reschedule.py +0 -0
  75. {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/src/booking_agent_cli/runtime.py +0 -0
  76. {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/src/booking_agent_cli/search_area.py +0 -0
  77. {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/src/booking_agent_cli/session.py +0 -0
  78. {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/src/booking_agent_cli/skill_bundle.py +0 -0
  79. {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/src/booking_agent_cli/skill_integrity.py +0 -0
  80. {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/src/booking_agent_cli/support_bundle_schema.py +0 -0
  81. {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/src/booking_agent_cli/support_report.py +0 -0
  82. {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/src/booking_agent_cli/timezone_context.py +0 -0
  83. {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/src/booking_agent_cli/updates.py +0 -0
  84. {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/tests/.env.tests +0 -0
  85. {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/tests/conftest.py +0 -0
  86. {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/tests/e2e/__init__.py +0 -0
  87. {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/tests/e2e/test_cancel_reschedule_e2e.py +0 -0
  88. {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/tests/e2e/test_latency_soak.py +0 -0
  89. {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/tests/e2e/test_staging_registry_integration.py +0 -0
  90. {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/tests/e2e/test_timezone_booking_e2e.py +0 -0
  91. {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/tests/e2e/test_ucp_native_e2e.py +0 -0
  92. {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/tests/e2e/test_ucp_skill_flow_e2e.py +0 -0
  93. {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/tests/e2e/ucp_native.py +0 -0
  94. {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/tests/integration/__init__.py +0 -0
  95. {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/tests/integration/test_activity_log.py +0 -0
  96. {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/tests/integration/test_at_rest_hygiene.py +0 -0
  97. {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/tests/integration/test_booking_rejection.py +0 -0
  98. {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/tests/integration/test_bulk_cancel.py +0 -0
  99. {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/tests/integration/test_calendar_connection.py +0 -0
  100. {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/tests/integration/test_calendar_connector.py +0 -0
  101. {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/tests/integration/test_calendar_flow.py +0 -0
  102. {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/tests/integration/test_calendar_preference.py +0 -0
  103. {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/tests/integration/test_checkout_signals.py +0 -0
  104. {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/tests/integration/test_circuit_breaker.py +0 -0
  105. {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/tests/integration/test_config.py +0 -0
  106. {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/tests/integration/test_correlation_flow.py +0 -0
  107. {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/tests/integration/test_declined_spt.py +0 -0
  108. {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/tests/integration/test_discover_registry_rejection.py +0 -0
  109. {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/tests/integration/test_flow_evidence.py +0 -0
  110. {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/tests/integration/test_flow_gates.py +0 -0
  111. {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/tests/integration/test_flow_reschedule.py +0 -0
  112. {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/tests/integration/test_flow_step_timings.py +0 -0
  113. {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/tests/integration/test_google_calendar_oauth.py +0 -0
  114. {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/tests/integration/test_home_visit_address.py +0 -0
  115. {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/tests/integration/test_http_trace.py +0 -0
  116. {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/tests/integration/test_identity_logout.py +0 -0
  117. {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/tests/integration/test_install_integrity.py +0 -0
  118. {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/tests/integration/test_lazy_oauth.py +0 -0
  119. {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/tests/integration/test_link_auth.py +0 -0
  120. {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/tests/integration/test_link_cli_spt.py +0 -0
  121. {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/tests/integration/test_namespace.py +0 -0
  122. {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/tests/integration/test_oauth_listener.py +0 -0
  123. {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/tests/integration/test_oauth_metadata.py +0 -0
  124. {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/tests/integration/test_platform_key_pop.py +0 -0
  125. {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/tests/integration/test_platform_profile.py +0 -0
  126. {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/tests/integration/test_pop_keystore.py +0 -0
  127. {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/tests/integration/test_profile_cache.py +0 -0
  128. {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/tests/integration/test_release_hold_uses_delete.py +0 -0
  129. {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/tests/integration/test_request_retry_semantics.py +0 -0
  130. {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/tests/integration/test_search_area.py +0 -0
  131. {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/tests/integration/test_session_compat.py +0 -0
  132. {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/tests/integration/test_session_retention.py +0 -0
  133. {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/tests/integration/test_skill_integrity.py +0 -0
  134. {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/tests/integration/test_skill_portability.py +0 -0
  135. {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/tests/integration/test_support_report.py +0 -0
  136. {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/tests/integration/test_timezone_context.py +0 -0
  137. {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/tests/integration/test_ucp_cancel_result.py +0 -0
  138. {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/tests/integration/test_ucp_checkout_pop.py +0 -0
  139. {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/tests/integration/test_ucp_checkout_proof_wireup.py +0 -0
  140. {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/tests/integration/test_ucp_checkout_validate.py +0 -0
  141. {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/tests/integration/test_ucp_profile.py +0 -0
  142. {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/tests/integration/test_update_command.py +0 -0
  143. {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/tests/integration/test_wheel_contents.py +0 -0
  144. {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/tests/spec_schemas/SOURCE +0 -0
  145. {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/tests/spec_schemas/acp_booking_extension.json +0 -0
  146. {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/tests/spec_schemas/availability.json +0 -0
  147. {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/tests/spec_schemas/booking.json +0 -0
  148. {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/tests/spec_schemas/calendar_freebusy.json +0 -0
  149. {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/tests/spec_schemas/catalog.json +0 -0
  150. {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/tests/spec_schemas/paid_bookings.json +0 -0
  151. {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/tests/spec_schemas/profile.json +0 -0
  152. {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/tests/spec_schemas/registry.json +0 -0
  153. {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/tests/spec_schemas/rest_common.json +0 -0
  154. {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/tests/spec_schemas/usp.json +0 -0
  155. {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/tests/spec_schemas/waitlist.json +0 -0
  156. {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/tests/spec_schemas/webhook_event.json +0 -0
  157. {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/tests/support/__init__.py +0 -0
  158. {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/tests/support/fixtures/__init__.py +0 -0
  159. {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/tests/support/fixtures/profiles.py +0 -0
  160. {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/tests/support/fixtures/provenance/pypi_attestations-0.0.30-py3-none-any.whl.provenance.json +0 -0
  161. {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/tests/support/fixtures/services.py +0 -0
  162. {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/tests/support/mock/__init__.py +0 -0
  163. {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/tests/support/mock/backend.py +0 -0
  164. {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/tests/support/mock/link_cli.py +0 -0
  165. {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/tests/support/mock/mock_calendar.py +0 -0
  166. {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/tests/support/mock/pop_business.py +0 -0
  167. {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/tests/support/mock/pop_transport.py +0 -0
  168. {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/tests/support/mock/ucp_checkout_store.py +0 -0
  169. {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/tests/support/mock/ucp_server.py +0 -0
  170. {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/tests/support/offline_stack/__init__.py +0 -0
  171. {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/tests/support/offline_stack/stack.py +0 -0
  172. {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/tests/unit/__init__.py +0 -0
  173. {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/tests/unit/test_authorization_policy.py +0 -0
  174. {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/tests/unit/test_booking_poll_credentials.py +0 -0
  175. {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/tests/unit/test_booking_request_error.py +0 -0
  176. {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/tests/unit/test_calendar_filter.py +0 -0
  177. {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/tests/unit/test_catalog.py +0 -0
  178. {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/tests/unit/test_check_version.py +0 -0
  179. {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/tests/unit/test_conditional_writes.py +0 -0
  180. {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/tests/unit/test_delivery_address.py +0 -0
  181. {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/tests/unit/test_discovery.py +0 -0
  182. {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/tests/unit/test_geo_eligibility.py +0 -0
  183. {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/tests/unit/test_geocode.py +0 -0
  184. {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/tests/unit/test_headless.py +0 -0
  185. {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/tests/unit/test_http_deadlines.py +0 -0
  186. {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/tests/unit/test_http_retry.py +0 -0
  187. {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/tests/unit/test_live_search_registry.py +0 -0
  188. {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/tests/unit/test_merchant_pacing.py +0 -0
  189. {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/tests/unit/test_mock_support.py +0 -0
  190. {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/tests/unit/test_money_path_events.py +0 -0
  191. {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/tests/unit/test_nonce_from_pop_error_body.py +0 -0
  192. {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/tests/unit/test_observability.py +0 -0
  193. {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/tests/unit/test_outbound_member_names.py +0 -0
  194. {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/tests/unit/test_package_layering.py +0 -0
  195. {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/tests/unit/test_provenance.py +0 -0
  196. {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/tests/unit/test_registry_search_filters.py +0 -0
  197. {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/tests/unit/test_registry_search_pricing.py +0 -0
  198. {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/tests/unit/test_registry_search_responses.py +0 -0
  199. {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/tests/unit/test_registry_url.py +0 -0
  200. {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/tests/unit/test_release_notes.py +0 -0
  201. {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/tests/unit/test_rest_client.py +0 -0
  202. {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/tests/unit/test_scoped_method_authorization.py +0 -0
  203. {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/tests/unit/test_session_credentials.py +0 -0
  204. {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/tests/unit/test_spec_conformance.py +0 -0
  205. {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/tests/unit/test_step_timing.py +0 -0
  206. {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/tests/unit/test_ucp_capabilities.py +0 -0
  207. {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/tests/unit/test_ucp_complete_outcome.py +0 -0
  208. {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/tests/unit/test_ucp_create_price_guard.py +0 -0
  209. {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/tests/unit/test_ucp_negotiation.py +0 -0
  210. {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/tests/unit/test_updates.py +0 -0
  211. {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/tests/unit/test_wire_model_conformance.py +0 -0
  212. {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/tests/vectors/pop/001-jcs-nested-buyer.json +0 -0
  213. {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/tests/vectors/pop/002-jcs-meta-only.json +0 -0
  214. {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/tests/vectors/pop/003-jcs-unicode-empty-meta.json +0 -0
  215. {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/tests/vectors/pop/004-proof-issuance-no-ath.json +0 -0
  216. {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/tests/vectors/pop/005-proof-presentation-ath-nonce.json +0 -0
  217. {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/tests/vectors/pop/006-cross-key-replay.json +0 -0
  218. {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/tests/vectors/pop/007-downgrade-cnf-without-proof.json +0 -0
  219. {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/tests/vectors/pop/008-replayed-jti.json +0 -0
  220. {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/tests/vectors/pop/009-aud-mismatch.json +0 -0
  221. {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/tests/vectors/pop/010-alg-none.json +0 -0
  222. {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/tests/vectors/pop/README.md +0 -0
  223. {booking_agent_cli-0.5.0 → booking_agent_cli-0.6.0}/tests/vectors/pop/SOURCE +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: booking-agent-cli
3
- Version: 0.5.0
3
+ Version: 0.6.0
4
4
  Summary: The Booking Agent CLI: a stateful USP booking flow with hard gates, plus its USP protocol library.
5
5
  Project-URL: Homepage, https://github.com/wix/booking-agent
6
6
  Project-URL: Documentation, https://github.com/wix/booking-agent/blob/master/booking-agent/README.md
@@ -78,7 +78,7 @@ Each release also publishes that skill at an immutable `skill-v<version>` tag an
78
78
  the release workflow, so a harness can pin and verify the exact text it follows:
79
79
 
80
80
  ```bash
81
- curl -fsSLO https://raw.githubusercontent.com/wix/booking-agent/skill-v0.53.0/booking-agent/skills/booking-agent/SKILL.md
81
+ curl -fsSLO https://raw.githubusercontent.com/wix/booking-agent/skill-v0.54.0/booking-agent/skills/booking-agent/SKILL.md
82
82
  gh attestation verify SKILL.md --repo wix/booking-agent \
83
83
  --signer-workflow wix/booking-agent/.github/workflows/release.yml
84
84
  ```
@@ -36,7 +36,7 @@ Each release also publishes that skill at an immutable `skill-v<version>` tag an
36
36
  the release workflow, so a harness can pin and verify the exact text it follows:
37
37
 
38
38
  ```bash
39
- curl -fsSLO https://raw.githubusercontent.com/wix/booking-agent/skill-v0.53.0/booking-agent/skills/booking-agent/SKILL.md
39
+ curl -fsSLO https://raw.githubusercontent.com/wix/booking-agent/skill-v0.54.0/booking-agent/skills/booking-agent/SKILL.md
40
40
  gh attestation verify SKILL.md --repo wix/booking-agent \
41
41
  --signer-workflow wix/booking-agent/.github/workflows/release.yml
42
42
  ```
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "booking-agent-cli"
3
- version = "0.5.0"
3
+ version = "0.6.0"
4
4
  description = "The Booking Agent CLI: a stateful USP booking flow with hard gates, plus its USP protocol library."
5
5
  readme = "README.md"
6
6
  license = "Apache-2.0"
@@ -3,4 +3,4 @@
3
3
  Ships in the booking-agent-cli wheel and never imports ``booking_agent_cli``.
4
4
  """
5
5
 
6
- __version__ = "0.5.0"
6
+ __version__ = "0.6.0"
@@ -14,6 +14,9 @@ USP_PLATFORM_CONTACT=support@booking-agent.dev
14
14
  # Sessions, tokens, and traces. Holds the user .env, so override it only in the
15
15
  # process environment.
16
16
  BOOKING_AGENT_SESSION_DIR=~/.booking-agent
17
+ # Absolute folder a host keeps between runs. When set, booking-agent links its state and the
18
+ # Link sign-in into it by itself before every command.
19
+ # BOOKING_AGENT_PERSISTENT_ROOT=
17
20
  BOOKING_AGENT_MAX_SESSIONS=30
18
21
  LINK_CLI_BIN=link-cli
19
22
 
@@ -1,3 +1,3 @@
1
1
  """booking_agent_cli: typer-based stateful CLI for USP booking flows."""
2
2
 
3
- __version__ = "0.5.0"
3
+ __version__ = "0.6.0"
@@ -1,5 +1,5 @@
1
1
  ---
2
- version: 0.53.0
2
+ version: 0.54.0
3
3
  name: booking-agent
4
4
  description: |
5
5
  Books time-based services via USP from a Link wallet. Use when the user wants to book, schedule, reserve, or pay for a service from a business (haircut, lesson, repair, class, appointment, consult). The `booking-agent` CLI owns the booking state machine and refuses to proceed when human gates are skipped. Sign the user in to Link on your own, right after install and at the start of every booking (`booking-agent flow link-auth connect`, then start `booking-agent flow link-auth poll` in the background and post the sign-in URL in the same turn, exactly like `pay poll`); never wait for the user to ask to sign in or to say they approved. After `booking-agent flow business use`, follow `next_command` through `use-ucp-checkout-mode`; businesses without native checkout fail closed. Payment uses `booking-agent flow pay request` (default returns immediately with the approval URL), then start `booking-agent flow pay poll` in the background and post the approval URL to the user in the same turn; the poll finishes checkout once they approve. Confirm through `booking-agent flow book poll --wait`; never start an inbound webhook. Surface the approval URL exactly once. Personal identification (name, phone, email, address) must always come from the user's Link account via `link-cli`, never from chat or placeholders. Before any location-based search, the CLI proposes the Link shipping address and the user confirms it or gives another; online services skip this. Talk to the user in plain everyday language only; never expose CLI, UCP, auth, session, or other implementation details unless they explicitly ask.
@@ -19,7 +19,7 @@ metadata:
19
19
  - link-cli
20
20
  install:
21
21
  - kind: uv
22
- package: "booking-agent-cli==0.5.0"
22
+ package: "booking-agent-cli==0.6.0"
23
23
  bins: [booking-agent]
24
24
  user-invocable: true
25
25
  ---
@@ -220,7 +220,7 @@ To take a fixed, published version instead of the latest, fetch it at the releas
220
220
  workflow signs it under, and check the signature before you follow it:
221
221
 
222
222
  ```bash
223
- curl -fsSLO https://raw.githubusercontent.com/wix/booking-agent/skill-v0.53.0/booking-agent/skills/booking-agent/SKILL.md
223
+ curl -fsSLO https://raw.githubusercontent.com/wix/booking-agent/skill-v0.54.0/booking-agent/skills/booking-agent/SKILL.md
224
224
  gh attestation verify SKILL.md \
225
225
  --repo wix/booking-agent \
226
226
  --signer-workflow wix/booking-agent/.github/workflows/release.yml
@@ -245,17 +245,17 @@ before installing:
245
245
 
246
246
  ```bash
247
247
  dir="$HOME/.booking-agent/verified-wheels"
248
- uvx pip download "booking-agent-cli==0.5.0" --no-deps --only-binary :all: -d "$dir"
248
+ uvx pip download "booking-agent-cli==0.6.0" --no-deps --only-binary :all: -d "$dir"
249
249
  uvx pypi-attestations==0.0.30 verify pypi --repository https://github.com/wix/booking-agent \
250
- "$dir/booking_agent_cli-0.5.0-py3-none-any.whl"
251
- uv tool install --force "$dir/booking_agent_cli-0.5.0-py3-none-any.whl"
250
+ "$dir/booking_agent_cli-0.6.0-py3-none-any.whl"
251
+ uv tool install --force "$dir/booking_agent_cli-0.6.0-py3-none-any.whl"
252
252
 
253
253
  booking-agent --help
254
254
  ```
255
255
 
256
256
  Install only the wheel whose verify step passed, never one that failed it. If `uvx` is not
257
- available, install the same pinned release with `uv tool install booking-agent-cli==0.5.0` or
258
- `pipx install booking-agent-cli==0.5.0`; the CLI checks its own files against the signed release
257
+ available, install the same pinned release with `uv tool install booking-agent-cli==0.6.0` or
258
+ `pipx install booking-agent-cli==0.6.0`; the CLI checks its own files against the signed release
259
259
  on the first `booking-agent flow init` and refuses to book or pay if they do not match.
260
260
 
261
261
  Harnesses that read the `install` block in this skill's metadata do this for you. Do not go on
@@ -275,22 +275,22 @@ npm install -g @stripe/link-cli
275
275
 
276
276
  ### After installing
277
277
 
278
- **Keep state between conversations (only when this runtime needs it).** When this runtime's
279
- filesystem starts fresh in each conversation and it gives you a folder that survives between
280
- conversations, run this once per conversation, before `booking-agent flow sessions list`:
278
+ **Keep state between conversations.** When the host names a folder that survives between
279
+ conversations in `BOOKING_AGENT_PERSISTENT_ROOT`, the CLI links its state and the Link sign-in into
280
+ it by itself before every command: do nothing and never mention it. Only when the variable is unset
281
+ and this runtime gives you such a folder, run this once per conversation, before `booking-agent flow
282
+ sessions list`:
281
283
 
282
284
  ```bash
283
285
  booking-agent runtime prepare --persistent-root <absolute folder that survives>
284
286
  ```
285
287
 
286
- It links the booking state and the Link sign-in into that folder, so saved bookings, calendar
287
- preferences, and the sign-in carry over. Use the same folder every time; a different one, or one
288
- already named `booking-agent`, moves the existing state along instead of splitting it. Add `--link
289
- <runtime folder>=<persistent folder>` for each extra cache folder the runtime asks you to keep (a
290
- relative persistent folder is placed under the root). If the user is asked to sign in or reconnect
291
- right after a restart, run `booking-agent runtime doctor` and follow its `next_command`. When the
292
- runtime keeps its files anyway, or offers no surviving folder, skip the command. Do not tell the
293
- user about it. If it fails, continue the booking without it.
288
+ Use the same folder every time; it merges what each folder holds (booking files by session, the
289
+ newer copy of anything else) and deletes nothing. Add `--link <runtime folder>=<persistent folder>`
290
+ for each extra cache folder the runtime asks you to keep. If the user is asked to sign in or
291
+ reconnect right after a restart, run `booking-agent runtime doctor` and follow its `next_command`.
292
+ When the runtime keeps its files anyway, skip all of this. Do not tell the user about it. If it
293
+ fails, continue the booking without it.
294
294
 
295
295
  **Sign the user in to Link**, as described in [Link sign-in](#link-sign-in-automatic). It works
296
296
  with no booking in progress. Do not wait for the user to offer to sign in.
@@ -309,7 +309,7 @@ prints each effective setting and where it came from; `booking-agent config init
309
309
 
310
310
  The CLI decides whether a newer version is worth mentioning; you never decide from version
311
311
  numbers or release pages. Pass this skill's version on every `flow init` (`--skill-version
312
- 0.53.0`, from the frontmatter above), so the CLI can tell when these instructions and the
312
+ 0.54.0`, from the frontmatter above), so the CLI can tell when these instructions and the
313
313
  installed CLI no longer match.
314
314
 
315
315
  - When a command returns `awaiting_user: update_consent`, ask the question in `prompt` in plain
@@ -471,7 +471,7 @@ confirmation email or on the business's page (`booking_url`). Do not invent a lo
471
471
  **Step 1b — Create a new session:**
472
472
 
473
473
  ```bash
474
- booking-agent flow init --user-request "<user's exact request>" --skill-version 0.53.0 \
474
+ booking-agent flow init --user-request "<user's exact request>" --skill-version 0.54.0 \
475
475
  --skill-path "<absolute path of the SKILL.md you loaded>"
476
476
  ```
477
477
 
@@ -29,6 +29,15 @@ app.add_typer(runtime_app, name="runtime")
29
29
  app.add_typer(skill_app, name="skill")
30
30
  app.add_typer(update_app, name="update")
31
31
 
32
+ @app.callback()
33
+ def _main(ctx: typer.Context) -> None:
34
+ """Keep state in the host's persistent folder (when it names one) before any command runs."""
35
+ if ctx.invoked_subcommand != "runtime":
36
+ from .runtime_prepare import auto_prepare
37
+
38
+ auto_prepare()
39
+
40
+
32
41
  INTERNAL_ERROR_MESSAGE = (
33
42
  "The booking assistant hit an unexpected internal error. Tell the user in plain "
34
43
  "words that something went wrong on our side, then follow next_command."
@@ -10,6 +10,7 @@ deleted, so file modes on credentials and keys stay as they were.
10
10
  """
11
11
  from __future__ import annotations
12
12
 
13
+ import json
13
14
  import os
14
15
  import shutil
15
16
  import sys
@@ -18,11 +19,19 @@ from pathlib import Path
18
19
  from typing import Dict, List, Optional, Tuple
19
20
 
20
21
  from booking_agent import settings
21
- from booking_agent.secure_file import ensure_private_dir
22
+ from booking_agent.secure_file import ensure_private_dir, write_private_text
22
23
 
23
24
  STATE_SUBDIR = "booking-agent"
24
25
  LINK_SUBDIR = "link-cli"
25
26
  _LINK_CLI_APP = "link-cli-nodejs"
27
+ ROOT_ENV = "BOOKING_AGENT_PERSISTENT_ROOT"
28
+ _SESSION_FILE = ".usp-session.json"
29
+ # Order a booking moves through, used to keep the further-along copy of the same session.
30
+ _PHASE_ORDER = (
31
+ "INIT", "PREREQ_OK", "BUSINESS_SELECTED", "CALENDAR_GATE", "AVAILABILITY_QUERIED",
32
+ "SLOTS_PRESENTED", "SLOT_SELECTED", "BUYER_RESOLVED", "BOOKED", "PAYMENT_PENDING",
33
+ "CONFIRMED", "FAILED",
34
+ )
26
35
 
27
36
 
28
37
  class PrepareError(ValueError):
@@ -75,6 +84,117 @@ def _is_empty_dir(path: Path) -> bool:
75
84
  return path.is_dir() and not any(path.iterdir())
76
85
 
77
86
 
87
+ def _phase_rank(session: Dict[str, object]) -> int:
88
+ phase = session.get("flow_phase")
89
+ return _PHASE_ORDER.index(phase) if phase in _PHASE_ORDER else -1
90
+
91
+
92
+ def merge_session_stores(newer: Dict[str, object], older: Dict[str, object]) -> Dict[str, object]:
93
+ """Union of two saved booking files, by session id, without dropping any booking.
94
+
95
+ A session present in both keeps the copy that got further (a stale ``INIT`` snapshot never
96
+ hides the finished booking). Everything else at the top level comes from ``newer`` and falls
97
+ back to ``older``. Works on the raw JSON so fields this version does not know survive.
98
+ """
99
+ merged: Dict[str, object] = {**older, **{k: v for k, v in newer.items() if v is not None}}
100
+ by_id: Dict[object, Dict[str, object]] = {}
101
+ order: List[object] = []
102
+ for source in (older, newer):
103
+ for session in source.get("sessions") or []:
104
+ if not isinstance(session, dict):
105
+ continue
106
+ sid = session.get("id")
107
+ if sid not in by_id:
108
+ by_id[sid] = session
109
+ order.append(sid)
110
+ elif _phase_rank(session) >= _phase_rank(by_id[sid]):
111
+ by_id[sid] = session
112
+ merged["sessions"] = [by_id[sid] for sid in order]
113
+ for key in ("sessions_created", "schema_version"):
114
+ values = [v for v in (newer.get(key), older.get(key)) if isinstance(v, int)]
115
+ if values:
116
+ merged[key] = max(values)
117
+ answered = []
118
+ for source in (older, newer):
119
+ for item in source.get("update_prompts_answered") or []:
120
+ if isinstance(item, str) and item not in answered:
121
+ answered.append(item)
122
+ if answered:
123
+ merged["update_prompts_answered"] = answered
124
+ active = newer.get("active_session_id") or older.get("active_session_id")
125
+ merged["active_session_id"] = active if active in by_id else (order[-1] if order else None)
126
+ return merged
127
+
128
+
129
+ def _merge_session_files(src: Path, dst: Path) -> None:
130
+ def _load(path: Path) -> Dict[str, object]:
131
+ try:
132
+ raw = json.loads(path.read_text())
133
+ return raw if isinstance(raw, dict) else {}
134
+ except (OSError, ValueError):
135
+ return {}
136
+
137
+ src_newer = src.stat().st_mtime >= dst.stat().st_mtime
138
+ a, b = _load(src), _load(dst)
139
+ merged = merge_session_stores(a, b) if src_newer else merge_session_stores(b, a)
140
+ write_private_text(dst, json.dumps(merged, indent=2, sort_keys=False))
141
+ src.unlink()
142
+
143
+
144
+ def _merge_tree(src: Path, dst: Path, aside: Path, kept: List[str], rel: str = "") -> None:
145
+ """Move everything from ``src`` into ``dst``; nothing is deleted.
146
+
147
+ A name only in ``src`` moves over. Two folders merge. Two booking files merge by session.
148
+ Any other pair keeps the newer file at ``dst`` and moves the older one under ``aside``.
149
+ """
150
+ for child in sorted(src.iterdir()):
151
+ target = dst / child.name
152
+ here = f"{rel}/{child.name}" if rel else child.name
153
+ if not (target.exists() or target.is_symlink()):
154
+ shutil.move(str(child), str(target))
155
+ elif child.is_dir() and target.is_dir():
156
+ _merge_tree(child, target, aside, kept, here)
157
+ if _is_empty_dir(child):
158
+ child.rmdir()
159
+ elif child.name == _SESSION_FILE and child.is_file() and target.is_file():
160
+ _merge_session_files(child, target)
161
+ elif (
162
+ child.is_file() and target.is_file()
163
+ and child.stat().st_mtime > target.stat().st_mtime
164
+ ):
165
+ # The incoming file is newer: the older one at the destination is kept aside.
166
+ shutil.move(str(target), str(_aside_path(aside, here, kept)))
167
+ shutil.move(str(child), str(target))
168
+ else:
169
+ shutil.move(str(child), str(_aside_path(aside, here, kept)))
170
+
171
+
172
+ def _aside_path(aside: Path, rel: str, kept: List[str]) -> Path:
173
+ path = aside / rel
174
+ path.parent.mkdir(parents=True, exist_ok=True)
175
+ kept.append(rel)
176
+ return path
177
+
178
+
179
+ def merge_dir_into(src: Path, dst: Path, *, aside_name: str) -> Dict[str, str]:
180
+ """Merge folder ``src`` into ``dst``, then remove the (now empty) ``src``."""
181
+ aside = src.with_name(aside_name)
182
+ aside.mkdir(parents=True, exist_ok=True)
183
+ kept: List[str] = []
184
+ _merge_tree(src, dst, aside, kept)
185
+ if _is_empty_dir(src):
186
+ src.rmdir()
187
+ out = {"merged_existing_contents": "true"}
188
+ if kept:
189
+ out["older_copies_kept_at"] = str(aside)
190
+ else:
191
+ try:
192
+ aside.rmdir()
193
+ except OSError:
194
+ pass
195
+ return out
196
+
197
+
78
198
  def _move_children(src: Path, dst: Path) -> None:
79
199
  dst.mkdir(parents=True, exist_ok=True)
80
200
  for child in src.iterdir():
@@ -97,12 +217,14 @@ def link_one(runtime: Path, persistent: Path, *, private: bool) -> Dict[str, str
97
217
  # Linked into another root (a different conversation chose a different folder).
98
218
  # Bring that root's saved state along when this one has none; otherwise leave it alone.
99
219
  previous = runtime.resolve()
100
- if previous.is_dir() and not _is_empty_dir(previous):
220
+ if previous.is_dir() and not _is_empty_dir(previous) and previous != persistent.resolve():
101
221
  if _is_empty_dir(persistent):
102
222
  _move_children(previous, persistent)
103
223
  out["moved_existing_contents"] = "true"
104
224
  else:
105
- out["previous_root_left_at"] = str(previous)
225
+ out.update(merge_dir_into(
226
+ previous, persistent, aside_name=f"{previous.name}.before-prepare-{int(time.time())}",
227
+ ))
106
228
  runtime.unlink()
107
229
  relinked = True
108
230
  elif runtime.exists():
@@ -116,10 +238,11 @@ def link_one(runtime: Path, persistent: Path, *, private: bool) -> Dict[str, str
116
238
  runtime.rmdir()
117
239
  out["moved_existing_contents"] = "true"
118
240
  else:
119
- # Both hold data: the persistent copy wins, the runtime copy is kept aside.
120
- aside = runtime.with_name(f"{runtime.name}.before-prepare-{int(time.time())}")
121
- os.replace(runtime, aside)
122
- out["kept_runtime_copy_at"] = str(aside)
241
+ # Both hold data: merge them. Booking files merge by session id; for any other
242
+ # file the newer copy stays and the older one is kept aside, never deleted.
243
+ out.update(merge_dir_into(
244
+ runtime, persistent, aside_name=f"{runtime.name}.before-prepare-{int(time.time())}",
245
+ ))
123
246
 
124
247
  runtime.parent.mkdir(parents=True, exist_ok=True)
125
248
  os.symlink(persistent, runtime, target_is_directory=True)
@@ -142,25 +265,28 @@ def normalized_root(persistent_root: str) -> Path:
142
265
 
143
266
 
144
267
  def _lift_nested_layout(root: Path) -> Dict[str, str]:
145
- """Undo the old nested layout (``<root>/booking-agent/{booking-agent,link-cli}``)."""
268
+ """Undo the old nested layout (``<root>/booking-agent/{booking-agent,link-cli}``).
269
+
270
+ Both folders are merged into their proper place, so a stale top-level booking file never
271
+ hides the real one from the nested folder.
272
+ """
146
273
  state = root / STATE_SUBDIR
147
274
  lifted: Dict[str, str] = {}
275
+ stamp = int(time.time())
148
276
  nested_state = state / STATE_SUBDIR
149
277
  if nested_state.is_dir():
150
- for child in list(nested_state.iterdir()):
151
- if not (state / child.name).exists():
152
- os.replace(child, state / child.name)
153
- lifted["lifted_from_nested_layout"] = "true"
154
- if _is_empty_dir(nested_state):
155
- nested_state.rmdir()
278
+ merge_dir_into(nested_state, state, aside_name=f"{STATE_SUBDIR}.before-prepare-{stamp}")
279
+ lifted["lifted_from_nested_layout"] = "true"
156
280
  nested_link = state / LINK_SUBDIR
157
281
  if nested_link.is_dir():
158
282
  target = root / LINK_SUBDIR
159
- if not target.exists() or _is_empty_dir(target):
283
+ if target.exists() and not _is_empty_dir(target):
284
+ merge_dir_into(nested_link, target, aside_name=f"{LINK_SUBDIR}.before-prepare-{stamp}")
285
+ else:
160
286
  if target.exists():
161
287
  target.rmdir()
162
288
  os.replace(nested_link, target)
163
- lifted["lifted_from_nested_layout"] = "true"
289
+ lifted["lifted_from_nested_layout"] = "true"
164
290
  return lifted
165
291
 
166
292
 
@@ -223,7 +349,24 @@ def diagnose() -> Dict[str, object]:
223
349
  for aside in sorted(runtime.parent.glob(f"{runtime.name}.before-prepare-*")) if runtime.parent.is_dir() else []:
224
350
  findings.append({"kind": "runtime_copy_kept_aside", "path": str(aside), "severity": "info"})
225
351
 
352
+ configured = configured_root()
353
+ if configured is not None:
354
+ try:
355
+ expected = normalized_root(configured) / STATE_SUBDIR
356
+ except PrepareError:
357
+ expected = None
358
+ applied = bool(
359
+ expected is not None and state_info["linked_to"]
360
+ and Path(str(state_info["linked_to"])) == expected.resolve()
361
+ )
362
+ if not applied:
363
+ problems = True
364
+ findings.append({
365
+ "kind": "configured_root_not_applied", "path": str(state), "severity": "problem",
366
+ })
367
+ root_hint = root_hint or configured
226
368
  return {
369
+ "configured_root": configured,
227
370
  "state": state_info,
228
371
  "link_sign_in": link_info,
229
372
  "findings": findings,
@@ -232,3 +375,24 @@ def diagnose() -> Dict[str, object]:
232
375
  f"booking-agent runtime prepare --persistent-root {root_hint}" if problems and root_hint else None
233
376
  ),
234
377
  }
378
+
379
+
380
+ def configured_root() -> Optional[str]:
381
+ """The persistent folder a host names with ``BOOKING_AGENT_PERSISTENT_ROOT``, if any."""
382
+ value = (settings.get(ROOT_ENV) or "").strip()
383
+ return value or None
384
+
385
+
386
+ def auto_prepare() -> Optional[List[Dict[str, str]]]:
387
+ """Link state into the host-named persistent folder, quietly and idempotently.
388
+
389
+ Nothing happens unless the host set ``BOOKING_AGENT_PERSISTENT_ROOT``. A failure never
390
+ blocks a booking; ``runtime doctor`` reports it.
391
+ """
392
+ root = configured_root()
393
+ if root is None:
394
+ return None
395
+ try:
396
+ return prepare(root, [])
397
+ except (PrepareError, OSError):
398
+ return None
@@ -14,10 +14,17 @@ REGRESSIONS = {
14
14
  "state split across two persistent roots": [
15
15
  ("test_runtime_prepare.py", "test_a_root_already_named_booking_agent_is_not_nested_again"),
16
16
  ("test_runtime_prepare.py", "test_moving_to_a_different_root_brings_the_saved_state_along"),
17
- ("test_runtime_prepare.py", "test_moving_to_a_root_that_has_its_own_state_leaves_the_old_one_untouched"),
17
+ ("test_runtime_prepare.py", "test_moving_to_a_root_that_has_its_own_state_merges_both"),
18
18
  ("test_runtime_prepare.py", "test_a_state_folder_nested_by_the_old_layout_is_lifted_back_up"),
19
19
  ("test_runtime_prepare.py", "test_doctor_reports_where_state_lives_and_what_is_orphaned"),
20
20
  ],
21
+ "restored booking files hidden behind a stale top-level copy (hosted layout)": [
22
+ ("test_runtime_prepare.py", "test_the_recorded_hosted_layout_is_repaired_without_losing_the_real_booking"),
23
+ ("test_runtime_prepare.py", "test_the_further_along_copy_of_a_session_wins_and_no_booking_is_lost"),
24
+ ],
25
+ "assistant had to remember to set up persistence": [
26
+ ("test_runtime_prepare.py", "test_the_named_persistent_folder_is_prepared_before_any_command"),
27
+ ],
21
28
  "sign-in approved but the poll never reported, so the user is asked again": [
22
29
  ("test_link_auth.py", "test_buyer_resolve_trusts_link_when_the_sign_in_poll_never_reported"),
23
30
  ],
@@ -66,19 +66,23 @@ def test_moves_existing_contents_and_keeps_their_modes(runner, dirs):
66
66
  assert (link_dir / "config.json").read_text() == "{}"
67
67
 
68
68
 
69
- def test_keeps_the_runtime_copy_aside_when_both_hold_data(runner, dirs):
69
+ def test_merges_when_both_hold_data_and_keeps_the_older_file_aside(runner, dirs):
70
70
  _home, root, link_dir = dirs
71
71
  (root / "link-cli").mkdir(parents=True)
72
- (root / "link-cli" / "config.json").write_text("persistent")
72
+ (root / "link-cli" / "config.json").write_text("older")
73
73
  link_dir.mkdir(parents=True)
74
- (link_dir / "config.json").write_text("runtime")
74
+ (link_dir / "config.json").write_text("newer")
75
+ (link_dir / "extra.json").write_text("only-here")
76
+ os.utime(root / "link-cli" / "config.json", (1_000, 1_000))
77
+ os.utime(link_dir / "config.json", (2_000, 2_000))
75
78
 
76
79
  result, body = _run(runner, "--persistent-root", str(root))
77
80
 
78
81
  assert result.exit_code == 0, result.stdout
79
- assert (link_dir / "config.json").read_text() == "persistent"
80
- aside = body["data"]["links"][1]["kept_runtime_copy_at"]
81
- assert open(os.path.join(aside, "config.json")).read() == "runtime"
82
+ assert (link_dir / "config.json").read_text() == "newer"
83
+ assert (link_dir / "extra.json").read_text() == "only-here"
84
+ aside = body["data"]["links"][1]["older_copies_kept_at"]
85
+ assert open(os.path.join(aside, "config.json")).read() == "older"
82
86
 
83
87
 
84
88
  def test_extra_links_resolve_relative_paths_under_the_root(runner, dirs, tmp_path):
@@ -187,19 +191,23 @@ def test_moving_to_a_different_root_brings_the_saved_state_along(runner, dirs, t
187
191
  assert [link["result"] for link in body["data"]["links"]][:2] == ["relinked", "relinked"]
188
192
 
189
193
 
190
- def test_moving_to_a_root_that_has_its_own_state_leaves_the_old_one_untouched(runner, dirs, tmp_path):
194
+ def test_moving_to_a_root_that_has_its_own_state_merges_both(runner, dirs, tmp_path):
191
195
  home, root, link_dir = dirs
192
196
  _run(runner, "--persistent-root", str(root))
193
197
  (root / "link-cli" / "config.json").write_text("old-sign-in")
198
+ (root / "link-cli" / "only-old.json").write_text("kept")
199
+ os.utime(root / "link-cli" / "config.json", (1_000, 1_000))
194
200
  other = tmp_path / "other-keep"
195
201
  _seed(other / "booking-agent", other / "link-cli", session="newer", link="new-sign-in")
202
+ os.utime(other / "link-cli" / "config.json", (2_000, 2_000))
196
203
 
197
204
  result, body = _run(runner, "--persistent-root", str(other))
198
205
 
199
206
  assert result.exit_code == 0, result.stdout
200
207
  assert (link_dir / "config.json").read_text() == "new-sign-in"
201
- assert (root / "link-cli" / "config.json").read_text() == "old-sign-in"
202
- assert body["data"]["links"][1]["previous_root_left_at"] == str((root / "link-cli").resolve())
208
+ assert (link_dir / "only-old.json").read_text() == "kept"
209
+ assert body["data"]["links"][1]["merged_existing_contents"] == "true"
210
+ assert not (root / "link-cli").exists()
203
211
 
204
212
 
205
213
  def test_doctor_reports_where_state_lives_and_what_is_orphaned(runner, dirs, tmp_path):
@@ -228,3 +236,89 @@ def test_doctor_is_quiet_when_everything_is_in_one_place(runner, dirs):
228
236
  assert result.exit_code == 0, result.stdout
229
237
  assert body["data"]["healthy"] is True
230
238
  assert body["data"]["findings"] == []
239
+
240
+
241
+ # --- Two booking files from different times are merged, never one hiding the other ---------------
242
+
243
+ def _store(*sessions, active=None, created=None):
244
+ return {
245
+ "schema_version": 2, "sessions_created": created or len(sessions),
246
+ "active_session_id": active, "sessions": list(sessions),
247
+ }
248
+
249
+
250
+ def test_the_further_along_copy_of_a_session_wins_and_no_booking_is_lost():
251
+ stale = {"id": "s1", "flow_phase": "INIT"}
252
+ real = {"id": "s1", "flow_phase": "CONFIRMED", "booking": {"id": "b1"}}
253
+ other = {"id": "s2", "flow_phase": "PREREQ_OK"}
254
+ merged = runtime_prepare.merge_session_stores(
255
+ _store(stale, active="s1"), _store(real, other, active="s2"),
256
+ )
257
+ by_id = {s["id"]: s for s in merged["sessions"]}
258
+ assert by_id["s1"]["flow_phase"] == "CONFIRMED" and by_id["s1"]["booking"] == {"id": "b1"}
259
+ assert set(by_id) == {"s1", "s2"}
260
+ assert merged["active_session_id"] == "s1"
261
+
262
+
263
+ def test_the_recorded_hosted_layout_is_repaired_without_losing_the_real_booking(runner, dirs):
264
+ """A stale INIT store at the top level, the real one nested, a fresh store from today in /root."""
265
+ home, root, link_dir = dirs
266
+ state = root / "booking-agent"
267
+ state.mkdir(parents=True)
268
+ (state / ".usp-session.json").write_text(json.dumps(_store({"id": "run1", "flow_phase": "INIT"})))
269
+ nested = state / "booking-agent"
270
+ nested.mkdir()
271
+ (nested / ".usp-session.json").write_text(json.dumps(_store(
272
+ {"id": "run1", "flow_phase": "PAYMENT_PENDING"}, {"id": "run1b", "flow_phase": "CONFIRMED"},
273
+ )))
274
+ (state / "link-cli").mkdir()
275
+ (state / "link-cli" / "config.json").write_text("nested-link")
276
+ live = home / ".booking-agent"
277
+ live.mkdir()
278
+ (live / ".usp-session.json").write_text(json.dumps(_store({"id": "today", "flow_phase": "CONFIRMED"})))
279
+
280
+ result, _body = _run(runner, "--persistent-root", str(root))
281
+
282
+ assert result.exit_code == 0, result.stdout
283
+ merged = json.loads((home / ".booking-agent" / ".usp-session.json").read_text())
284
+ phases = {s["id"]: s["flow_phase"] for s in merged["sessions"]}
285
+ assert phases == {"run1": "PAYMENT_PENDING", "run1b": "CONFIRMED", "today": "CONFIRMED"}
286
+ assert (link_dir / "config.json").read_text() == "nested-link"
287
+ assert not (state / "booking-agent").exists() and not (state / "link-cli").exists()
288
+
289
+
290
+ # --- A host that names its persistent folder needs no instruction from the assistant ------------
291
+
292
+ def test_the_named_persistent_folder_is_prepared_before_any_command(runner, dirs, monkeypatch):
293
+ home, root, link_dir = dirs
294
+ monkeypatch.setenv("BOOKING_AGENT_PERSISTENT_ROOT", str(root))
295
+
296
+ result = runner.invoke(app, ["flow", "sessions", "list"])
297
+
298
+ assert result.exit_code == 0, result.stdout
299
+ assert (home / ".booking-agent").is_symlink()
300
+ assert (home / ".booking-agent").resolve() == (root / "booking-agent").resolve()
301
+ assert link_dir.resolve() == (root / "link-cli").resolve()
302
+
303
+
304
+ def test_without_the_variable_nothing_is_linked(runner, dirs):
305
+ home, _root, _link_dir = dirs
306
+ runner.invoke(app, ["flow", "sessions", "list"])
307
+ assert not (home / ".booking-agent").is_symlink()
308
+
309
+
310
+ def test_a_bad_persistent_folder_never_blocks_a_command(runner, dirs, monkeypatch):
311
+ monkeypatch.setenv("BOOKING_AGENT_PERSISTENT_ROOT", "relative/path")
312
+ result = runner.invoke(app, ["flow", "sessions", "list"])
313
+ assert result.exit_code == 0, result.stdout
314
+
315
+
316
+ def test_doctor_flags_a_named_folder_the_state_is_not_in(runner, dirs, monkeypatch):
317
+ _home, root, _link_dir = dirs
318
+ monkeypatch.setenv("BOOKING_AGENT_PERSISTENT_ROOT", str(root))
319
+
320
+ body = json.loads(runner.invoke(app, ["runtime", "doctor"]).stdout)
321
+
322
+ assert body["data"]["healthy"] is False
323
+ assert {f["kind"] for f in body["data"]["findings"]} >= {"configured_root_not_applied"}
324
+ assert body["next_command"] == f"booking-agent runtime prepare --persistent-root {root}"