pushframe 5.1.3__tar.gz → 5.1.5__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 (415) hide show
  1. pushframe-5.1.5/.planning/debug/status-crash-401-trip.md +61 -0
  2. {pushframe-5.1.3 → pushframe-5.1.5}/CHANGELOG.md +52 -1
  3. {pushframe-5.1.3 → pushframe-5.1.5}/PKG-INFO +1 -1
  4. {pushframe-5.1.3 → pushframe-5.1.5}/docs/CLI.md +9 -4
  5. {pushframe-5.1.3 → pushframe-5.1.5}/pushframe/__init__.py +1 -1
  6. {pushframe-5.1.3 → pushframe-5.1.5}/pushframe/cli.py +39 -3
  7. {pushframe-5.1.3 → pushframe-5.1.5}/pushframe/gsync.py +8 -3
  8. {pushframe-5.1.3 → pushframe-5.1.5}/pushframe/preflight.py +28 -4
  9. {pushframe-5.1.3 → pushframe-5.1.5}/pushframe/session.py +139 -0
  10. {pushframe-5.1.3 → pushframe-5.1.5}/pyproject.toml +1 -1
  11. {pushframe-5.1.3 → pushframe-5.1.5}/tests/test_cli_status.py +128 -0
  12. {pushframe-5.1.3 → pushframe-5.1.5}/tests/test_preflight_sweep.py +7 -3
  13. pushframe-5.1.5/tests/test_preflight_vault.py +78 -0
  14. pushframe-5.1.5/tests/test_token_refresh_extension.py +157 -0
  15. {pushframe-5.1.3 → pushframe-5.1.5}/tests/test_version.py +1 -1
  16. {pushframe-5.1.3 → pushframe-5.1.5}/uv.lock +1 -1
  17. {pushframe-5.1.3 → pushframe-5.1.5}/.env.sample +0 -0
  18. {pushframe-5.1.3 → pushframe-5.1.5}/.freebuff/project-id +0 -0
  19. {pushframe-5.1.3 → pushframe-5.1.5}/.github/workflows/release.yml +0 -0
  20. {pushframe-5.1.3 → pushframe-5.1.5}/.github/workflows/tests.yml +0 -0
  21. {pushframe-5.1.3 → pushframe-5.1.5}/.gitignore +0 -0
  22. {pushframe-5.1.3 → pushframe-5.1.5}/.gsd/dispatch-isolation-sentinel.json +0 -0
  23. {pushframe-5.1.3 → pushframe-5.1.5}/.planning/MILESTONES.md +0 -0
  24. {pushframe-5.1.3 → pushframe-5.1.5}/.planning/PROJECT.md +0 -0
  25. {pushframe-5.1.3 → pushframe-5.1.5}/.planning/REQUIREMENTS.md +0 -0
  26. {pushframe-5.1.3 → pushframe-5.1.5}/.planning/RETROSPECTIVE.md +0 -0
  27. {pushframe-5.1.3 → pushframe-5.1.5}/.planning/ROADMAP.md +0 -0
  28. {pushframe-5.1.3 → pushframe-5.1.5}/.planning/STATE.md +0 -0
  29. {pushframe-5.1.3 → pushframe-5.1.5}/.planning/WINDOWS.md +0 -0
  30. {pushframe-5.1.3 → pushframe-5.1.5}/.planning/codebase/ARCHITECTURE.md +0 -0
  31. {pushframe-5.1.3 → pushframe-5.1.5}/.planning/codebase/CONCERNS.md +0 -0
  32. {pushframe-5.1.3 → pushframe-5.1.5}/.planning/codebase/CONVENTIONS.md +0 -0
  33. {pushframe-5.1.3 → pushframe-5.1.5}/.planning/codebase/INTEGRATIONS.md +0 -0
  34. {pushframe-5.1.3 → pushframe-5.1.5}/.planning/codebase/STACK.md +0 -0
  35. {pushframe-5.1.3 → pushframe-5.1.5}/.planning/codebase/STRUCTURE.md +0 -0
  36. {pushframe-5.1.3 → pushframe-5.1.5}/.planning/codebase/TESTING.md +0 -0
  37. {pushframe-5.1.3 → pushframe-5.1.5}/.planning/config.json +0 -0
  38. {pushframe-5.1.3 → pushframe-5.1.5}/.planning/debug/knowledge-base.md +0 -0
  39. {pushframe-5.1.3 → pushframe-5.1.5}/.planning/debug/login-475-null-creds.md +0 -0
  40. {pushframe-5.1.3 → pushframe-5.1.5}/.planning/debug/resolved/cli-status-verbose-logging.md +0 -0
  41. {pushframe-5.1.3 → pushframe-5.1.5}/.planning/debug/resolved/inspect-asset-null-fields.md +0 -0
  42. {pushframe-5.1.3 → pushframe-5.1.5}/.planning/debug/resolved/select-asset-401-unauthorized.md +0 -0
  43. {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v1.0-REQUIREMENTS.md +0 -0
  44. {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v1.0-ROADMAP.md +0 -0
  45. {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v1.0-phases/01-toolchain-revival/01-01-PLAN.md +0 -0
  46. {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v1.0-phases/01-toolchain-revival/01-01-SUMMARY.md +0 -0
  47. {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v1.0-phases/01-toolchain-revival/01-02-PLAN.md +0 -0
  48. {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v1.0-phases/01-toolchain-revival/01-02-SUMMARY.md +0 -0
  49. {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v1.0-phases/01-toolchain-revival/01-CONTEXT.md +0 -0
  50. {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v1.0-phases/01-toolchain-revival/01-DISCUSSION-LOG.md +0 -0
  51. {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v1.0-phases/01-toolchain-revival/01-PATTERNS.md +0 -0
  52. {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v1.0-phases/01-toolchain-revival/01-RESEARCH.md +0 -0
  53. {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v1.0-phases/01-toolchain-revival/01-REVIEW.md +0 -0
  54. {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v1.0-phases/01-toolchain-revival/01-VERIFICATION.md +0 -0
  55. {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v1.0-phases/02-live-read-path-verification/02-01-PLAN.md +0 -0
  56. {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v1.0-phases/02-live-read-path-verification/02-01-SUMMARY.md +0 -0
  57. {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v1.0-phases/02-live-read-path-verification/02-02-PLAN.md +0 -0
  58. {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v1.0-phases/02-live-read-path-verification/02-02-SUMMARY.md +0 -0
  59. {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v1.0-phases/02-live-read-path-verification/02-CONTEXT.md +0 -0
  60. {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v1.0-phases/02-live-read-path-verification/02-DISCUSSION-LOG.md +0 -0
  61. {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v1.0-phases/02-live-read-path-verification/02-LIVE-EVIDENCE.md +0 -0
  62. {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v1.0-phases/02-live-read-path-verification/02-PATTERNS.md +0 -0
  63. {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v1.0-phases/02-live-read-path-verification/02-RESEARCH.md +0 -0
  64. {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v1.0-phases/02-live-read-path-verification/02-REVIEW.md +0 -0
  65. {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v1.0-phases/02-live-read-path-verification/02-VERIFICATION.md +0 -0
  66. {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v1.0-phases/03-run-docs-verification-report/03-01-PLAN.md +0 -0
  67. {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v1.0-phases/03-run-docs-verification-report/03-01-SUMMARY.md +0 -0
  68. {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v1.0-phases/03-run-docs-verification-report/03-CONTEXT.md +0 -0
  69. {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v1.0-phases/03-run-docs-verification-report/03-DISCUSSION-LOG.md +0 -0
  70. {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v1.0-phases/03-run-docs-verification-report/03-PATTERNS.md +0 -0
  71. {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v1.0-phases/03-run-docs-verification-report/03-RESEARCH.md +0 -0
  72. {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v1.0-phases/03-run-docs-verification-report/03-REVIEW.md +0 -0
  73. {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v1.0-phases/03-run-docs-verification-report/03-VERIFICATION.md +0 -0
  74. {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v1.1-ROADMAP.md +0 -0
  75. {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v1.1-phases/04-client-transport-seam-for-offline-testability/.gitkeep +0 -0
  76. {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v1.1-phases/04-client-transport-seam-for-offline-testability/04-01-PLAN.md +0 -0
  77. {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v1.1-phases/04-client-transport-seam-for-offline-testability/04-01-SUMMARY.md +0 -0
  78. {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v1.1-phases/04-client-transport-seam-for-offline-testability/04-02-PLAN.md +0 -0
  79. {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v1.1-phases/04-client-transport-seam-for-offline-testability/04-02-SUMMARY.md +0 -0
  80. {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v1.1-phases/04-client-transport-seam-for-offline-testability/04-03-PLAN.md +0 -0
  81. {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v1.1-phases/04-client-transport-seam-for-offline-testability/04-03-SUMMARY.md +0 -0
  82. {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v1.1-phases/04-client-transport-seam-for-offline-testability/04-PATTERNS.md +0 -0
  83. {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v1.1-phases/04-client-transport-seam-for-offline-testability/04-RESEARCH.md +0 -0
  84. {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v1.1-phases/04-client-transport-seam-for-offline-testability/04-REVIEW.md +0 -0
  85. {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v1.1-phases/04-client-transport-seam-for-offline-testability/04-SECURITY.md +0 -0
  86. {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v1.1-phases/04-client-transport-seam-for-offline-testability/04-UAT.md +0 -0
  87. {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v1.1-phases/04-client-transport-seam-for-offline-testability/04-VERIFICATION.md +0 -0
  88. {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v2.0-REQUIREMENTS.md +0 -0
  89. {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v2.0-ROADMAP.md +0 -0
  90. {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v2.0-phases/05-cli-skeleton-status/05-01-PLAN.md +0 -0
  91. {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v2.0-phases/05-cli-skeleton-status/05-01-SUMMARY.md +0 -0
  92. {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v2.0-phases/05-cli-skeleton-status/05-02-PLAN.md +0 -0
  93. {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v2.0-phases/05-cli-skeleton-status/05-02-SUMMARY.md +0 -0
  94. {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v2.0-phases/05-cli-skeleton-status/05-CONTEXT.md +0 -0
  95. {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v2.0-phases/05-cli-skeleton-status/05-DISCUSSION-LOG.md +0 -0
  96. {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v2.0-phases/05-cli-skeleton-status/05-PATTERNS.md +0 -0
  97. {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v2.0-phases/05-cli-skeleton-status/05-REVIEW.md +0 -0
  98. {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v2.0-phases/05-cli-skeleton-status/05-SECURITY.md +0 -0
  99. {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v2.0-phases/05-cli-skeleton-status/05-UAT.md +0 -0
  100. {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v2.0-phases/05-cli-skeleton-status/05-VERIFICATION.md +0 -0
  101. {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v2.0-phases/06-inspect-frame-resolution/06-01-PLAN.md +0 -0
  102. {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v2.0-phases/06-inspect-frame-resolution/06-01-SUMMARY.md +0 -0
  103. {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v2.0-phases/06-inspect-frame-resolution/06-02-PLAN.md +0 -0
  104. {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v2.0-phases/06-inspect-frame-resolution/06-02-SUMMARY.md +0 -0
  105. {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v2.0-phases/06-inspect-frame-resolution/06-CONTEXT.md +0 -0
  106. {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v2.0-phases/06-inspect-frame-resolution/06-DISCUSSION-LOG.md +0 -0
  107. {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v2.0-phases/06-inspect-frame-resolution/06-PATTERNS.md +0 -0
  108. {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v2.0-phases/06-inspect-frame-resolution/06-RESEARCH.md +0 -0
  109. {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v2.0-phases/06-inspect-frame-resolution/06-REVIEW.md +0 -0
  110. {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v2.0-phases/06-inspect-frame-resolution/06-SECURITY.md +0 -0
  111. {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v2.0-phases/06-inspect-frame-resolution/06-VERIFICATION.md +0 -0
  112. {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v2.0-phases/07-sync-diffing-engine-dry-run-only/07-01-PLAN.md +0 -0
  113. {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v2.0-phases/07-sync-diffing-engine-dry-run-only/07-01-SUMMARY.md +0 -0
  114. {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v2.0-phases/07-sync-diffing-engine-dry-run-only/07-02-PLAN.md +0 -0
  115. {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v2.0-phases/07-sync-diffing-engine-dry-run-only/07-02-SUMMARY.md +0 -0
  116. {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v2.0-phases/07-sync-diffing-engine-dry-run-only/07-03-PLAN.md +0 -0
  117. {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v2.0-phases/07-sync-diffing-engine-dry-run-only/07-03-SUMMARY.md +0 -0
  118. {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v2.0-phases/07-sync-diffing-engine-dry-run-only/07-CONTEXT.md +0 -0
  119. {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v2.0-phases/07-sync-diffing-engine-dry-run-only/07-DISCUSSION-LOG.md +0 -0
  120. {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v2.0-phases/07-sync-diffing-engine-dry-run-only/07-PATTERNS.md +0 -0
  121. {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v2.0-phases/07-sync-diffing-engine-dry-run-only/07-REVIEW-FIX.md +0 -0
  122. {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v2.0-phases/07-sync-diffing-engine-dry-run-only/07-REVIEW.md +0 -0
  123. {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v2.0-phases/07-sync-diffing-engine-dry-run-only/07-SECURITY.md +0 -0
  124. {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v2.0-phases/07-sync-diffing-engine-dry-run-only/07-VERIFICATION.md +0 -0
  125. {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v2.0-phases/08-destructive-execution-upload-delete-verification/08-01-PLAN.md +0 -0
  126. {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v2.0-phases/08-destructive-execution-upload-delete-verification/08-01-SUMMARY.md +0 -0
  127. {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v2.0-phases/08-destructive-execution-upload-delete-verification/08-02-PLAN.md +0 -0
  128. {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v2.0-phases/08-destructive-execution-upload-delete-verification/08-02-SUMMARY.md +0 -0
  129. {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v2.0-phases/08-destructive-execution-upload-delete-verification/08-03-PLAN.md +0 -0
  130. {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v2.0-phases/08-destructive-execution-upload-delete-verification/08-03-SUMMARY.md +0 -0
  131. {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v2.0-phases/08-destructive-execution-upload-delete-verification/08-04-PLAN.md +0 -0
  132. {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v2.0-phases/08-destructive-execution-upload-delete-verification/08-04-SUMMARY.md +0 -0
  133. {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v2.0-phases/08-destructive-execution-upload-delete-verification/08-CONTEXT.md +0 -0
  134. {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v2.0-phases/08-destructive-execution-upload-delete-verification/08-DISCUSSION-LOG.md +0 -0
  135. {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v2.0-phases/08-destructive-execution-upload-delete-verification/08-LIVE-FINDINGS.md +0 -0
  136. {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v2.0-phases/08-destructive-execution-upload-delete-verification/08-PATTERNS.md +0 -0
  137. {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v2.0-phases/08-destructive-execution-upload-delete-verification/08-RESEARCH.md +0 -0
  138. {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v2.0-phases/08-destructive-execution-upload-delete-verification/08-REVIEW-FIX.md +0 -0
  139. {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v2.0-phases/08-destructive-execution-upload-delete-verification/08-REVIEW.md +0 -0
  140. {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v2.0-phases/08-destructive-execution-upload-delete-verification/08-VERIFICATION.md +0 -0
  141. {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v2.0-phases/09-proactive-write-rate-limiter-geo-guard/.gitkeep +0 -0
  142. {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v2.0-phases/09-proactive-write-rate-limiter-geo-guard/09-01-PLAN.md +0 -0
  143. {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v2.0-phases/09-proactive-write-rate-limiter-geo-guard/09-01-SUMMARY.md +0 -0
  144. {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v2.0-phases/09-proactive-write-rate-limiter-geo-guard/09-02-PLAN.md +0 -0
  145. {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v2.0-phases/09-proactive-write-rate-limiter-geo-guard/09-02-SUMMARY.md +0 -0
  146. {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v2.0-phases/09-proactive-write-rate-limiter-geo-guard/09-CONTEXT.md +0 -0
  147. {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v2.0-phases/09-proactive-write-rate-limiter-geo-guard/09-RESEARCH.md +0 -0
  148. {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v2.0-phases/09-proactive-write-rate-limiter-geo-guard/09-REVIEW-FIX.md +0 -0
  149. {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v2.0-phases/09-proactive-write-rate-limiter-geo-guard/09-REVIEW.md +0 -0
  150. {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v2.0-phases/09-proactive-write-rate-limiter-geo-guard/09-VERIFICATION.md +0 -0
  151. {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v2.0-phases/10-hide-instead-of-delete-sync-mode/.gitkeep +0 -0
  152. {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v2.0-phases/10-hide-instead-of-delete-sync-mode/10-01-PLAN.md +0 -0
  153. {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v2.0-phases/10-hide-instead-of-delete-sync-mode/10-01-SUMMARY.md +0 -0
  154. {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v2.0-phases/10-hide-instead-of-delete-sync-mode/10-02-PLAN.md +0 -0
  155. {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v2.0-phases/10-hide-instead-of-delete-sync-mode/10-02-SUMMARY.md +0 -0
  156. {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v2.0-phases/10-hide-instead-of-delete-sync-mode/10-03-PLAN.md +0 -0
  157. {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v2.0-phases/10-hide-instead-of-delete-sync-mode/10-03-SUMMARY.md +0 -0
  158. {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v2.0-phases/10-hide-instead-of-delete-sync-mode/10-04-PLAN.md +0 -0
  159. {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v2.0-phases/10-hide-instead-of-delete-sync-mode/10-04-SUMMARY.md +0 -0
  160. {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v2.0-phases/10-hide-instead-of-delete-sync-mode/10-CONTEXT.md +0 -0
  161. {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v2.0-phases/10-hide-instead-of-delete-sync-mode/10-DISCUSSION-LOG.md +0 -0
  162. {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v2.0-phases/10-hide-instead-of-delete-sync-mode/10-LIVE-FINDINGS.md +0 -0
  163. {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v2.0-phases/10-hide-instead-of-delete-sync-mode/10-PATTERNS.md +0 -0
  164. {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v2.0-phases/10-hide-instead-of-delete-sync-mode/10-RESEARCH.md +0 -0
  165. {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v2.0-phases/10-hide-instead-of-delete-sync-mode/10-SECURITY.md +0 -0
  166. {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v2.0-phases/10-hide-instead-of-delete-sync-mode/10-UAT.md +0 -0
  167. {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v2.0-phases/10-hide-instead-of-delete-sync-mode/10-VERIFICATION.md +0 -0
  168. {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v2.0-phases/10-hide-instead-of-delete-sync-mode/COVERAGE.md +0 -0
  169. {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v2.0-quick/260630-qs8-make-main-py-load-a-local-env-file-at-st/260630-qs8-PLAN.md +0 -0
  170. {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v2.0-quick/260630-qs8-make-main-py-load-a-local-env-file-at-st/260630-qs8-SUMMARY.md +0 -0
  171. {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v2.0-quick/260708-dt9-add-progress-feedback-to-aura-cli-sync-a/260708-dt9-PLAN.md +0 -0
  172. {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v2.0-quick/260708-dt9-add-progress-feedback-to-aura-cli-sync-a/260708-dt9-SUMMARY.md +0 -0
  173. {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v2.0-quick/260708-dt9-add-progress-feedback-to-aura-cli-sync-a/deferred-items.md +0 -0
  174. {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v2.0-quick/260708-fyr-batch-refactor-of-sync-apply-write-path-/260708-fyr-PLAN.md +0 -0
  175. {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v2.0-quick/260708-fyr-batch-refactor-of-sync-apply-write-path-/260708-fyr-SUMMARY.md +0 -0
  176. {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v2.0-quick/260708-fyr-batch-refactor-of-sync-apply-write-path-/260708-fyr-VERIFICATION.md +0 -0
  177. {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v2.0-quick/README.md +0 -0
  178. {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v3.0-REQUIREMENTS.md +0 -0
  179. {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v3.0-ROADMAP.md +0 -0
  180. {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v3.0-phases/11-write-path-reliability-format-support/11-01-PLAN.md +0 -0
  181. {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v3.0-phases/11-write-path-reliability-format-support/11-01-SUMMARY.md +0 -0
  182. {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v3.0-phases/11-write-path-reliability-format-support/11-02-PLAN.md +0 -0
  183. {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v3.0-phases/11-write-path-reliability-format-support/11-02-SUMMARY.md +0 -0
  184. {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v3.0-phases/11-write-path-reliability-format-support/11-03-PLAN.md +0 -0
  185. {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v3.0-phases/11-write-path-reliability-format-support/11-03-SUMMARY.md +0 -0
  186. {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v3.0-phases/11-write-path-reliability-format-support/11-04-PLAN.md +0 -0
  187. {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v3.0-phases/11-write-path-reliability-format-support/11-04-SUMMARY.md +0 -0
  188. {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v3.0-phases/11-write-path-reliability-format-support/11-05-PLAN.md +0 -0
  189. {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v3.0-phases/11-write-path-reliability-format-support/11-05-SUMMARY.md +0 -0
  190. {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v3.0-phases/11-write-path-reliability-format-support/11-06-PLAN.md +0 -0
  191. {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v3.0-phases/11-write-path-reliability-format-support/11-06-SUMMARY.md +0 -0
  192. {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v3.0-phases/11-write-path-reliability-format-support/11-CONTEXT.md +0 -0
  193. {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v3.0-phases/11-write-path-reliability-format-support/11-DISCUSSION-LOG.md +0 -0
  194. {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v3.0-phases/11-write-path-reliability-format-support/11-LIVE-FINDINGS.md +0 -0
  195. {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v3.0-phases/11-write-path-reliability-format-support/11-PATTERNS.md +0 -0
  196. {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v3.0-phases/11-write-path-reliability-format-support/11-REVIEW.md +0 -0
  197. {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v3.0-phases/11-write-path-reliability-format-support/11-VERIFICATION.md +0 -0
  198. {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v4.0-REQUIREMENTS.md +0 -0
  199. {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v4.0-ROADMAP.md +0 -0
  200. {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v4.0-phases/16-local-mechanism-spike-decision/16-01-PLAN.md +0 -0
  201. {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v4.0-phases/16-local-mechanism-spike-decision/16-01-SUMMARY.md +0 -0
  202. {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v4.0-phases/16-local-mechanism-spike-decision/16-02-PLAN.md +0 -0
  203. {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v4.0-phases/16-local-mechanism-spike-decision/16-02-SUMMARY.md +0 -0
  204. {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v4.0-phases/16-local-mechanism-spike-decision/16-03-PLAN.md +0 -0
  205. {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v4.0-phases/16-local-mechanism-spike-decision/16-03-SUMMARY.md +0 -0
  206. {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v4.0-phases/16-local-mechanism-spike-decision/16-CONTEXT.md +0 -0
  207. {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v4.0-phases/16-local-mechanism-spike-decision/16-DECISION-RECORD.md +0 -0
  208. {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v4.0-phases/16-local-mechanism-spike-decision/16-DISCUSSION-LOG.md +0 -0
  209. {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v4.0-phases/16-local-mechanism-spike-decision/16-LIVE-FINDINGS.md +0 -0
  210. {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v4.0-phases/16-local-mechanism-spike-decision/COVERAGE.md +0 -0
  211. {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v4.0-phases/17-google-link-album-selection/17-01-PLAN.md +0 -0
  212. {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v4.0-phases/17-google-link-album-selection/17-01-SUMMARY.md +0 -0
  213. {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v4.0-phases/17-google-link-album-selection/17-02-PLAN.md +0 -0
  214. {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v4.0-phases/17-google-link-album-selection/17-02-SUMMARY.md +0 -0
  215. {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v4.0-phases/17-google-link-album-selection/17-CONTEXT.md +0 -0
  216. {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v4.0-phases/17-google-link-album-selection/17-DISCUSS-CHECKPOINT.json +0 -0
  217. {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v4.0-phases/17-google-link-album-selection/17-VERIFICATION.md +0 -0
  218. {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v4.0-phases/17-google-link-album-selection/COVERAGE.md +0 -0
  219. {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v4.0-phases/18-album-frame-mirror-sync-single-pair/18-01-PLAN.md +0 -0
  220. {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v4.0-phases/18-album-frame-mirror-sync-single-pair/18-01-SUMMARY.md +0 -0
  221. {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v4.0-phases/18-album-frame-mirror-sync-single-pair/18-02-PLAN.md +0 -0
  222. {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v4.0-phases/18-album-frame-mirror-sync-single-pair/18-02-SUMMARY.md +0 -0
  223. {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v4.0-phases/18-album-frame-mirror-sync-single-pair/18-03-PLAN.md +0 -0
  224. {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v4.0-phases/18-album-frame-mirror-sync-single-pair/18-03-SUMMARY.md +0 -0
  225. {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v4.0-phases/18-album-frame-mirror-sync-single-pair/18-CONTEXT.md +0 -0
  226. {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v4.0-phases/18-album-frame-mirror-sync-single-pair/18-RESEARCH.md +0 -0
  227. {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v4.0-phases/18-album-frame-mirror-sync-single-pair/18-UAT.md +0 -0
  228. {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v4.0-phases/18-album-frame-mirror-sync-single-pair/18-VERIFICATION.md +0 -0
  229. {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v4.0-phases/19-debt-closeout/19-01-PLAN.md +0 -0
  230. {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v4.0-phases/19-debt-closeout/19-01-SUMMARY.md +0 -0
  231. {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v4.0-phases/19-debt-closeout/19-02-PLAN.md +0 -0
  232. {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v4.0-phases/19-debt-closeout/19-02-SUMMARY.md +0 -0
  233. {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v4.0-phases/19-debt-closeout/19-CONTEXT.md +0 -0
  234. {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v4.0-phases/19-debt-closeout/19-RESEARCH.md +0 -0
  235. {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v4.0-phases/19-debt-closeout/19-VERIFICATION.md +0 -0
  236. {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v5.0-REQUIREMENTS.md +0 -0
  237. {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v5.0-ROADMAP.md +0 -0
  238. {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v5.0-phases/20-pushframe-rename-migration-repo-switch/20-SUMMARY.md +0 -0
  239. {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v5.0-phases/21-debian-package-apt-repo/21-01-PLAN.md +0 -0
  240. {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v5.0-phases/21-debian-package-apt-repo/21-01-SUMMARY.md +0 -0
  241. {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v5.0-phases/21-debian-package-apt-repo/21-02-PLAN.md +0 -0
  242. {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v5.0-phases/21-debian-package-apt-repo/21-02-SUMMARY.md +0 -0
  243. {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v5.0-phases/21-debian-package-apt-repo/21-CONTEXT.md +0 -0
  244. {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v5.0-phases/21-debian-package-apt-repo/21-RESEARCH.md +0 -0
  245. {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v5.0-phases/21-debian-package-apt-repo/21-VERIFICATION.md +0 -0
  246. {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v5.0-phases/22-pypi-release-engineering/22-01-PLAN.md +0 -0
  247. {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v5.0-phases/22-pypi-release-engineering/22-01-SUMMARY.md +0 -0
  248. {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v5.0-phases/22-pypi-release-engineering/22-02-PLAN.md +0 -0
  249. {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v5.0-phases/22-pypi-release-engineering/22-02-SUMMARY.md +0 -0
  250. {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v5.0-phases/22-pypi-release-engineering/22-CONTEXT.md +0 -0
  251. {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v5.0-phases/22-pypi-release-engineering/22-RESEARCH.md +0 -0
  252. {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v5.0-phases/22-pypi-release-engineering/22-VERIFICATION.md +0 -0
  253. {pushframe-5.1.3 → pushframe-5.1.5}/.planning/milestones/v5.0-phases/22-pypi-release-engineering/OPERATOR-STEPS.md +0 -0
  254. {pushframe-5.1.3 → pushframe-5.1.5}/.planning/phases/23-config-wizard-precedence/23-01-PLAN.md +0 -0
  255. {pushframe-5.1.3 → pushframe-5.1.5}/.planning/phases/23-config-wizard-precedence/23-01-SUMMARY.md +0 -0
  256. {pushframe-5.1.3 → pushframe-5.1.5}/.planning/phases/23-config-wizard-precedence/23-CONTEXT.md +0 -0
  257. {pushframe-5.1.3 → pushframe-5.1.5}/.planning/phases/23-config-wizard-precedence/23-RESEARCH.md +0 -0
  258. {pushframe-5.1.3 → pushframe-5.1.5}/.planning/phases/23-config-wizard-precedence/23-VERIFICATION.md +0 -0
  259. {pushframe-5.1.3 → pushframe-5.1.5}/.planning/phases/24-token-first-sessions-preflight-sweep-sec-01-03-prf-01-02/24-01-PLAN.md +0 -0
  260. {pushframe-5.1.3 → pushframe-5.1.5}/.planning/phases/24-token-first-sessions-preflight-sweep-sec-01-03-prf-01-02/24-01-SUMMARY.md +0 -0
  261. {pushframe-5.1.3 → pushframe-5.1.5}/.planning/phases/24-token-first-sessions-preflight-sweep-sec-01-03-prf-01-02/24-CONTEXT.md +0 -0
  262. {pushframe-5.1.3 → pushframe-5.1.5}/.planning/phases/24-token-first-sessions-preflight-sweep-sec-01-03-prf-01-02/24-RESEARCH.md +0 -0
  263. {pushframe-5.1.3 → pushframe-5.1.5}/.planning/phases/24-token-first-sessions-preflight-sweep-sec-01-03-prf-01-02/24-VERIFICATION.md +0 -0
  264. {pushframe-5.1.3 → pushframe-5.1.5}/.planning/phases/25-multi-frame-mappings-systemd-user-scheduling-mtf-01-03-tmr-01-03/25-01-PLAN.md +0 -0
  265. {pushframe-5.1.3 → pushframe-5.1.5}/.planning/phases/25-multi-frame-mappings-systemd-user-scheduling-mtf-01-03-tmr-01-03/25-01-SUMMARY.md +0 -0
  266. {pushframe-5.1.3 → pushframe-5.1.5}/.planning/phases/25-multi-frame-mappings-systemd-user-scheduling-mtf-01-03-tmr-01-03/25-CONTEXT.md +0 -0
  267. {pushframe-5.1.3 → pushframe-5.1.5}/.planning/phases/25-multi-frame-mappings-systemd-user-scheduling-mtf-01-03-tmr-01-03/25-RESEARCH.md +0 -0
  268. {pushframe-5.1.3 → pushframe-5.1.5}/.planning/phases/25-multi-frame-mappings-systemd-user-scheduling-mtf-01-03-tmr-01-03/25-VERIFICATION.md +0 -0
  269. {pushframe-5.1.3 → pushframe-5.1.5}/.planning/research/ALBUM-ACCESS-V4-ADDENDUM.md +0 -0
  270. {pushframe-5.1.3 → pushframe-5.1.5}/.planning/research/ALBUM-ACCESS.md +0 -0
  271. {pushframe-5.1.3 → pushframe-5.1.5}/.planning/research/ARCHITECTURE.md +0 -0
  272. {pushframe-5.1.3 → pushframe-5.1.5}/.planning/research/BROWSER-AUTOMATION.md +0 -0
  273. {pushframe-5.1.3 → pushframe-5.1.5}/.planning/research/FEATURES.md +0 -0
  274. {pushframe-5.1.3 → pushframe-5.1.5}/.planning/research/PITFALLS.md +0 -0
  275. {pushframe-5.1.3 → pushframe-5.1.5}/.planning/research/STACK.md +0 -0
  276. {pushframe-5.1.3 → pushframe-5.1.5}/.planning/research/SUMMARY.md +0 -0
  277. {pushframe-5.1.3 → pushframe-5.1.5}/.planning/state.json +0 -0
  278. {pushframe-5.1.3 → pushframe-5.1.5}/.planning/todos/completed/2026-07-06-promote-debug-flag-to-a-global-cli-convention.md +0 -0
  279. {pushframe-5.1.3 → pushframe-5.1.5}/.python-version +0 -0
  280. {pushframe-5.1.3 → pushframe-5.1.5}/CLAUDE.md +0 -0
  281. {pushframe-5.1.3 → pushframe-5.1.5}/LICENSE +0 -0
  282. {pushframe-5.1.3 → pushframe-5.1.5}/README.md +0 -0
  283. {pushframe-5.1.3 → pushframe-5.1.5}/SECURITY.md +0 -0
  284. {pushframe-5.1.3 → pushframe-5.1.5}/VERIFICATION-REPORT.md +0 -0
  285. {pushframe-5.1.3 → pushframe-5.1.5}/docs/agents/domain.md +0 -0
  286. {pushframe-5.1.3 → pushframe-5.1.5}/docs/agents/issue-tracker.md +0 -0
  287. {pushframe-5.1.3 → pushframe-5.1.5}/docs/agents/triage-labels.md +0 -0
  288. {pushframe-5.1.3 → pushframe-5.1.5}/docs/superpowers/specs/2026-07-09-write-rate-limiter-design.md +0 -0
  289. {pushframe-5.1.3 → pushframe-5.1.5}/main.py +0 -0
  290. {pushframe-5.1.3 → pushframe-5.1.5}/packaging/control-template +0 -0
  291. {pushframe-5.1.3 → pushframe-5.1.5}/packaging/lintian-overrides +0 -0
  292. {pushframe-5.1.3 → pushframe-5.1.5}/packaging/postinst +0 -0
  293. {pushframe-5.1.3 → pushframe-5.1.5}/packaging/pushframe.copyright +0 -0
  294. {pushframe-5.1.3 → pushframe-5.1.5}/probes/.gitignore +0 -0
  295. {pushframe-5.1.3 → pushframe-5.1.5}/probes/README.md +0 -0
  296. {pushframe-5.1.3 → pushframe-5.1.5}/probes/browser_bootstrap.py +0 -0
  297. {pushframe-5.1.3 → pushframe-5.1.5}/probes/common.py +0 -0
  298. {pushframe-5.1.3 → pushframe-5.1.5}/probes/cookie_vault.py +0 -0
  299. {pushframe-5.1.3 → pushframe-5.1.5}/probes/fidelity_check.py +0 -0
  300. {pushframe-5.1.3 → pushframe-5.1.5}/probes/rpc_capture.py +0 -0
  301. {pushframe-5.1.3 → pushframe-5.1.5}/probes/shared_link_probe.py +0 -0
  302. {pushframe-5.1.3 → pushframe-5.1.5}/pushframe/api/__init__.py +0 -0
  303. {pushframe-5.1.3 → pushframe-5.1.5}/pushframe/api/accountApi.py +0 -0
  304. {pushframe-5.1.3 → pushframe-5.1.5}/pushframe/api/activityApi.py +0 -0
  305. {pushframe-5.1.3 → pushframe-5.1.5}/pushframe/api/assetApi.py +0 -0
  306. {pushframe-5.1.3 → pushframe-5.1.5}/pushframe/api/baseApi.py +0 -0
  307. {pushframe-5.1.3 → pushframe-5.1.5}/pushframe/api/frameApi.py +0 -0
  308. {pushframe-5.1.3 → pushframe-5.1.5}/pushframe/api/notificationApi.py +0 -0
  309. {pushframe-5.1.3 → pushframe-5.1.5}/pushframe/api/peopleApi.py +0 -0
  310. {pushframe-5.1.3 → pushframe-5.1.5}/pushframe/api/playlistApi.py +0 -0
  311. {pushframe-5.1.3 → pushframe-5.1.5}/pushframe/aura.py +0 -0
  312. {pushframe-5.1.3 → pushframe-5.1.5}/pushframe/aws/__init__.py +0 -0
  313. {pushframe-5.1.3 → pushframe-5.1.5}/pushframe/aws/awsclient.py +0 -0
  314. {pushframe-5.1.3 → pushframe-5.1.5}/pushframe/aws/s3client.py +0 -0
  315. {pushframe-5.1.3 → pushframe-5.1.5}/pushframe/aws/sqsclient.py +0 -0
  316. {pushframe-5.1.3 → pushframe-5.1.5}/pushframe/cache.py +0 -0
  317. {pushframe-5.1.3 → pushframe-5.1.5}/pushframe/client.py +0 -0
  318. {pushframe-5.1.3 → pushframe-5.1.5}/pushframe/config_store.py +0 -0
  319. {pushframe-5.1.3 → pushframe-5.1.5}/pushframe/doctor.py +0 -0
  320. {pushframe-5.1.3 → pushframe-5.1.5}/pushframe/exif.py +0 -0
  321. {pushframe-5.1.3 → pushframe-5.1.5}/pushframe/export.py +0 -0
  322. {pushframe-5.1.3 → pushframe-5.1.5}/pushframe/google/__init__.py +0 -0
  323. {pushframe-5.1.3 → pushframe-5.1.5}/pushframe/google/bootstrap.py +0 -0
  324. {pushframe-5.1.3 → pushframe-5.1.5}/pushframe/google/cache.py +0 -0
  325. {pushframe-5.1.3 → pushframe-5.1.5}/pushframe/google/client.py +0 -0
  326. {pushframe-5.1.3 → pushframe-5.1.5}/pushframe/google/enumerate.py +0 -0
  327. {pushframe-5.1.3 → pushframe-5.1.5}/pushframe/google/manifest.py +0 -0
  328. {pushframe-5.1.3 → pushframe-5.1.5}/pushframe/google/parsers.py +0 -0
  329. {pushframe-5.1.3 → pushframe-5.1.5}/pushframe/google/redaction.py +0 -0
  330. {pushframe-5.1.3 → pushframe-5.1.5}/pushframe/google/vault.py +0 -0
  331. {pushframe-5.1.3 → pushframe-5.1.5}/pushframe/migration.py +0 -0
  332. {pushframe-5.1.3 → pushframe-5.1.5}/pushframe/models/__init__.py +0 -0
  333. {pushframe-5.1.3 → pushframe-5.1.5}/pushframe/models/activity.py +0 -0
  334. {pushframe-5.1.3 → pushframe-5.1.5}/pushframe/models/asset.py +0 -0
  335. {pushframe-5.1.3 → pushframe-5.1.5}/pushframe/models/frame.py +0 -0
  336. {pushframe-5.1.3 → pushframe-5.1.5}/pushframe/models/meta.py +0 -0
  337. {pushframe-5.1.3 → pushframe-5.1.5}/pushframe/models/person.py +0 -0
  338. {pushframe-5.1.3 → pushframe-5.1.5}/pushframe/models/user.py +0 -0
  339. {pushframe-5.1.3 → pushframe-5.1.5}/pushframe/pairs.py +0 -0
  340. {pushframe-5.1.3 → pushframe-5.1.5}/pushframe/ratelimit.py +0 -0
  341. {pushframe-5.1.3 → pushframe-5.1.5}/pushframe/reconcile.py +0 -0
  342. {pushframe-5.1.3 → pushframe-5.1.5}/pushframe/schedule.py +0 -0
  343. {pushframe-5.1.3 → pushframe-5.1.5}/pushframe/sync.py +0 -0
  344. {pushframe-5.1.3 → pushframe-5.1.5}/pushframe/utils/dt.py +0 -0
  345. {pushframe-5.1.3 → pushframe-5.1.5}/pushframe/utils/io.py +0 -0
  346. {pushframe-5.1.3 → pushframe-5.1.5}/pushframe/utils/settings.py +0 -0
  347. {pushframe-5.1.3 → pushframe-5.1.5}/scripts/build-deb.sh +0 -0
  348. {pushframe-5.1.3 → pushframe-5.1.5}/scripts/make-apt-key.sh +0 -0
  349. {pushframe-5.1.3 → pushframe-5.1.5}/scripts/publish-apt-repo.sh +0 -0
  350. {pushframe-5.1.3 → pushframe-5.1.5}/scripts/test-apt-journey-container.sh +0 -0
  351. {pushframe-5.1.3 → pushframe-5.1.5}/scripts/test-config-journey-container.sh +0 -0
  352. {pushframe-5.1.3 → pushframe-5.1.5}/scripts/test-deb-container.sh +0 -0
  353. {pushframe-5.1.3 → pushframe-5.1.5}/scripts/test-install-journey-container.sh +0 -0
  354. {pushframe-5.1.3 → pushframe-5.1.5}/scripts/test-multipair-journey-container.sh +0 -0
  355. {pushframe-5.1.3 → pushframe-5.1.5}/tests/conftest.py +0 -0
  356. {pushframe-5.1.3 → pushframe-5.1.5}/tests/fixtures/asset_unprocessed.json +0 -0
  357. {pushframe-5.1.3 → pushframe-5.1.5}/tests/fixtures/assets_page1.json +0 -0
  358. {pushframe-5.1.3 → pushframe-5.1.5}/tests/fixtures/assets_page2.json +0 -0
  359. {pushframe-5.1.3 → pushframe-5.1.5}/tests/fixtures/assets_placeholders.json +0 -0
  360. {pushframe-5.1.3 → pushframe-5.1.5}/tests/fixtures/error_envelope.json +0 -0
  361. {pushframe-5.1.3 → pushframe-5.1.5}/tests/fixtures/frame_detail.json +0 -0
  362. {pushframe-5.1.3 → pushframe-5.1.5}/tests/fixtures/frames.json +0 -0
  363. {pushframe-5.1.3 → pushframe-5.1.5}/tests/fixtures/google_rpc_snackc_page.json +0 -0
  364. {pushframe-5.1.3 → pushframe-5.1.5}/tests/fixtures/google_share_page_sample.html +0 -0
  365. {pushframe-5.1.3 → pushframe-5.1.5}/tests/fixtures/login.json +0 -0
  366. {pushframe-5.1.3 → pushframe-5.1.5}/tests/fixtures/probe_af_initdata_sample.html +0 -0
  367. {pushframe-5.1.3 → pushframe-5.1.5}/tests/offline.py +0 -0
  368. {pushframe-5.1.3 → pushframe-5.1.5}/tests/test_asset_partial.py +0 -0
  369. {pushframe-5.1.3 → pushframe-5.1.5}/tests/test_asset_unprocessed.py +0 -0
  370. {pushframe-5.1.3 → pushframe-5.1.5}/tests/test_aura_logger.py +0 -0
  371. {pushframe-5.1.3 → pushframe-5.1.5}/tests/test_batch_update_contract.py +0 -0
  372. {pushframe-5.1.3 → pushframe-5.1.5}/tests/test_cli_apply.py +0 -0
  373. {pushframe-5.1.3 → pushframe-5.1.5}/tests/test_cli_google_album.py +0 -0
  374. {pushframe-5.1.3 → pushframe-5.1.5}/tests/test_cli_google_link.py +0 -0
  375. {pushframe-5.1.3 → pushframe-5.1.5}/tests/test_cli_inspect.py +0 -0
  376. {pushframe-5.1.3 → pushframe-5.1.5}/tests/test_cli_push_budget_geo.py +0 -0
  377. {pushframe-5.1.3 → pushframe-5.1.5}/tests/test_cli_reconcile.py +0 -0
  378. {pushframe-5.1.3 → pushframe-5.1.5}/tests/test_cli_status_google.py +0 -0
  379. {pushframe-5.1.3 → pushframe-5.1.5}/tests/test_cli_sync.py +0 -0
  380. {pushframe-5.1.3 → pushframe-5.1.5}/tests/test_client_rate_limit.py +0 -0
  381. {pushframe-5.1.3 → pushframe-5.1.5}/tests/test_config_wizard.py +0 -0
  382. {pushframe-5.1.3 → pushframe-5.1.5}/tests/test_doctor.py +0 -0
  383. {pushframe-5.1.3 → pushframe-5.1.5}/tests/test_execute_plan.py +0 -0
  384. {pushframe-5.1.3 → pushframe-5.1.5}/tests/test_execute_plan_budget_geo.py +0 -0
  385. {pushframe-5.1.3 → pushframe-5.1.5}/tests/test_fixtures_validity.py +0 -0
  386. {pushframe-5.1.3 → pushframe-5.1.5}/tests/test_google_cache.py +0 -0
  387. {pushframe-5.1.3 → pushframe-5.1.5}/tests/test_google_client.py +0 -0
  388. {pushframe-5.1.3 → pushframe-5.1.5}/tests/test_google_enumerate.py +0 -0
  389. {pushframe-5.1.3 → pushframe-5.1.5}/tests/test_google_manifest.py +0 -0
  390. {pushframe-5.1.3 → pushframe-5.1.5}/tests/test_google_parsers.py +0 -0
  391. {pushframe-5.1.3 → pushframe-5.1.5}/tests/test_google_vault.py +0 -0
  392. {pushframe-5.1.3 → pushframe-5.1.5}/tests/test_gsync_all.py +0 -0
  393. {pushframe-5.1.3 → pushframe-5.1.5}/tests/test_gsync_engine.py +0 -0
  394. {pushframe-5.1.3 → pushframe-5.1.5}/tests/test_gsync_execute.py +0 -0
  395. {pushframe-5.1.3 → pushframe-5.1.5}/tests/test_imports.py +0 -0
  396. {pushframe-5.1.3 → pushframe-5.1.5}/tests/test_logout.py +0 -0
  397. {pushframe-5.1.3 → pushframe-5.1.5}/tests/test_migration.py +0 -0
  398. {pushframe-5.1.3 → pushframe-5.1.5}/tests/test_network_guard.py +0 -0
  399. {pushframe-5.1.3 → pushframe-5.1.5}/tests/test_offline_read_path.py +0 -0
  400. {pushframe-5.1.3 → pushframe-5.1.5}/tests/test_pairs.py +0 -0
  401. {pushframe-5.1.3 → pushframe-5.1.5}/tests/test_prep_upload_formats.py +0 -0
  402. {pushframe-5.1.3 → pushframe-5.1.5}/tests/test_probe_browser.py +0 -0
  403. {pushframe-5.1.3 → pushframe-5.1.5}/tests/test_probe_shared_link.py +0 -0
  404. {pushframe-5.1.3 → pushframe-5.1.5}/tests/test_ratelimit.py +0 -0
  405. {pushframe-5.1.3 → pushframe-5.1.5}/tests/test_read_path.py +0 -0
  406. {pushframe-5.1.3 → pushframe-5.1.5}/tests/test_reconcile.py +0 -0
  407. {pushframe-5.1.3 → pushframe-5.1.5}/tests/test_retry_401.py +0 -0
  408. {pushframe-5.1.3 → pushframe-5.1.5}/tests/test_schedule.py +0 -0
  409. {pushframe-5.1.3 → pushframe-5.1.5}/tests/test_session.py +0 -0
  410. {pushframe-5.1.3 → pushframe-5.1.5}/tests/test_settings_budget.py +0 -0
  411. {pushframe-5.1.3 → pushframe-5.1.5}/tests/test_settings_resolution.py +0 -0
  412. {pushframe-5.1.3 → pushframe-5.1.5}/tests/test_sync_engine.py +0 -0
  413. {pushframe-5.1.3 → pushframe-5.1.5}/tests/test_write_endpoints_failloud.py +0 -0
  414. {pushframe-5.1.3 → pushframe-5.1.5}/tests/test_write_formats_live.py +0 -0
  415. {pushframe-5.1.3 → pushframe-5.1.5}/tests/test_write_throttling.py +0 -0
@@ -0,0 +1,61 @@
1
+ ---
2
+ slug: status-crash-401-trip
3
+ status: resolved
4
+ created: 2026-09-30T16:20:00+02:00
5
+ updated: 2026-09-30T16:45:00+02:00
6
+ trigger: pushframe status crashes with a raw httpx traceback when the account's anti-abuse trip is armed (401 logout:true on frames.json)
7
+ ---
8
+
9
+ # Debug: status crashes with raw traceback on tripped-account 401
10
+
11
+ ## Symptoms
12
+
13
+ - expected: `pushframe status` resumes the stored token session and either lists frames or fails NAMED with a remedy (PRF-02 contract: never a traceback).
14
+ - actual: uncaught `httpx.HTTPStatusError` — a 3-exception chain ending in a raw traceback.
15
+ - error: `401 Unauthorized for https://api.pushd.com/v5/frames.json — server body: {"error": true, "message": "Request Unauthenticated", "logout": true}`
16
+ - timeline: visible since the venus trip (2026-09-30); the client DOES log the body via loguru (`_raise_for_status_with_body:270`) but nothing classifies or catches it on this path.
17
+ - reproduction: `pushframe status` on an account whose trip is armed (venus, live 2026-09-30 16:15).
18
+
19
+ ## Scope decisions (user)
20
+
21
+ - Fix the status UX crash ONLY — the trip itself is not a pushframe bug (the 24h protocol is documented).
22
+ - Contract: classify the 401 body — trip-shaped (`logout:true` / "Request Unauthenticated") → anti-abuse verdict + wait remedy, exit non-zero; token-shaped → `pushframe config` remedy.
23
+
24
+ ## Current Focus
25
+
26
+ hypothesis: run_status has no handler around the frames read; the 401 trip classification lives only in the WRITE path (execute_plan) and doctor, so a READ-surface 401 escapes as a raw exception.
27
+ test: read run_status' try/except and the write-path classifier; confirm no read-path catch.
28
+ expecting: no except clause covering HTTPStatusError from get_frames in run_status.
29
+ next_action: gather initial evidence (run_status, classifier, PRF-02 sweep inventory).
30
+
31
+ ## Evidence
32
+
33
+ - 2026-09-30T16:20:00+02:00: user transcript (venus 16:15) — establish_session SUCCEEDED (resume is offline: headers only), the 401 came from frame_api.get_frames; loguru warning from client.py:270 printed the body, then the exception propagated to the REPL.
34
+ - 2026-09-30T16:35:00+02:00: DECISIVE user experiment — with PUSHFRAME_EMAIL/PASSWORD env set (fresh login each run), `pushframe status` SUCCEEDS: login OK, frames listed (Cadre de Fabrice). Same account, same minutes: token path 401, env path 200.
35
+ - 2026-09-30T16:35:00+02:00: the 14:30 login-475 has LIFTED by ~16:35 (~2h) — the user's env login succeeded with no 475. Lockouts on the login surface clear in hours, not 24h.
36
+
37
+ ## Eliminated
38
+
39
+ - hypothesis: the anti-abuse trip spread to READS (refuted 16:35) — a fresh login reads frames.json fine; the 401-on-reads with the trip-shaped body was actually a DEAD STORED TOKEN. The 14:30 wave-4 interpretation ("reads refused too") is void: that probe resumed a stale token and never reached a write.
40
+ - hypothesis: account permanently 475-locked (refuted 16:35) — login surface answers again after ~2h.
41
+
42
+ ## Current Focus (updated 16:35)
43
+
44
+ hypothesis: CONFIRMED for the crash — run_status has no handler around get_frames(); the 401 trip classification lives only in the write path (sync._is_write_trip_body) and doctor. On the READ surface, the logout:true body means STALE TOKEN first (env-login reads green); trip-on-reads only if it recurs on a fresh token.
45
+ test: wrap the frames read; classify trip-shaped body (remedy: refresh token via config; caveat: if it recurs on a fresh login, it is the trip — wait); RateLimitError → wait message; generic → named + config remedy.
46
+ expecting: status exits 1 named, never tracebacks, on all three shapes.
47
+ next_action: implement + tests, then suite.
48
+
49
+ ## Resolution
50
+
51
+ root_cause: run_status had no handler around the frames read — the 401 trip classification lived only in the write path (sync._is_write_trip_body) and doctor, so a read-surface 401 escaped as a raw 3-exception-chain traceback (PRF-02 violation on the one read command every operator runs first). Root cause of the USER'S alarm was different: the stored token was dead (venus resumptions), while the env path's fresh login read frames fine — proving the trip never reached reads and the 14:30 login-475 had lifted in ~2h.
52
+ fix: run_status wraps get_frames() — trip-shaped 401 body → named message with the refresh-token remedy AND the fresh-token-24h caveat; other 401 → token-rejected remedy; RateLimitError (475/429) → named WAIT (never `pushframe config`); generic → named; all exit 1, zero tracebacks. Tests: 3 new in test_cli_status.py (trip body, token body, 475) — suite 507 passed.
53
+ verification: tests/test_cli_status.py 10/10; full offline suite 507 passed; user's live repro shape covered by the trip-body test.
54
+ files_changed: pushframe/cli.py, tests/test_cli_status.py, CHANGELOG.md, .planning/debug/status-crash-401-trip.md
55
+ addendum (16:50): followed by the one-shot TTY refresh — run_status now offers ONE re-login on a refused stored token and continues in the same run; a refused FRESH login prints the 24h verdict and is never retried; non-TTY fails named. tests/test_cli_status.py 12/12, suite 509 passed. Updated contract test: fresh env creds + trip body = trip-on-reads verdict.
56
+
57
+ ## Lessons
58
+
59
+ - The wave-4 interpretation "reads refused too" is VOID: that probe resumed a stale token and never reached a write. Trip-on-reads remains UNPROVEN; the 24h protocol now has a concrete re-entry criterion (same body on a FRESH login).
60
+ - The env path logs in fresh EVERY run — during tripped/token-dead periods it is the honest comparison path, but it also means each env `status` costs a login call (do not loop it).
61
+ - Silence clock resets: last pushd contact moved to 16:15–16:3x (status 401 + the user's env login). Next single probe: tomorrow ≥ 16:30, `pushframe status` on the refreshed token.
@@ -12,7 +12,58 @@ every channel.
12
12
  `pushframe` is an unofficial community CLI for Aura Frames digital photo
13
13
  frames; it is not affiliated with Aura Frames Inc.
14
14
 
15
- ## [Unreleased]
15
+ ## [5.1.5] - 2026-09-30
16
+
17
+ ### Fixed
18
+
19
+ - **`google-sync` no longer claims "no Google session vault" on a healthy
20
+ vault** (venus, 2026-09-30): the vault preflight probed the default path
21
+ WITHOUT `expanduser()` — a literal `~` path never exists, so the check
22
+ fired on every machine whose vault was perfectly healthy (a
23
+ `google-link` seconds earlier had saved it) — and its shape check
24
+ demanded a JSON object while the vault is a JSON list of cookie
25
+ records, so even the right path would have failed. Two stacked
26
+ phase-24 bugs, masked until now because CI has no vault either and the
27
+ scheduled `--pair` branch skips this block entirely. One tell was in
28
+ every transcript: `google-link` prints "existing Google session found"
29
+ only when a vault READ succeeds. The preflight now expands the default
30
+ path, accepts the list shape (dict kept for backward compatibility),
31
+ and takes a `vault_path` argument; `PUSHFRAME_VAULT_PATH` pins the
32
+ path for tests. The preflight-sweep suite is now pinned away from the
33
+ real machine's vault (it silently depended on the bug) and its
34
+ `gsync_no_creds` case asserts what it always meant to (Aura-side named
35
+ auth failure).
36
+
37
+ ### Added
38
+
39
+ - **One-shot TTY token refresh on every frames-reading verb** — `status`,
40
+ `inspect`, `sync`, `push` and `google-sync` (venus, 2026-09-30): when
41
+ the stored token is refused (401) and stdin is a terminal, the command
42
+ offers ONE re-login right there — the new token is persisted and the
43
+ same run continues (venus proved a fresh login reads green while the
44
+ stored token was dead). All five verbs share one gate
45
+ (`session.frames_read_with_refresh`), so the behavior, the failure
46
+ shapes and the guardrails are byte-identical everywhere. Guardrails:
47
+ exactly one attempt per process — a refused FRESH login is never
48
+ retried and prints the 24-hour-silence verdict (that means the
49
+ anti-abuse trip reached the read surface); non-TTY/scheduled runs
50
+ never prompt, they fail named with the `pushframe config` remedy; the
51
+ env-credential path (fresh credentials every run) never prompts.
52
+ One test per verb proves the contract: 401 once → exactly one
53
+ wizard-seam login → re-read succeeds → token persisted.
54
+
55
+ ### Fixed
56
+
57
+ - **`pushframe status` no longer tracebacks on a refused frames read**
58
+ (venus, debug session status-crash-401-trip): a 401 with the trip's
59
+ `logout:true` body prints a named diagnosis — the remedy is
60
+ `pushframe config` (refresh the stored token; a fresh login read frames
61
+ fine minutes later, so on a READ that body means a dead token first),
62
+ with the 24h-silence protocol reserved for the case where the same
63
+ body recurs on a genuinely fresh login. A 475/429 on the read prints a
64
+ named WAIT (never a re-login suggestion). Every shape exits 1 named —
65
+ the PRF-02 no-traceback contract now holds on the token path's first
66
+ authenticated call.
16
67
 
17
68
  ## [5.1.3] — 2026-09-30
18
69
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: pushframe
3
- Version: 5.1.3
3
+ Version: 5.1.5
4
4
  Summary: Unofficial community CLI for Aura Frames digital photo frames (sync a local folder or a Google Photos album; not affiliated with Aura Frames Inc.)
5
5
  Project-URL: Homepage, https://github.com/coredmp95/pushframe
6
6
  Project-URL: Repository, https://github.com/coredmp95/pushframe
@@ -282,10 +282,15 @@ it is the value a command would actually use).
282
282
 
283
283
  ```
284
284
  usage: pushframe status [-h]
285
- ```
286
-
287
- The first command to run. It checks your credentials are present, logs in, and lists your
288
- frames. Nothing is ever written.
285
+ ```The first command to run. With env credentials it logs in fresh; with a
286
+ stored token session it resumes it (no login call) and lists your frames.
287
+ Nothing is ever written except a session token: if the stored token is
288
+ refused (HTTP 401), an interactive run offers ONE re-login right there and
289
+ continues — a refused fresh login is never retried (that shape is the
290
+ anti-abuse trip on reads: 24-hour silence); a non-interactive run fails
291
+ named with the `pushframe config` remedy instead of prompting. `inspect`,
292
+ `sync`, `push` and `google-sync` read frames through the same gate, so
293
+ this contract holds on every frames-reading verb.
289
294
 
290
295
  ```bash
291
296
  uv run pushframe status
@@ -1,4 +1,4 @@
1
1
  # pushframe — unofficial community CLI for Aura Frames (not affiliated).
2
2
  # Version single source for the CLI surface (REL-02); pyproject keeps the
3
3
  # packaging metadata version and tests/test_version.py enforces lockstep.
4
- __version__ = "5.1.3"
4
+ __version__ = "5.1.5"
@@ -12,6 +12,8 @@ from tqdm import tqdm
12
12
  from pushframe.aura import Aura
13
13
  from pushframe.aws.s3client import S3Client
14
14
  from pushframe.aws.sqsclient import SQSClient
15
+ import httpx
16
+
15
17
  from pushframe.client import RateLimitError
16
18
  from pushframe.google.bootstrap import BootstrapError, PROFILE_ENV_VAR, default_bootstrap
17
19
  from pushframe.google.client import GoogleSession
@@ -609,6 +611,7 @@ def run_status(aura=None, debug: bool = False, google_session=None) -> int:
609
611
  # and before login/get_frames (the HTTP calls that trigger them).
610
612
  _configure_cli_logging(debug)
611
613
 
614
+ stored = None # the token-refresh seam only exists on the stored path
612
615
  if session[0] == 'env':
613
616
  try:
614
617
  aura.login()
@@ -625,7 +628,18 @@ def run_status(aura=None, debug: bool = False, google_session=None) -> int:
625
628
  user_id=stored.get('user_id'))
626
629
  who = stored['email']
627
630
 
628
- frames = aura.frame_api.get_frames()
631
+ # The frames read is the FIRST authenticated call of the token path —
632
+ # its failure modes are operator-relevant and must never traceback
633
+ # (PRF-02). Key lesson of 2026-09-30 (debug/status-crash-401-trip): on
634
+ # a READ, the trip-shaped logout:true body means a STALE STORED TOKEN
635
+ # first — a fresh login read frames fine minutes later — so the remedy
636
+ # is ONE TTY re-login right here (venus: the env path proved a fresh
637
+ # login reads green); the 24h wait is only the answer if a FRESH token
638
+ # is refused too. Never more than ONE refresh attempt per run, never a
639
+ # prompt off-TTY (scheduled runs fail named instead).
640
+ frames = _read_frames_with_refresh(aura, who, stored)
641
+ if frames is None:
642
+ return 1 # named failure already printed by the helper
629
643
  print(f'Logged in as {who}')
630
644
  print(f'{len(frames)} frames:')
631
645
  for frame in frames:
@@ -639,6 +653,14 @@ def run_status(aura=None, debug: bool = False, google_session=None) -> int:
639
653
  return 0
640
654
 
641
655
 
656
+ def _read_frames_with_refresh(aura, who, stored):
657
+ """Thin wrapper over the shared gate (session.frames_read_with_refresh)
658
+ — status, inspect, sync/push and google-sync all read frames through
659
+ the same named-failure + one-shot-refresh contract."""
660
+ from pushframe.session import frames_read_with_refresh
661
+ return frames_read_with_refresh(aura, who, stored)
662
+
663
+
642
664
  def _google_status_section(google_session=None) -> list[str]:
643
665
  """Build the `Google:` section lines for `status` (LGS-03/D-02).
644
666
 
@@ -941,8 +963,15 @@ def run_inspect(frame_arg: str, aura=None, debug: bool = False) -> int:
941
963
  print(f'Login failed: {e}')
942
964
  return 1
943
965
 
966
+ from pushframe.session import auto_refresh_stored
967
+ stored = auto_refresh_stored()
968
+ frames = _read_frames_with_refresh(
969
+ aura, who=(stored or {}).get('email')
970
+ or os.getenv('PUSHFRAME_EMAIL') or os.getenv('AURA_EMAIL'),
971
+ stored=stored)
972
+ if frames is None:
973
+ return 1
944
974
  try:
945
- frames = aura.frame_api.get_frames()
946
975
  resolved = resolve_frame(frame_arg, frames)
947
976
 
948
977
  if resolved.status == 'ambiguous':
@@ -1225,8 +1254,15 @@ def run_sync(dir_arg: str, frame_arg: str, apply: bool = False, yes: bool = Fals
1225
1254
  print(f'Login failed: {e}')
1226
1255
  return 1
1227
1256
 
1257
+ from pushframe.session import auto_refresh_stored
1258
+ stored = auto_refresh_stored()
1259
+ frames = _read_frames_with_refresh(
1260
+ aura, who=(stored or {}).get('email')
1261
+ or os.getenv('PUSHFRAME_EMAIL') or os.getenv('AURA_EMAIL'),
1262
+ stored=stored)
1263
+ if frames is None:
1264
+ return 1
1228
1265
  try:
1229
- frames = aura.frame_api.get_frames()
1230
1266
  resolved = resolve_frame(frame_arg, frames)
1231
1267
 
1232
1268
  if resolved.status == 'ambiguous':
@@ -215,7 +215,7 @@ def run_google_sync(album_target: str, frame_arg: str, *, apply: bool = False,
215
215
  aura=None, s3_client=None, sqs_client=None, budget=None,
216
216
  workers: int = 4, threshold: float | None = None,
217
217
  input_fn=None, is_interactive: bool | None = None,
218
- list_shared=None, cache_dir=None,
218
+ list_shared=None, cache_dir=None, vault_path=None,
219
219
  manifest_path=None, batch_size: int | None = None,
220
220
  pair: str | None = None, run_all: bool = False,
221
221
  scheduled: bool = False) -> int:
@@ -314,7 +314,7 @@ def run_google_sync(album_target: str, frame_arg: str, *, apply: bool = False,
314
314
  # with the full remedy (google-link + the headless ssh -X recipe).
315
315
  try:
316
316
  from pushframe.preflight import require_google_vault
317
- require_google_vault()
317
+ require_google_vault(vault_path)
318
318
  except Exception as e:
319
319
  print(f'google-sync failed: {e}')
320
320
  return 1
@@ -382,7 +382,12 @@ def run_google_sync(album_target: str, frame_arg: str, *, apply: bool = False,
382
382
  print(f'Login failed: {e}')
383
383
  return 1
384
384
 
385
- frames = aura.frame_api.get_frames()
385
+ from pushframe.session import auto_refresh_stored, frames_read_with_refresh
386
+ _stored = auto_refresh_stored()
387
+ frames = frames_read_with_refresh(
388
+ aura, who=(_stored or {}).get('email'), stored=_stored)
389
+ if frames is None:
390
+ return 2
386
391
  frame_res = resolve_frame(frame_arg, frames)
387
392
  if frame_res.status == 'ambiguous':
388
393
  print(f"'{frame_arg}' matches more than one frame:")
@@ -6,6 +6,7 @@ Google cookie vault; sync/push require the source directory to EXIST (D-03:
6
6
  hard error) while empty stays the friendly "nothing to do" (not an error —
7
7
  looping scripts over sometimes-empty dirs must not break).
8
8
  """
9
+ import os
9
10
  from pathlib import Path
10
11
 
11
12
 
@@ -14,11 +15,32 @@ class PreflightError(Exception):
14
15
  action; str() renders both."""
15
16
 
16
17
 
18
+ def default_vault_path() -> Path:
19
+ """The vault path google-sync's preflight probes: PUSHFRAME_VAULT_PATH
20
+ when set (tests and power users pin it — a suite must never depend on
21
+ the real machine's vault), else the production path expanded per-user."""
22
+ from pushframe.google.vault import DEFAULT_VAULT_PATH as _default
23
+ override = os.getenv("PUSHFRAME_VAULT_PATH")
24
+ return Path(override).expanduser() if override else _default.expanduser()
25
+
26
+
17
27
  def require_google_vault(vault_path: Path | None = None) -> Path:
18
- """google-sync's vault check (PRF-01): present + non-empty JSON object."""
28
+ """google-sync's vault check (PRF-01): present + parseable JSON holding
29
+ at least one cookie record.
30
+
31
+ Two 2026-09-30 live-proven bugs fixed here (venus: link saves fine,
32
+ google-sync claims 'no vault' anyway):
33
+ - the default path was probed WITHOUT expanduser(): a literal `~` path
34
+ never exists, so this preflight always fired on machines whose vault
35
+ was perfectly healthy (phase-24 regression, masked on CI — CI has no
36
+ vault either — and on scheduled runs, whose --pair branch skips this
37
+ block entirely);
38
+ - the shape check demanded a JSON OBJECT while vault.save() writes a
39
+ JSON LIST of cookie records — a healthy vault would have failed the
40
+ 'unreadable' branch even with the right path.
41
+ """
19
42
  import json
20
- from pushframe.google.vault import DEFAULT_VAULT_PATH as _default
21
- path = vault_path or _default
43
+ path = Path(vault_path).expanduser() if vault_path else default_vault_path()
22
44
  if not path.exists():
23
45
  raise PreflightError(
24
46
  f'no Google session vault at {path} — google-sync needs a linked '
@@ -27,7 +49,9 @@ def require_google_vault(vault_path: Path | None = None) -> Path:
27
49
  f'window can open; the 0600 vault survives logouts).')
28
50
  try:
29
51
  data = json.loads(path.read_text())
30
- if not isinstance(data, dict) or not data:
52
+ healthy = (isinstance(data, list) and len(data) > 0) or \
53
+ (isinstance(data, dict) and len(data) > 0)
54
+ if not healthy:
31
55
  raise ValueError('empty or malformed vault')
32
56
  except (ValueError, OSError) as e:
33
57
  raise PreflightError(
@@ -42,6 +42,145 @@ def _is_auth_failure(exc: Exception) -> bool:
42
42
  return '401' in str(exc) and 'unauthorized' in str(exc).lower()
43
43
 
44
44
 
45
+ def _tty() -> bool:
46
+ import sys
47
+ return sys.stdin.isatty()
48
+
49
+
50
+ def _run_login_prompt(aura, email):
51
+ """ONE interactive re-login on the wizard seam; persists the new token.
52
+ Returns the Aura with fresh auth headers; None on an empty-email abort
53
+ (only possible when nothing is stored)."""
54
+ print('refused — one re-login to refresh the stored token:')
55
+ result = _prompt_login(aura, _tty())
56
+ if result is None:
57
+ raise NoCredentialsError(
58
+ 'login aborted (no email given) — no session stored; '
59
+ 're-run the command to retry')
60
+ return result
61
+
62
+
63
+ def auto_refresh_stored():
64
+ """The stored dict when the auto-refresh case is even possible (a stored
65
+ token session, no env override) — None otherwise. Shared by every verb's
66
+ frames-read gate."""
67
+ import os
68
+ from pushframe import config_store
69
+ if ((os.getenv('PUSHFRAME_EMAIL') or os.getenv('AURA_EMAIL'))
70
+ and (os.getenv('PUSHFRAME_PASSWORD') or os.getenv('AURA_PASSWORD'))):
71
+ return None # env override: credentials are fresh every run
72
+ return config_store.load()
73
+
74
+
75
+ def frames_read_with_refresh(aura, who, stored):
76
+ """THE shared frames-read gate (status, inspect, sync/push, google-sync):
77
+ the ONE-shot TTY token refresh plus the named, never-a-traceback failure
78
+ shapes (PRF-02). Returns the frames list, or None after printing a named
79
+ failure (caller returns non-zero).
80
+
81
+ Refresh fires only via refresh_if_auto (stored session, no env override,
82
+ TTY) — exactly once per process. A FRESH login still refused with the
83
+ trip's logout:true body prints the 24-hour-silence verdict; other 401s
84
+ name the `pushframe config` remedy; 475/429 name the WAIT.
85
+ """
86
+ import httpx
87
+ from pushframe.client import RateLimitError
88
+ from pushframe.sync import _is_write_trip_body
89
+ while True:
90
+ try:
91
+ return aura.frame_api.get_frames()
92
+ except httpx.HTTPStatusError as e:
93
+ if e.response.status_code == 401 and stored and refresh_if_auto(aura, stored):
94
+ continue # exactly one re-read, on the fresh token
95
+ if e.response.status_code == 401:
96
+ fresh = stored is None # env path: credentials were already fresh
97
+ print(f'Logged in as {who}')
98
+ if _is_write_trip_body(str(e)):
99
+ print('frames read refused — HTTP 401 with the trip\'s '
100
+ 'signature body on a READ.')
101
+ if fresh:
102
+ print('FRESH credentials were refused with the '
103
+ 'trip\'s body: this is the anti-abuse trip on '
104
+ 'the read surface — total silence 24h, one '
105
+ 'probe after. Do not loop.')
106
+ else:
107
+ print('The stored token is dead OR the trip reached '
108
+ 'reads — refresh once via `pushframe config` '
109
+ '(Enter keeps the email). If a FRESH login is '
110
+ 'refused too, that is the trip on reads: '
111
+ 'total silence 24h, one probe after.')
112
+ else:
113
+ print(f'frames read refused — HTTP 401: {e}')
114
+ print('The stored session looks rejected: run '
115
+ '`pushframe config` to refresh the token (Enter '
116
+ 'keeps the email), then re-run.')
117
+ return None
118
+ print(f'frames read failed: {e}')
119
+ return None
120
+ except RateLimitError as e:
121
+ print(f'Logged in as {who}')
122
+ print(f'rate-limited or locked out while reading frames — {e}')
123
+ print('Wait before retrying; do NOT loop this command (each call '
124
+ 'extends the lockout).')
125
+ return None
126
+
127
+
128
+ def refresh_if_auto(aura, stored) -> bool:
129
+ """Refresh ONLY when the refusal is the auto-refresh case: a stored
130
+ token session (no env creds) on an interactive terminal — that is, when
131
+ `pushframe config` could fix it without the operator typing anything
132
+ new. Returns True when the aura was refreshed and the caller may retry
133
+ its first authenticated read ONCE; False when the caller must fail the
134
+ named way (no stored session, env override, or non-TTY).
135
+
136
+ Guardrails (venus anti-abuse contract, 2026-09-30): exactly one attempt
137
+ per process, a refused FRESH login is never retried (its SessionExpired
138
+ error carries the 24h-silence verdict), non-TTY never prompts.
139
+ """
140
+ import os
141
+ import sys
142
+ if stored is None or not stored.get('email') or not stored.get('auth_token'):
143
+ return False
144
+ if (os.getenv('PUSHFRAME_EMAIL') or os.getenv('AURA_EMAIL')) and \
145
+ (os.getenv('PUSHFRAME_PASSWORD') or os.getenv('AURA_PASSWORD')):
146
+ return False # env override: creds are already fresh every run
147
+ if not sys.stdin.isatty():
148
+ return False
149
+ try:
150
+ refresh_token_once(aura, stored)
151
+ except Exception:
152
+ return False
153
+ return True
154
+
155
+
156
+ def refresh_token_once(aura, stored: dict):
157
+ """The anti-abuse-bounded token refresh (venus, 2026-09-30): on a 401
158
+ refusal of the STORED token, a TTY run gets ONE re-login prompt instead
159
+ of failing. ONE attempt per process — a refused FRESH login is never
160
+ retried (retrying a tripped surface feeds the trip), and non-TTY runs
161
+ (scheduled) never prompt at all: they raise SessionExpiredError, named.
162
+
163
+ Returns the authenticated Aura, or raises (SessionExpiredError /
164
+ NoCredentialsError). The caller's 401 message travels in the exception
165
+ chain for the trip-vs-token classification downstream.
166
+ """
167
+ import sys
168
+ if not sys.stdin.isatty():
169
+ raise SessionExpiredError(
170
+ 'stored token refused and stdin is not a terminal — re-run '
171
+ '`pushframe config` once interactively to refresh it')
172
+ print(f'stored token refused (current email: {stored["email"]})')
173
+ try:
174
+ return _run_login_prompt(aura, stored['email'])
175
+ except Exception as e:
176
+ if _is_auth_failure(e):
177
+ raise SessionExpiredError(
178
+ 'the FRESH login was refused too — this is not a stale '
179
+ 'token: total silence (no command, no re-login) and one '
180
+ 'probe after 24h (the anti-abuse protocol); do NOT loop.') from e
181
+ raise
182
+
183
+
45
184
  def _store_token(email: str, auth_token: str, user_id: str | None) -> None:
46
185
  """Persist the session facts — token only, never the password (SEC-01)."""
47
186
  data = {'email': email, 'auth_token': auth_token}
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "pushframe"
3
- version = "5.1.3"
3
+ version = "5.1.5"
4
4
  description = "Unofficial community CLI for Aura Frames digital photo frames (sync a local folder or a Google Photos album; not affiliated with Aura Frames Inc.)"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.14"
@@ -30,6 +30,134 @@ def _reset_loguru(monkeypatch):
30
30
  logger.remove()
31
31
 
32
32
 
33
+ FRAME_401_TRIP_BODY = httpx.Response(
34
+ 401, json={"error": True, "message": "Request Unauthenticated", "logout": True})
35
+ FRAME_401_TOKEN_BODY = httpx.Response(
36
+ 401, json={"error": True, "message": "invalid session token"})
37
+
38
+
39
+ def test_status_fresh_creds_refused_with_trip_body_means_the_trip(monkeypatch, capsys):
40
+ """Debug session status-crash-401-trip, updated contract: on the env
41
+ path the credentials are ALREADY fresh — a trip-shaped 401 body there
42
+ is the anti-abuse trip on reads: 24h-silence verdict, no re-login
43
+ suggestion, never a traceback."""
44
+ monkeypatch.setenv('AURA_EMAIL', 'you@example.invalid')
45
+ monkeypatch.setenv('AURA_PASSWORD', 'super-secret-pw')
46
+
47
+ aura = offline_aura(overrides={'/v5/frames.json': FRAME_401_TRIP_BODY})
48
+ rc = run_status(aura=aura)
49
+
50
+ assert rc == 1
51
+ out = capsys.readouterr().out
52
+ assert 'HTTP 401' in out
53
+ assert 'FRESH credentials' in out
54
+ assert '24h' in out
55
+ assert 'Traceback' not in out
56
+
57
+
58
+ def test_status_stale_token_401_non_tty_fails_named_without_refresh(monkeypatch, capsys, tmp_path):
59
+ """Scheduled/no-TTY runs NEVER prompt: the refresh guard raises the
60
+ named SessionExpiredError and status surfaces it with the config
61
+ remedy — exit 1, zero tracebacks."""
62
+ from pushframe.utils import settings
63
+ monkeypatch.setattr(settings, 'CONFIG_PATH', tmp_path / 'config.json')
64
+ from pushframe import config_store
65
+ config_store.update(email='vaulted@example.invalid', auth_token='tok-dead',
66
+ user_id='u-1')
67
+ monkeypatch.delenv('AURA_EMAIL', raising=False)
68
+ monkeypatch.delenv('AURA_PASSWORD', raising=False)
69
+ monkeypatch.setattr('sys.stdin.isatty', lambda: False)
70
+
71
+ aura = offline_aura(overrides={'/v5/frames.json': FRAME_401_TOKEN_BODY})
72
+ rc = run_status(aura=aura)
73
+
74
+ assert rc == 1
75
+ out = capsys.readouterr().out
76
+ assert 'refresh' in out and 'pushframe config' in out
77
+ assert 'Traceback' not in out
78
+
79
+
80
+ def test_status_stale_token_401_tty_refreshes_once_and_succeeds(monkeypatch, capsys, tmp_path):
81
+ """The venus story, automated: stored token refused → ONE TTY re-login
82
+ prompt → new token persisted → the SAME run lists the frames. Never a
83
+ second attempt: the login is called exactly once."""
84
+ from pushframe.utils import settings
85
+ monkeypatch.setattr(settings, 'CONFIG_PATH', tmp_path / 'config.json')
86
+ from pushframe import config_store
87
+ config_store.update(email='vaulted@example.invalid', auth_token='tok-dead',
88
+ user_id='u-1')
89
+ monkeypatch.delenv('AURA_EMAIL', raising=False)
90
+ monkeypatch.delenv('AURA_PASSWORD', raising=False)
91
+ monkeypatch.setattr('sys.stdin.isatty', lambda: True)
92
+
93
+ logins = []
94
+
95
+ import getpass as gp
96
+ monkeypatch.setattr(gp, 'getpass', lambda *a: 'typed-pw') # noqa: S105
97
+ monkeypatch.setattr('builtins.input', lambda *a: 'vaulted@example.invalid')
98
+
99
+ def fake_wizard_login(email, password):
100
+ logins.append((email, password))
101
+ return {'email': email, 'auth_token': 'fresh-tok', # noqa: S105
102
+ 'user_id': 'u-1', 'frames': []}
103
+
104
+ monkeypatch.setattr('pushframe.cli._wizard_login', fake_wizard_login)
105
+
106
+ # frames.json: the trip-shaped 401 once (dead token), then the normal
107
+ # fixture listing after the refresh.
108
+ state = {'calls': 0}
109
+ from tests.offline import FIXTURES_DIR
110
+ frames_fixture = (FIXTURES_DIR / 'frames.json').read_text()
111
+
112
+ def frames_route(request):
113
+ state['calls'] += 1
114
+ if state['calls'] == 1:
115
+ return FRAME_401_TRIP_BODY
116
+ return httpx.Response(200, content=frames_fixture)
117
+
118
+ aura = offline_aura(overrides={'/v5/frames.json': frames_route})
119
+
120
+ rc = run_status(aura=aura)
121
+
122
+ assert rc == 0
123
+ out = capsys.readouterr().out
124
+ assert 'Fake Frame' in out
125
+ assert logins == [('vaulted@example.invalid', 'typed-pw')] # noqa: S105 — exactly ONE
126
+ assert state['calls'] == 2 # refused once, re-read once after refresh
127
+ assert config_store.load()['auth_token'] == 'fresh-tok' # noqa: S105
128
+
129
+
130
+ def test_status_token_shaped_401_names_the_config_remedy(monkeypatch, capsys):
131
+ monkeypatch.setenv('AURA_EMAIL', 'you@example.invalid')
132
+ monkeypatch.setenv('AURA_PASSWORD', 'super-secret-pw')
133
+
134
+ aura = offline_aura(overrides={'/v5/frames.json': FRAME_401_TOKEN_BODY})
135
+ rc = run_status(aura=aura)
136
+
137
+ assert rc == 1
138
+ out = capsys.readouterr().out
139
+ assert 'HTTP 401' in out and 'pushframe config' in out
140
+ assert 'Traceback' not in out
141
+
142
+
143
+ def test_status_rate_limit_475_named_wait_message(monkeypatch, capsys):
144
+ """A 475 on the frames read is classified by the client layer itself
145
+ (_raise_if_rate_limited): status must surface it as a named WAIT, never
146
+ a traceback."""
147
+ monkeypatch.setenv('AURA_EMAIL', 'you@example.invalid')
148
+ monkeypatch.setenv('AURA_PASSWORD', 'super-secret-pw')
149
+
150
+ aura = offline_aura(overrides={'/v5/frames.json': httpx.Response(
151
+ 475, text='The Aura API is rate-limiting or has locked out this account.')})
152
+ rc = run_status(aura=aura)
153
+
154
+ assert rc == 1
155
+ out = capsys.readouterr().out
156
+ assert 'rate-limited or locked out' in out
157
+ assert 'pushframe config' not in out # a 475 is a WAIT, not a token fix
158
+ assert 'Traceback' not in out
159
+
160
+
33
161
  def test_status_missing_creds_exits_nonzero_no_network(monkeypatch, capsys):
34
162
  monkeypatch.delenv('AURA_EMAIL', raising=False)
35
163
  monkeypatch.delenv('AURA_PASSWORD', raising=False)
@@ -34,7 +34,7 @@ CASES = [
34
34
  id="mode-8-inspect-no-creds"),
35
35
  pytest.param(
36
36
  "gsync_no_creds", {"album": "Cadre", "frame": "Fabrice"},
37
- "google-sync failed", "google-link",
37
+ "not authenticated", "pushframe config",
38
38
  id="mode-8-gsync-no-creds"),
39
39
  pytest.param(
40
40
  "sync_dir_no_creds", {"dir": "."},
@@ -68,6 +68,10 @@ def no_creds(tmp_path, monkeypatch):
68
68
  for var in ("PUSHFRAME_EMAIL", "AURA_EMAIL",
69
69
  "PUSHFRAME_PASSWORD", "AURA_PASSWORD"):
70
70
  monkeypatch.delenv(var, raising=False)
71
+ # The vault preflight is pinned AWAY from the real machine's vault: after
72
+ # the 2026-09-30 path fix this suite would otherwise depend on whether
73
+ # the dev box happens to hold a healthy vault (venus live-proven bug).
74
+ monkeypatch.setenv("PUSHFRAME_VAULT_PATH", str(tmp_path / "no-vault.json"))
71
75
 
72
76
 
73
77
  # --- callables ---------------------------------------------------------------
@@ -109,10 +113,10 @@ def gsync_no_creds(capsys, no_creds, album, frame):
109
113
  # named because no credentials of any kind are available.
110
114
  vault = settings.CONFIG_PATH.parent / "google-cookies.json"
111
115
  vault.parent.mkdir(parents=True, exist_ok=True)
112
- vault.write_text(json.dumps({"SID": "x"})) # minimal-looking vault
116
+ vault.write_text(json.dumps([{"name": "SID", "value": "x"}])) # list shape (vault.save's)
113
117
  from pushframe.gsync import run_google_sync
114
118
  return run_google_sync(album, frame, apply=False, yes=True,
115
- is_interactive=False)
119
+ is_interactive=False, vault_path=str(vault))
116
120
 
117
121
 
118
122
  class _MuteAura: