agent-notes 2.9.0__tar.gz → 2.10.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 (225) hide show
  1. {agent_notes-2.9.0 → agent_notes-2.10.0}/PKG-INFO +1 -1
  2. agent_notes-2.10.0/agent_notes/VERSION +1 -0
  3. {agent_notes-2.9.0 → agent_notes-2.10.0}/agent_notes/commands/wizard.py +30 -1
  4. {agent_notes-2.9.0 → agent_notes-2.10.0}/agent_notes/services/fs.py +18 -22
  5. {agent_notes-2.9.0 → agent_notes-2.10.0}/agent_notes/services/installer.py +156 -1
  6. {agent_notes-2.9.0 → agent_notes-2.10.0}/agent_notes.egg-info/PKG-INFO +1 -1
  7. {agent_notes-2.9.0 → agent_notes-2.10.0}/agent_notes.egg-info/SOURCES.txt +3 -0
  8. agent_notes-2.10.0/tests/unit/commands/test_wizard_preflight.py +232 -0
  9. agent_notes-2.10.0/tests/unit/services/test_fs.py +192 -0
  10. agent_notes-2.10.0/tests/unit/services/test_installer_plan.py +293 -0
  11. agent_notes-2.9.0/agent_notes/VERSION +0 -1
  12. {agent_notes-2.9.0 → agent_notes-2.10.0}/LICENSE +0 -0
  13. {agent_notes-2.9.0 → agent_notes-2.10.0}/README.md +0 -0
  14. {agent_notes-2.9.0 → agent_notes-2.10.0}/agent_notes/__init__.py +0 -0
  15. {agent_notes-2.9.0 → agent_notes-2.10.0}/agent_notes/__main__.py +0 -0
  16. {agent_notes-2.9.0 → agent_notes-2.10.0}/agent_notes/cli.py +0 -0
  17. {agent_notes-2.9.0 → agent_notes-2.10.0}/agent_notes/commands/__init__.py +0 -0
  18. {agent_notes-2.9.0 → agent_notes-2.10.0}/agent_notes/commands/_install_helpers.py +0 -0
  19. {agent_notes-2.9.0 → agent_notes-2.10.0}/agent_notes/commands/build.py +0 -0
  20. {agent_notes-2.9.0 → agent_notes-2.10.0}/agent_notes/commands/config.py +0 -0
  21. {agent_notes-2.9.0 → agent_notes-2.10.0}/agent_notes/commands/doctor.py +0 -0
  22. {agent_notes-2.9.0 → agent_notes-2.10.0}/agent_notes/commands/info.py +0 -0
  23. {agent_notes-2.9.0 → agent_notes-2.10.0}/agent_notes/commands/install.py +0 -0
  24. {agent_notes-2.9.0 → agent_notes-2.10.0}/agent_notes/commands/list.py +0 -0
  25. {agent_notes-2.9.0 → agent_notes-2.10.0}/agent_notes/commands/memory.py +0 -0
  26. {agent_notes-2.9.0 → agent_notes-2.10.0}/agent_notes/commands/regenerate.py +0 -0
  27. {agent_notes-2.9.0 → agent_notes-2.10.0}/agent_notes/commands/set_role.py +0 -0
  28. {agent_notes-2.9.0 → agent_notes-2.10.0}/agent_notes/commands/uninstall.py +0 -0
  29. {agent_notes-2.9.0 → agent_notes-2.10.0}/agent_notes/commands/update.py +0 -0
  30. {agent_notes-2.9.0 → agent_notes-2.10.0}/agent_notes/commands/validate.py +0 -0
  31. {agent_notes-2.9.0 → agent_notes-2.10.0}/agent_notes/config.py +0 -0
  32. {agent_notes-2.9.0 → agent_notes-2.10.0}/agent_notes/data/agents/agents.yaml +0 -0
  33. {agent_notes-2.9.0 → agent_notes-2.10.0}/agent_notes/data/agents/analyst.md +0 -0
  34. {agent_notes-2.9.0 → agent_notes-2.10.0}/agent_notes/data/agents/api-reviewer.md +0 -0
  35. {agent_notes-2.9.0 → agent_notes-2.10.0}/agent_notes/data/agents/architect.md +0 -0
  36. {agent_notes-2.9.0 → agent_notes-2.10.0}/agent_notes/data/agents/coder.md +0 -0
  37. {agent_notes-2.9.0 → agent_notes-2.10.0}/agent_notes/data/agents/database-specialist.md +0 -0
  38. {agent_notes-2.9.0 → agent_notes-2.10.0}/agent_notes/data/agents/debugger.md +0 -0
  39. {agent_notes-2.9.0 → agent_notes-2.10.0}/agent_notes/data/agents/devil.md +0 -0
  40. {agent_notes-2.9.0 → agent_notes-2.10.0}/agent_notes/data/agents/devops.md +0 -0
  41. {agent_notes-2.9.0 → agent_notes-2.10.0}/agent_notes/data/agents/explorer.md +0 -0
  42. {agent_notes-2.9.0 → agent_notes-2.10.0}/agent_notes/data/agents/integrations.md +0 -0
  43. {agent_notes-2.9.0 → agent_notes-2.10.0}/agent_notes/data/agents/lead.md +0 -0
  44. {agent_notes-2.9.0 → agent_notes-2.10.0}/agent_notes/data/agents/performance-profiler.md +0 -0
  45. {agent_notes-2.9.0 → agent_notes-2.10.0}/agent_notes/data/agents/refactorer.md +0 -0
  46. {agent_notes-2.9.0 → agent_notes-2.10.0}/agent_notes/data/agents/reviewer.md +0 -0
  47. {agent_notes-2.9.0 → agent_notes-2.10.0}/agent_notes/data/agents/security-auditor.md +0 -0
  48. {agent_notes-2.9.0 → agent_notes-2.10.0}/agent_notes/data/agents/shared/cost_reporting.md +0 -0
  49. {agent_notes-2.9.0 → agent_notes-2.10.0}/agent_notes/data/agents/system-auditor.md +0 -0
  50. {agent_notes-2.9.0 → agent_notes-2.10.0}/agent_notes/data/agents/tech-writer.md +0 -0
  51. {agent_notes-2.9.0 → agent_notes-2.10.0}/agent_notes/data/agents/test-runner.md +0 -0
  52. {agent_notes-2.9.0 → agent_notes-2.10.0}/agent_notes/data/agents/test-writer.md +0 -0
  53. {agent_notes-2.9.0 → agent_notes-2.10.0}/agent_notes/data/cli/claude.yaml +0 -0
  54. {agent_notes-2.9.0 → agent_notes-2.10.0}/agent_notes/data/cli/copilot.yaml +0 -0
  55. {agent_notes-2.9.0 → agent_notes-2.10.0}/agent_notes/data/cli/opencode.yaml +0 -0
  56. {agent_notes-2.9.0 → agent_notes-2.10.0}/agent_notes/data/commands/brainstorm.md +0 -0
  57. {agent_notes-2.9.0 → agent_notes-2.10.0}/agent_notes/data/commands/debug.md +0 -0
  58. {agent_notes-2.9.0 → agent_notes-2.10.0}/agent_notes/data/commands/review.md +0 -0
  59. {agent_notes-2.9.0 → agent_notes-2.10.0}/agent_notes/data/global-claude.md +0 -0
  60. {agent_notes-2.9.0 → agent_notes-2.10.0}/agent_notes/data/global-copilot.md +0 -0
  61. {agent_notes-2.9.0 → agent_notes-2.10.0}/agent_notes/data/global-opencode.md +0 -0
  62. {agent_notes-2.9.0 → agent_notes-2.10.0}/agent_notes/data/hooks/session-context.md.tpl +0 -0
  63. {agent_notes-2.9.0 → agent_notes-2.10.0}/agent_notes/data/models/claude-haiku-4-5.yaml +0 -0
  64. {agent_notes-2.9.0 → agent_notes-2.10.0}/agent_notes/data/models/claude-opus-4-1.yaml +0 -0
  65. {agent_notes-2.9.0 → agent_notes-2.10.0}/agent_notes/data/models/claude-opus-4-5.yaml +0 -0
  66. {agent_notes-2.9.0 → agent_notes-2.10.0}/agent_notes/data/models/claude-opus-4-6.yaml +0 -0
  67. {agent_notes-2.9.0 → agent_notes-2.10.0}/agent_notes/data/models/claude-opus-4-7.yaml +0 -0
  68. {agent_notes-2.9.0 → agent_notes-2.10.0}/agent_notes/data/models/claude-sonnet-4-5.yaml +0 -0
  69. {agent_notes-2.9.0 → agent_notes-2.10.0}/agent_notes/data/models/claude-sonnet-4-6.yaml +0 -0
  70. {agent_notes-2.9.0 → agent_notes-2.10.0}/agent_notes/data/models/claude-sonnet-4.yaml +0 -0
  71. {agent_notes-2.9.0 → agent_notes-2.10.0}/agent_notes/data/plugin/claude.yaml +0 -0
  72. {agent_notes-2.9.0 → agent_notes-2.10.0}/agent_notes/data/plugin/opencode-index.js.template +0 -0
  73. {agent_notes-2.9.0 → agent_notes-2.10.0}/agent_notes/data/plugin/opencode.yaml +0 -0
  74. {agent_notes-2.9.0 → agent_notes-2.10.0}/agent_notes/data/pricing.yaml +0 -0
  75. {agent_notes-2.9.0 → agent_notes-2.10.0}/agent_notes/data/roles/orchestrator.yaml +0 -0
  76. {agent_notes-2.9.0 → agent_notes-2.10.0}/agent_notes/data/roles/reasoner.yaml +0 -0
  77. {agent_notes-2.9.0 → agent_notes-2.10.0}/agent_notes/data/roles/scout.yaml +0 -0
  78. {agent_notes-2.9.0 → agent_notes-2.10.0}/agent_notes/data/roles/worker.yaml +0 -0
  79. {agent_notes-2.9.0 → agent_notes-2.10.0}/agent_notes/data/rules/code-quality.md +0 -0
  80. {agent_notes-2.9.0 → agent_notes-2.10.0}/agent_notes/data/rules/safety.md +0 -0
  81. {agent_notes-2.9.0 → agent_notes-2.10.0}/agent_notes/data/skills/brainstorming/SKILL.md +0 -0
  82. {agent_notes-2.9.0 → agent_notes-2.10.0}/agent_notes/data/skills/caveman/SKILL.md +0 -0
  83. {agent_notes-2.9.0 → agent_notes-2.10.0}/agent_notes/data/skills/code-review/SKILL.md +0 -0
  84. {agent_notes-2.9.0 → agent_notes-2.10.0}/agent_notes/data/skills/debugging-protocol/SKILL.md +0 -0
  85. {agent_notes-2.9.0 → agent_notes-2.10.0}/agent_notes/data/skills/docker-compose/SKILL.md +0 -0
  86. {agent_notes-2.9.0 → agent_notes-2.10.0}/agent_notes/data/skills/docker-compose-advanced/SKILL.md +0 -0
  87. {agent_notes-2.9.0 → agent_notes-2.10.0}/agent_notes/data/skills/docker-dockerfile/SKILL.md +0 -0
  88. {agent_notes-2.9.0 → agent_notes-2.10.0}/agent_notes/data/skills/docker-dockerfile-languages/SKILL.md +0 -0
  89. {agent_notes-2.9.0 → agent_notes-2.10.0}/agent_notes/data/skills/git/SKILL.md +0 -0
  90. {agent_notes-2.9.0 → agent_notes-2.10.0}/agent_notes/data/skills/grill-me/SKILL.md +0 -0
  91. {agent_notes-2.9.0 → agent_notes-2.10.0}/agent_notes/data/skills/grill-with-docs/SKILL.md +0 -0
  92. {agent_notes-2.9.0 → agent_notes-2.10.0}/agent_notes/data/skills/improve-codebase-architecture/SKILL.md +0 -0
  93. {agent_notes-2.9.0 → agent_notes-2.10.0}/agent_notes/data/skills/obsidian-memory/SKILL.md +0 -0
  94. {agent_notes-2.9.0 → agent_notes-2.10.0}/agent_notes/data/skills/rails-active-storage/SKILL.md +0 -0
  95. {agent_notes-2.9.0 → agent_notes-2.10.0}/agent_notes/data/skills/rails-broadcasting/SKILL.md +0 -0
  96. {agent_notes-2.9.0 → agent_notes-2.10.0}/agent_notes/data/skills/rails-concerns/SKILL.md +0 -0
  97. {agent_notes-2.9.0 → agent_notes-2.10.0}/agent_notes/data/skills/rails-controllers/SKILL.md +0 -0
  98. {agent_notes-2.9.0 → agent_notes-2.10.0}/agent_notes/data/skills/rails-controllers-advanced/SKILL.md +0 -0
  99. {agent_notes-2.9.0 → agent_notes-2.10.0}/agent_notes/data/skills/rails-helpers/SKILL.md +0 -0
  100. {agent_notes-2.9.0 → agent_notes-2.10.0}/agent_notes/data/skills/rails-initializers/SKILL.md +0 -0
  101. {agent_notes-2.9.0 → agent_notes-2.10.0}/agent_notes/data/skills/rails-javascript/SKILL.md +0 -0
  102. {agent_notes-2.9.0 → agent_notes-2.10.0}/agent_notes/data/skills/rails-jobs/SKILL.md +0 -0
  103. {agent_notes-2.9.0 → agent_notes-2.10.0}/agent_notes/data/skills/rails-kamal/SKILL.md +0 -0
  104. {agent_notes-2.9.0 → agent_notes-2.10.0}/agent_notes/data/skills/rails-lib/SKILL.md +0 -0
  105. {agent_notes-2.9.0 → agent_notes-2.10.0}/agent_notes/data/skills/rails-mailers/SKILL.md +0 -0
  106. {agent_notes-2.9.0 → agent_notes-2.10.0}/agent_notes/data/skills/rails-migrations/SKILL.md +0 -0
  107. {agent_notes-2.9.0 → agent_notes-2.10.0}/agent_notes/data/skills/rails-models/SKILL.md +0 -0
  108. {agent_notes-2.9.0 → agent_notes-2.10.0}/agent_notes/data/skills/rails-models-advanced/SKILL.md +0 -0
  109. {agent_notes-2.9.0 → agent_notes-2.10.0}/agent_notes/data/skills/rails-routes/SKILL.md +0 -0
  110. {agent_notes-2.9.0 → agent_notes-2.10.0}/agent_notes/data/skills/rails-style/SKILL.md +0 -0
  111. {agent_notes-2.9.0 → agent_notes-2.10.0}/agent_notes/data/skills/rails-testing-controllers/SKILL.md +0 -0
  112. {agent_notes-2.9.0 → agent_notes-2.10.0}/agent_notes/data/skills/rails-testing-models/SKILL.md +0 -0
  113. {agent_notes-2.9.0 → agent_notes-2.10.0}/agent_notes/data/skills/rails-testing-system/SKILL.md +0 -0
  114. {agent_notes-2.9.0 → agent_notes-2.10.0}/agent_notes/data/skills/rails-validations/SKILL.md +0 -0
  115. {agent_notes-2.9.0 → agent_notes-2.10.0}/agent_notes/data/skills/rails-view-components/SKILL.md +0 -0
  116. {agent_notes-2.9.0 → agent_notes-2.10.0}/agent_notes/data/skills/rails-view-components-advanced/SKILL.md +0 -0
  117. {agent_notes-2.9.0 → agent_notes-2.10.0}/agent_notes/data/skills/rails-views/SKILL.md +0 -0
  118. {agent_notes-2.9.0 → agent_notes-2.10.0}/agent_notes/data/skills/rails-views-advanced/SKILL.md +0 -0
  119. {agent_notes-2.9.0 → agent_notes-2.10.0}/agent_notes/data/skills/refactoring-protocol/SKILL.md +0 -0
  120. {agent_notes-2.9.0 → agent_notes-2.10.0}/agent_notes/data/skills/setup-project-context/SKILL.md +0 -0
  121. {agent_notes-2.9.0 → agent_notes-2.10.0}/agent_notes/data/skills/tdd/SKILL.md +0 -0
  122. {agent_notes-2.9.0 → agent_notes-2.10.0}/agent_notes/data/skills/zoom-out/SKILL.md +0 -0
  123. {agent_notes-2.9.0 → agent_notes-2.10.0}/agent_notes/data/templates/__init__.py +0 -0
  124. {agent_notes-2.9.0 → agent_notes-2.10.0}/agent_notes/data/templates/__pycache__/__init__.cpython-314.pyc +0 -0
  125. {agent_notes-2.9.0 → agent_notes-2.10.0}/agent_notes/data/templates/frontmatter/__init__.py +0 -0
  126. {agent_notes-2.9.0 → agent_notes-2.10.0}/agent_notes/data/templates/frontmatter/__pycache__/__init__.cpython-314.pyc +0 -0
  127. {agent_notes-2.9.0 → agent_notes-2.10.0}/agent_notes/data/templates/frontmatter/__pycache__/claude.cpython-314.pyc +0 -0
  128. {agent_notes-2.9.0 → agent_notes-2.10.0}/agent_notes/data/templates/frontmatter/__pycache__/opencode.cpython-314.pyc +0 -0
  129. {agent_notes-2.9.0 → agent_notes-2.10.0}/agent_notes/data/templates/frontmatter/claude.py +0 -0
  130. {agent_notes-2.9.0 → agent_notes-2.10.0}/agent_notes/data/templates/frontmatter/opencode.py +0 -0
  131. {agent_notes-2.9.0 → agent_notes-2.10.0}/agent_notes/doctor_checks.py +0 -0
  132. {agent_notes-2.9.0 → agent_notes-2.10.0}/agent_notes/domain/__init__.py +0 -0
  133. {agent_notes-2.9.0 → agent_notes-2.10.0}/agent_notes/domain/agent.py +0 -0
  134. {agent_notes-2.9.0 → agent_notes-2.10.0}/agent_notes/domain/cli_backend.py +0 -0
  135. {agent_notes-2.9.0 → agent_notes-2.10.0}/agent_notes/domain/diagnostics.py +0 -0
  136. {agent_notes-2.9.0 → agent_notes-2.10.0}/agent_notes/domain/diff.py +0 -0
  137. {agent_notes-2.9.0 → agent_notes-2.10.0}/agent_notes/domain/model.py +0 -0
  138. {agent_notes-2.9.0 → agent_notes-2.10.0}/agent_notes/domain/role.py +0 -0
  139. {agent_notes-2.9.0 → agent_notes-2.10.0}/agent_notes/domain/rule.py +0 -0
  140. {agent_notes-2.9.0 → agent_notes-2.10.0}/agent_notes/domain/skill.py +0 -0
  141. {agent_notes-2.9.0 → agent_notes-2.10.0}/agent_notes/domain/state.py +0 -0
  142. {agent_notes-2.9.0 → agent_notes-2.10.0}/agent_notes/install_state.py +0 -0
  143. {agent_notes-2.9.0 → agent_notes-2.10.0}/agent_notes/registries/__init__.py +0 -0
  144. {agent_notes-2.9.0 → agent_notes-2.10.0}/agent_notes/registries/_base.py +0 -0
  145. {agent_notes-2.9.0 → agent_notes-2.10.0}/agent_notes/registries/agent_registry.py +0 -0
  146. {agent_notes-2.9.0 → agent_notes-2.10.0}/agent_notes/registries/cli_registry.py +0 -0
  147. {agent_notes-2.9.0 → agent_notes-2.10.0}/agent_notes/registries/model_registry.py +0 -0
  148. {agent_notes-2.9.0 → agent_notes-2.10.0}/agent_notes/registries/role_registry.py +0 -0
  149. {agent_notes-2.9.0 → agent_notes-2.10.0}/agent_notes/registries/rule_registry.py +0 -0
  150. {agent_notes-2.9.0 → agent_notes-2.10.0}/agent_notes/registries/skill_registry.py +0 -0
  151. {agent_notes-2.9.0 → agent_notes-2.10.0}/agent_notes/scripts/__init__.py +0 -0
  152. {agent_notes-2.9.0 → agent_notes-2.10.0}/agent_notes/scripts/_claude_backend.py +0 -0
  153. {agent_notes-2.9.0 → agent_notes-2.10.0}/agent_notes/scripts/_formatting.py +0 -0
  154. {agent_notes-2.9.0 → agent_notes-2.10.0}/agent_notes/scripts/_opencode_backend.py +0 -0
  155. {agent_notes-2.9.0 → agent_notes-2.10.0}/agent_notes/scripts/_pricing.py +0 -0
  156. {agent_notes-2.9.0 → agent_notes-2.10.0}/agent_notes/scripts/cost_report.py +0 -0
  157. {agent_notes-2.9.0 → agent_notes-2.10.0}/agent_notes/services/__init__.py +0 -0
  158. {agent_notes-2.9.0 → agent_notes-2.10.0}/agent_notes/services/credentials.py +0 -0
  159. {agent_notes-2.9.0 → agent_notes-2.10.0}/agent_notes/services/diagnostics/__init__.py +0 -0
  160. {agent_notes-2.9.0 → agent_notes-2.10.0}/agent_notes/services/diagnostics/_checks.py +0 -0
  161. {agent_notes-2.9.0 → agent_notes-2.10.0}/agent_notes/services/diagnostics/_display.py +0 -0
  162. {agent_notes-2.9.0 → agent_notes-2.10.0}/agent_notes/services/diagnostics/_fix.py +0 -0
  163. {agent_notes-2.9.0 → agent_notes-2.10.0}/agent_notes/services/diff.py +0 -0
  164. {agent_notes-2.9.0 → agent_notes-2.10.0}/agent_notes/services/install_state_builder.py +0 -0
  165. {agent_notes-2.9.0 → agent_notes-2.10.0}/agent_notes/services/memory_backend.py +0 -0
  166. {agent_notes-2.9.0 → agent_notes-2.10.0}/agent_notes/services/rendering.py +0 -0
  167. {agent_notes-2.9.0 → agent_notes-2.10.0}/agent_notes/services/session_context.py +0 -0
  168. {agent_notes-2.9.0 → agent_notes-2.10.0}/agent_notes/services/settings_writer.py +0 -0
  169. {agent_notes-2.9.0 → agent_notes-2.10.0}/agent_notes/services/state_store.py +0 -0
  170. {agent_notes-2.9.0 → agent_notes-2.10.0}/agent_notes/services/ui.py +0 -0
  171. {agent_notes-2.9.0 → agent_notes-2.10.0}/agent_notes/services/user_config.py +0 -0
  172. {agent_notes-2.9.0 → agent_notes-2.10.0}/agent_notes/services/validation.py +0 -0
  173. {agent_notes-2.9.0 → agent_notes-2.10.0}/agent_notes/state.py +0 -0
  174. {agent_notes-2.9.0 → agent_notes-2.10.0}/agent_notes.egg-info/dependency_links.txt +0 -0
  175. {agent_notes-2.9.0 → agent_notes-2.10.0}/agent_notes.egg-info/entry_points.txt +0 -0
  176. {agent_notes-2.9.0 → agent_notes-2.10.0}/agent_notes.egg-info/requires.txt +0 -0
  177. {agent_notes-2.9.0 → agent_notes-2.10.0}/agent_notes.egg-info/top_level.txt +0 -0
  178. {agent_notes-2.9.0 → agent_notes-2.10.0}/pyproject.toml +0 -0
  179. {agent_notes-2.9.0 → agent_notes-2.10.0}/setup.cfg +0 -0
  180. {agent_notes-2.9.0 → agent_notes-2.10.0}/tests/conftest.py +0 -0
  181. {agent_notes-2.9.0 → agent_notes-2.10.0}/tests/functional/__init__.py +0 -0
  182. {agent_notes-2.9.0 → agent_notes-2.10.0}/tests/functional/commands/__init__.py +0 -0
  183. {agent_notes-2.9.0 → agent_notes-2.10.0}/tests/functional/commands/test_config_command.py +0 -0
  184. {agent_notes-2.9.0 → agent_notes-2.10.0}/tests/functional/commands/test_doctor_command.py +0 -0
  185. {agent_notes-2.9.0 → agent_notes-2.10.0}/tests/functional/commands/test_info_command.py +0 -0
  186. {agent_notes-2.9.0 → agent_notes-2.10.0}/tests/functional/commands/test_install_command.py +0 -0
  187. {agent_notes-2.9.0 → agent_notes-2.10.0}/tests/functional/commands/test_list_command.py +0 -0
  188. {agent_notes-2.9.0 → agent_notes-2.10.0}/tests/functional/commands/test_regenerate_command.py +0 -0
  189. {agent_notes-2.9.0 → agent_notes-2.10.0}/tests/functional/commands/test_uninstall_command.py +0 -0
  190. {agent_notes-2.9.0 → agent_notes-2.10.0}/tests/functional/commands/test_update_command.py +0 -0
  191. {agent_notes-2.9.0 → agent_notes-2.10.0}/tests/functional/commands/test_validate_command.py +0 -0
  192. {agent_notes-2.9.0 → agent_notes-2.10.0}/tests/functional/memory/__init__.py +0 -0
  193. {agent_notes-2.9.0 → agent_notes-2.10.0}/tests/functional/memory/test_memory_command.py +0 -0
  194. {agent_notes-2.9.0 → agent_notes-2.10.0}/tests/functional/scripts/__init__.py +0 -0
  195. {agent_notes-2.9.0 → agent_notes-2.10.0}/tests/functional/scripts/test_release_script.py +0 -0
  196. {agent_notes-2.9.0 → agent_notes-2.10.0}/tests/integration/__init__.py +0 -0
  197. {agent_notes-2.9.0 → agent_notes-2.10.0}/tests/integration/build_output/__init__.py +0 -0
  198. {agent_notes-2.9.0 → agent_notes-2.10.0}/tests/integration/build_output/test_build_output.py +0 -0
  199. {agent_notes-2.9.0 → agent_notes-2.10.0}/tests/integration/install/__init__.py +0 -0
  200. {agent_notes-2.9.0 → agent_notes-2.10.0}/tests/integration/install/test_install_methods.py +0 -0
  201. {agent_notes-2.9.0 → agent_notes-2.10.0}/tests/integration/plugin_builders/__init__.py +0 -0
  202. {agent_notes-2.9.0 → agent_notes-2.10.0}/tests/integration/plugin_builders/test_plugin_builders.py +0 -0
  203. {agent_notes-2.9.0 → agent_notes-2.10.0}/tests/plugins/__init__.py +0 -0
  204. {agent_notes-2.9.0 → agent_notes-2.10.0}/tests/plugins/claude/__init__.py +0 -0
  205. {agent_notes-2.9.0 → agent_notes-2.10.0}/tests/plugins/claude/test_agents.py +0 -0
  206. {agent_notes-2.9.0 → agent_notes-2.10.0}/tests/plugins/test_skills.py +0 -0
  207. {agent_notes-2.9.0 → agent_notes-2.10.0}/tests/unit/__init__.py +0 -0
  208. {agent_notes-2.9.0 → agent_notes-2.10.0}/tests/unit/commands/__init__.py +0 -0
  209. {agent_notes-2.9.0 → agent_notes-2.10.0}/tests/unit/commands/test_cost_report_subcommand.py +0 -0
  210. {agent_notes-2.9.0 → agent_notes-2.10.0}/tests/unit/commands/test_count_agents.py +0 -0
  211. {agent_notes-2.9.0 → agent_notes-2.10.0}/tests/unit/commands/test_wizard_orchestrator_skip.py +0 -0
  212. {agent_notes-2.9.0 → agent_notes-2.10.0}/tests/unit/registries/__init__.py +0 -0
  213. {agent_notes-2.9.0 → agent_notes-2.10.0}/tests/unit/registries/test_registries.py +0 -0
  214. {agent_notes-2.9.0 → agent_notes-2.10.0}/tests/unit/scripts/__init__.py +0 -0
  215. {agent_notes-2.9.0 → agent_notes-2.10.0}/tests/unit/scripts/test_cost_report.py +0 -0
  216. {agent_notes-2.9.0 → agent_notes-2.10.0}/tests/unit/scripts/test_cost_report_scoping.py +0 -0
  217. {agent_notes-2.9.0 → agent_notes-2.10.0}/tests/unit/scripts/test_formatting_tty.py +0 -0
  218. {agent_notes-2.9.0 → agent_notes-2.10.0}/tests/unit/scripts/test_time_aggregation.py +0 -0
  219. {agent_notes-2.9.0 → agent_notes-2.10.0}/tests/unit/services/__init__.py +0 -0
  220. {agent_notes-2.9.0 → agent_notes-2.10.0}/tests/unit/services/test_build_functions.py +0 -0
  221. {agent_notes-2.9.0 → agent_notes-2.10.0}/tests/unit/services/test_credentials.py +0 -0
  222. {agent_notes-2.9.0 → agent_notes-2.10.0}/tests/unit/services/test_memory_backend.py +0 -0
  223. {agent_notes-2.9.0 → agent_notes-2.10.0}/tests/unit/services/test_memory_backend_io.py +0 -0
  224. {agent_notes-2.9.0 → agent_notes-2.10.0}/tests/unit/services/test_rendering_includes.py +0 -0
  225. {agent_notes-2.9.0 → agent_notes-2.10.0}/tests/unit/services/test_settings_writer.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: agent-notes
3
- Version: 2.9.0
3
+ Version: 2.10.0
4
4
  Summary: AI agent configuration manager for Claude Code, OpenCode, and Copilot
5
5
  Author-email: Eugene Naumov <min.verkligheten@gmail.com>
6
6
  License-Expression: MIT
@@ -0,0 +1 @@
1
+ 2.10.0
@@ -458,9 +458,11 @@ def _select_memory(step: int, total: int, version: str = '') -> tuple:
458
458
 
459
459
 
460
460
  def _confirm_install(clis: Set[str], scope: str, copy_mode: bool, selected_skills: List[str], role_models: Dict[str, Dict[str, str]], version: str = '', memory_backend: str = 'local', memory_path: str = '') -> bool:
461
- """Step 7: Confirmation."""
461
+ """Step 7: Confirmation — shows pre-flight summary including files to be backed up."""
462
+ import logging
462
463
  from ..services.ui import _clear_screen, _render_step_header
463
464
  from ..registries.cli_registry import load_registry
465
+ from ..services.installer import plan_install
464
466
  _clear_screen()
465
467
  _render_step_header(7, 7, version)
466
468
  skill_groups = _get_skill_groups()
@@ -469,6 +471,33 @@ def _confirm_install(clis: Set[str], scope: str, copy_mode: bool, selected_skill
469
471
  _render_install_summary(clis, scope, copy_mode, selected_skills, role_models, skill_groups, registry,
470
472
  memory_backend=memory_backend, memory_path=memory_path)
471
473
 
474
+ # Pre-flight: show which existing files will be backed up
475
+ _tty = sys.stdout.isatty()
476
+ _YELLOW = "\033[0;33m" if _tty else ""
477
+ _DIM = "\033[2m" if _tty else ""
478
+ _NC = "\033[0m" if _tty else ""
479
+
480
+ try:
481
+ manifest = plan_install(
482
+ scope=scope,
483
+ registry=registry,
484
+ selected_clis=set(clis),
485
+ selected_skills=selected_skills if selected_skills else None,
486
+ copy_mode=copy_mode,
487
+ )
488
+ overwrites = [a for a in manifest if a.action == "overwrite"]
489
+ to_install = [a for a in manifest if a.action != "skip"]
490
+
491
+ print(f" {_DIM}Files to install:{_NC} {len(to_install)}")
492
+ if overwrites:
493
+ print(f" {_YELLOW}Files to back up ({len(overwrites)}):{_NC}")
494
+ for a in overwrites:
495
+ print(f" {_DIM}{a.dst}{_NC} → {a.backup_path}")
496
+ print("")
497
+ except Exception:
498
+ # Pre-flight is best-effort — log at debug and continue
499
+ logging.getLogger(__name__).debug("plan_install failed during pre-flight", exc_info=True)
500
+
472
501
  choice = _safe_input("Proceed? [Y/n]: ", "Y").lower()
473
502
  return choice != "n"
474
503
 
@@ -2,6 +2,7 @@
2
2
 
3
3
  import shutil
4
4
  import sys
5
+ from datetime import datetime, timezone
5
6
  from pathlib import Path
6
7
  from typing import Optional
7
8
 
@@ -68,35 +69,30 @@ def files_identical(a: Path, b: Path) -> bool:
68
69
  return False
69
70
 
70
71
 
72
+ def _timestamped_backup_path(dst: Path) -> Path:
73
+ """Return a timestamped backup path for dst, e.g. CLAUDE.md.bak.20260430T022500123456Z."""
74
+ ts = datetime.now(timezone.utc).strftime("%Y%m%dT%H%M%S%fZ")
75
+ return Path(str(dst) + f".bak.{ts}")
76
+
77
+
71
78
  def handle_existing(src: Path, dst: Path) -> bool:
72
79
  """Handle an existing non-symlink destination file.
73
-
74
- Returns True if install should proceed, False to skip.
80
+
81
+ Backs up the destination with a timestamped name and proceeds with install.
82
+ Returns True if install should proceed, False to skip (identical content).
75
83
  """
76
84
  if files_identical(src, dst):
77
85
  _skipped(str(dst), "exists, identical content")
78
86
  return False
79
-
80
- print(f"\n {_Color.YELLOW}CONFLICT{_Color.NC} {dst}")
81
- print(f" File exists and differs from source.")
82
- response = input(" (b)ackup and replace, (s)kip? [b/s] ").strip().lower()
83
-
84
- if response == 'b':
85
- backup_path = Path(str(dst) + ".bak")
86
- if dst.is_dir():
87
- if backup_path.exists():
88
- shutil.rmtree(backup_path)
89
- shutil.copytree(dst, backup_path)
90
- shutil.rmtree(dst)
91
- else:
92
- if backup_path.exists():
93
- backup_path.unlink()
94
- dst.rename(backup_path)
95
- print(f" {_Color.CYAN}BACKUP{_Color.NC} {backup_path}")
96
- return True
87
+
88
+ backup_path = _timestamped_backup_path(dst)
89
+ if dst.is_dir():
90
+ shutil.copytree(dst, backup_path)
91
+ shutil.rmtree(dst)
97
92
  else:
98
- _skipped(str(dst), "user skipped")
99
- return False
93
+ dst.rename(backup_path)
94
+ print(f" {_Color.CYAN}BACKUP{_Color.NC} {backup_path}")
95
+ return True
100
96
 
101
97
 
102
98
  def place_file(src: Path, dst: Path, copy_mode: bool = False) -> None:
@@ -3,7 +3,7 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  from pathlib import Path
6
- from typing import Optional
6
+ from typing import List, NamedTuple, Optional
7
7
 
8
8
  from ..domain.cli_backend import CLIBackend
9
9
  from ..registries.cli_registry import CLIRegistry, load_registry
@@ -11,9 +11,19 @@ from .. import config
11
11
  from .fs import (
12
12
  place_file, place_dir_contents,
13
13
  remove_symlink, remove_all_symlinks_in_dir, remove_dir_if_empty,
14
+ files_identical, _timestamped_backup_path,
14
15
  )
15
16
  from .state_store import load_state, get_scope
16
17
 
18
+
19
+ class InstallAction(NamedTuple):
20
+ """Describes a single file placement that plan_install would perform."""
21
+
22
+ action: str # "install", "overwrite", or "skip"
23
+ src: Path
24
+ dst: Path
25
+ backup_path: Optional[Path] # set when action == "overwrite"
26
+
17
27
  # Re-import the atomic helpers from install (they stay in install.py):
18
28
  # We intentionally avoid circular import by lazy-importing inside functions.
19
29
 
@@ -144,6 +154,151 @@ def uninstall_component_for_backend(
144
154
  remove_dir_if_empty(dst)
145
155
 
146
156
 
157
+ def _plan_file(src: Path, dst: Path, copy_mode: bool = False) -> InstallAction:
158
+ """Return the InstallAction for a single src→dst placement."""
159
+ if copy_mode and dst.is_symlink() and dst.resolve() == src.resolve():
160
+ return InstallAction(action="skip", src=src, dst=dst, backup_path=None)
161
+ if dst.exists() and not dst.is_symlink():
162
+ if files_identical(src, dst):
163
+ return InstallAction(action="skip", src=src, dst=dst, backup_path=None)
164
+ backup_path = _timestamped_backup_path(dst)
165
+ return InstallAction(action="overwrite", src=src, dst=dst, backup_path=backup_path)
166
+ return InstallAction(action="install", src=src, dst=dst, backup_path=None)
167
+
168
+
169
+ def _plan_component(
170
+ backend: CLIBackend,
171
+ component: str,
172
+ scope: str,
173
+ copy_mode: bool = False,
174
+ ) -> List[InstallAction]:
175
+ """Return InstallActions for one (backend, component, scope) without writing."""
176
+ src = dist_source_for(backend, component)
177
+ if src is None:
178
+ return []
179
+ dst = target_dir_for(backend, component, scope)
180
+ if dst is None:
181
+ return []
182
+
183
+ actions: List[InstallAction] = []
184
+
185
+ if component == "config":
186
+ filename = config_filename_for(backend)
187
+ if not filename:
188
+ return []
189
+ src_file = src / filename
190
+ if not src_file.exists():
191
+ return []
192
+ actions.append(_plan_file(src_file, dst / filename, copy_mode))
193
+ elif component in ("agents", "rules", "commands"):
194
+ for src_file in sorted(src.glob("*.md")):
195
+ if src_file.exists():
196
+ actions.append(_plan_file(src_file, dst / src_file.name, copy_mode))
197
+ elif component == "skills":
198
+ if not src.exists():
199
+ return []
200
+ for skill_dir in sorted(d for d in src.iterdir() if d.is_dir()):
201
+ actions.append(_plan_file(skill_dir, dst / skill_dir.name, copy_mode))
202
+
203
+ return actions
204
+
205
+
206
+ def _plan_session_hook(
207
+ backend,
208
+ scope: str,
209
+ ) -> List[InstallAction]:
210
+ """Return InstallActions for the settings.json SessionStart hook write.
211
+
212
+ The hook is injected via merge (never a full overwrite), so:
213
+ - If settings.json does not exist: action="install"
214
+ - If settings.json exists but hook is absent: action="modify"
215
+ - If settings.json exists and hook is already present: action="skip"
216
+ """
217
+ from .settings_writer import has_hook
218
+
219
+ settings_path, _context_file, hook_command = _session_hook_paths(backend, scope)
220
+
221
+ if not settings_path.exists():
222
+ # Fresh write — settings.json will be created
223
+ return [InstallAction(action="install", src=settings_path, dst=settings_path, backup_path=None)]
224
+
225
+ if has_hook(settings_path, "SessionStart", hook_command):
226
+ # Already installed — no-op
227
+ return [InstallAction(action="skip", src=settings_path, dst=settings_path, backup_path=None)]
228
+
229
+ # Merge inject — file exists but hook is absent; classified as modify
230
+ return [InstallAction(action="modify", src=settings_path, dst=settings_path, backup_path=None)]
231
+
232
+
233
+ def plan_install(
234
+ scope: str,
235
+ registry: Optional[CLIRegistry] = None,
236
+ selected_clis: Optional[set] = None,
237
+ selected_skills: Optional[List[str]] = None,
238
+ copy_mode: bool = False,
239
+ ) -> List[InstallAction]:
240
+ """Return a manifest of what install_all would do, without writing any files.
241
+
242
+ Each entry is an InstallAction(action, src, dst, backup_path) where:
243
+ action == "install" — dst does not yet exist (fresh placement)
244
+ action == "modify" — dst exists and will be merge-updated (e.g. settings.json hook)
245
+ action == "overwrite" — dst exists and differs; backup_path is the timestamped path
246
+ action == "skip" — dst exists and is byte-identical (or symlink unchanged); no write needed
247
+ """
248
+ if registry is None:
249
+ registry = load_registry()
250
+
251
+ actions: List[InstallAction] = []
252
+
253
+ for backend in registry.all():
254
+ if selected_clis is not None and backend.name not in selected_clis:
255
+ continue
256
+ for component in COMPONENT_TYPES:
257
+ if component == "skills" and selected_skills is not None:
258
+ # Skills are filtered — plan them separately below
259
+ continue
260
+ actions.extend(_plan_component(backend, component, scope, copy_mode))
261
+
262
+ # Skills: respect the selected_skills filter (mirrors wizard's install_skills_filtered)
263
+ if scope == "global" or selected_skills is not None:
264
+ dist_skills_dir = config.DIST_SKILLS_DIR
265
+ if dist_skills_dir.exists():
266
+ skill_dirs = {d.name: d for d in dist_skills_dir.iterdir() if d.is_dir()}
267
+ names_to_plan = selected_skills if selected_skills is not None else list(skill_dirs.keys())
268
+
269
+ # Per-backend skill targets
270
+ for backend in registry.all():
271
+ if selected_clis is not None and backend.name not in selected_clis:
272
+ continue
273
+ if not backend.supports("skills"):
274
+ continue
275
+ dst_dir = target_dir_for(backend, "skills", scope)
276
+ if dst_dir is None:
277
+ continue
278
+ for name in sorted(names_to_plan):
279
+ skill_dir = skill_dirs.get(name)
280
+ if skill_dir:
281
+ actions.append(_plan_file(skill_dir, dst_dir / name, copy_mode))
282
+
283
+ # Universal skills mirror (~/.agents/skills/)
284
+ if scope == "global":
285
+ target = config.AGENTS_HOME / "skills"
286
+ for name in sorted(names_to_plan):
287
+ skill_dir = skill_dirs.get(name)
288
+ if skill_dir:
289
+ actions.append(_plan_file(skill_dir, target / name, copy_mode))
290
+
291
+ # SessionStart hook for Claude Code — always planned when claude backend is selected
292
+ try:
293
+ claude_backend = registry.get("claude")
294
+ if selected_clis is None or claude_backend.name in selected_clis:
295
+ actions.extend(_plan_session_hook(claude_backend, scope))
296
+ except KeyError:
297
+ pass
298
+
299
+ return actions
300
+
301
+
147
302
  def install_all(scope: str, copy_mode: bool, registry: Optional[CLIRegistry] = None) -> None:
148
303
  """Top-level: install every (backend, component) combo."""
149
304
  if registry is None:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: agent-notes
3
- Version: 2.9.0
3
+ Version: 2.10.0
4
4
  Summary: AI agent configuration manager for Claude Code, OpenCode, and Copilot
5
5
  Author-email: Eugene Naumov <min.verkligheten@gmail.com>
6
6
  License-Expression: MIT
@@ -203,6 +203,7 @@ tests/unit/commands/__init__.py
203
203
  tests/unit/commands/test_cost_report_subcommand.py
204
204
  tests/unit/commands/test_count_agents.py
205
205
  tests/unit/commands/test_wizard_orchestrator_skip.py
206
+ tests/unit/commands/test_wizard_preflight.py
206
207
  tests/unit/registries/__init__.py
207
208
  tests/unit/registries/test_registries.py
208
209
  tests/unit/scripts/__init__.py
@@ -213,6 +214,8 @@ tests/unit/scripts/test_time_aggregation.py
213
214
  tests/unit/services/__init__.py
214
215
  tests/unit/services/test_build_functions.py
215
216
  tests/unit/services/test_credentials.py
217
+ tests/unit/services/test_fs.py
218
+ tests/unit/services/test_installer_plan.py
216
219
  tests/unit/services/test_memory_backend.py
217
220
  tests/unit/services/test_memory_backend_io.py
218
221
  tests/unit/services/test_rendering_includes.py
@@ -0,0 +1,232 @@
1
+ """Tests for _confirm_install pre-flight summary in agent_notes.commands.wizard."""
2
+ import io
3
+ import logging
4
+ import sys
5
+ import pytest
6
+ from pathlib import Path
7
+ from unittest.mock import patch, MagicMock
8
+
9
+ from agent_notes.services.installer import InstallAction
10
+
11
+
12
+ # ---------------------------------------------------------------------------
13
+ # Helpers
14
+ # ---------------------------------------------------------------------------
15
+
16
+ def _make_install_action(action: str, dst_name: str = "CLAUDE.md", backup: Path = None) -> InstallAction:
17
+ src = Path("/fake/src") / dst_name
18
+ dst = Path("/fake/dst") / dst_name
19
+ return InstallAction(action=action, src=src, dst=dst, backup_path=backup)
20
+
21
+
22
+ def _run_confirm_install(monkeypatch, manifest, user_input: str = "Y"):
23
+ """Call _confirm_install with all heavy side-effects mocked out.
24
+
25
+ local imports inside _confirm_install:
26
+ from ..services.ui import _clear_screen, _render_step_header
27
+ from ..registries.cli_registry import load_registry
28
+ from ..services.installer import plan_install
29
+
30
+ We patch the source modules so the local imports pick up the stubs.
31
+ """
32
+ # Suppress screen-clearing and step headers (patched at source)
33
+ monkeypatch.setattr("agent_notes.services.ui._clear_screen", lambda: None)
34
+ monkeypatch.setattr("agent_notes.services.ui._render_step_header", lambda *a, **kw: None)
35
+
36
+ # plan_install is imported from services.installer inside the function
37
+ monkeypatch.setattr("agent_notes.services.installer.plan_install", lambda **kw: manifest)
38
+
39
+ # load_registry is imported from registries.cli_registry inside the function
40
+ monkeypatch.setattr(
41
+ "agent_notes.registries.cli_registry.load_registry",
42
+ lambda *a, **kw: MagicMock(),
43
+ )
44
+
45
+ # Stub out module-level helpers in wizard
46
+ monkeypatch.setattr("agent_notes.commands.wizard._render_install_summary", lambda *a, **kw: None)
47
+ monkeypatch.setattr("agent_notes.commands.wizard._get_skill_groups", lambda: {})
48
+
49
+ # Drive _safe_input (module-level import in wizard)
50
+ monkeypatch.setattr("agent_notes.commands.wizard._safe_input", lambda prompt, default: user_input)
51
+
52
+ # Redirect stdout to capture prints
53
+ buf = io.StringIO()
54
+ with patch("sys.stdout", buf):
55
+ from agent_notes.commands.wizard import _confirm_install
56
+ result = _confirm_install(
57
+ clis={"claude"},
58
+ scope="local",
59
+ copy_mode=False,
60
+ selected_skills=[],
61
+ role_models={},
62
+ )
63
+
64
+ return result, buf.getvalue()
65
+
66
+
67
+ # ---------------------------------------------------------------------------
68
+ # Tests
69
+ # ---------------------------------------------------------------------------
70
+
71
+ class TestConfirmInstallFileCount:
72
+ def test_shows_files_to_install_count(self, monkeypatch):
73
+ """The 'Files to install: N' line must show the correct non-skip count."""
74
+ manifest = [
75
+ _make_install_action("install", "a.md"),
76
+ _make_install_action("install", "b.md"),
77
+ _make_install_action("install", "c.md"),
78
+ _make_install_action("skip", "d.md"), # not counted
79
+ ]
80
+ _result, output = _run_confirm_install(monkeypatch, manifest, user_input="Y")
81
+
82
+ assert "Files to install:" in output
83
+ # The count of non-skip actions is 3
84
+ assert "3" in output
85
+
86
+ def test_skip_actions_excluded_from_count(self, monkeypatch):
87
+ """All-skip manifest reports 0 files to install."""
88
+ manifest = [_make_install_action("skip", f"{i}.md") for i in range(5)]
89
+ _result, output = _run_confirm_install(monkeypatch, manifest, user_input="Y")
90
+
91
+ assert "Files to install:" in output
92
+ assert "0" in output
93
+
94
+
95
+ class TestConfirmInstallBackupLines:
96
+ def test_lists_each_backup_path_when_overwrites_present(self, monkeypatch, tmp_path):
97
+ """Each overwrite action with a backup_path must appear in its own output line."""
98
+ bak1 = tmp_path / "CLAUDE.md.bak.20260430T022500000001Z"
99
+ bak2 = tmp_path / "settings.json.bak.20260430T022500000002Z"
100
+ manifest = [
101
+ _make_install_action("install", "agents/role.md"),
102
+ InstallAction(
103
+ action="overwrite",
104
+ src=Path("/fake/src/CLAUDE.md"),
105
+ dst=tmp_path / "CLAUDE.md",
106
+ backup_path=bak1,
107
+ ),
108
+ InstallAction(
109
+ action="overwrite",
110
+ src=Path("/fake/src/settings.json"),
111
+ dst=tmp_path / "settings.json",
112
+ backup_path=bak2,
113
+ ),
114
+ ]
115
+ _result, output = _run_confirm_install(monkeypatch, manifest, user_input="Y")
116
+
117
+ assert str(bak1) in output, f"Expected backup path {bak1} in output:\n{output}"
118
+ assert str(bak2) in output, f"Expected backup path {bak2} in output:\n{output}"
119
+
120
+ def test_backup_section_header_present_when_overwrites_exist(self, monkeypatch, tmp_path):
121
+ bak = tmp_path / "CLAUDE.md.bak.20260430T022500000001Z"
122
+ manifest = [
123
+ InstallAction(
124
+ action="overwrite",
125
+ src=Path("/fake/src/CLAUDE.md"),
126
+ dst=tmp_path / "CLAUDE.md",
127
+ backup_path=bak,
128
+ ),
129
+ ]
130
+ _result, output = _run_confirm_install(monkeypatch, manifest, user_input="Y")
131
+
132
+ assert "back up" in output.lower() or "backup" in output.lower(), (
133
+ f"Expected a backup section header in output:\n{output}"
134
+ )
135
+
136
+ def test_no_backup_section_when_no_overwrites(self, monkeypatch):
137
+ """If no overwrites, the backup listing block must not appear."""
138
+ manifest = [
139
+ _make_install_action("install", "a.md"),
140
+ _make_install_action("skip", "b.md"),
141
+ ]
142
+ _result, output = _run_confirm_install(monkeypatch, manifest, user_input="Y")
143
+
144
+ # "back up" section only appears when there are overwrites
145
+ assert "→" not in output, (
146
+ "No backup arrow (→) should appear when there are no overwrites"
147
+ )
148
+
149
+
150
+ class TestConfirmInstallPlanInstallException:
151
+ def test_plan_install_exception_does_not_raise_to_user(self, monkeypatch):
152
+ """When plan_install raises, _confirm_install must not propagate the exception."""
153
+ monkeypatch.setattr("agent_notes.services.ui._clear_screen", lambda: None)
154
+ monkeypatch.setattr("agent_notes.services.ui._render_step_header", lambda *a, **kw: None)
155
+ monkeypatch.setattr("agent_notes.commands.wizard._render_install_summary", lambda *a, **kw: None)
156
+ monkeypatch.setattr("agent_notes.commands.wizard._get_skill_groups", lambda: {})
157
+ monkeypatch.setattr(
158
+ "agent_notes.registries.cli_registry.load_registry",
159
+ lambda *a, **kw: MagicMock(),
160
+ )
161
+ monkeypatch.setattr("agent_notes.commands.wizard._safe_input", lambda prompt, default: "Y")
162
+
163
+ def exploding_plan_install(**kw):
164
+ raise RuntimeError("simulated plan_install failure")
165
+
166
+ monkeypatch.setattr("agent_notes.services.installer.plan_install", exploding_plan_install)
167
+
168
+ # Should not raise
169
+ from agent_notes.commands.wizard import _confirm_install
170
+ result = _confirm_install(
171
+ clis={"claude"},
172
+ scope="local",
173
+ copy_mode=False,
174
+ selected_skills=[],
175
+ role_models={},
176
+ )
177
+ # User answered Y, so result should be True (proceed)
178
+ assert result is True
179
+
180
+ def test_plan_install_exception_emits_debug_log(self, monkeypatch, caplog):
181
+ """When plan_install raises, a debug-level log must be emitted."""
182
+ monkeypatch.setattr("agent_notes.services.ui._clear_screen", lambda: None)
183
+ monkeypatch.setattr("agent_notes.services.ui._render_step_header", lambda *a, **kw: None)
184
+ monkeypatch.setattr("agent_notes.commands.wizard._render_install_summary", lambda *a, **kw: None)
185
+ monkeypatch.setattr("agent_notes.commands.wizard._get_skill_groups", lambda: {})
186
+ monkeypatch.setattr(
187
+ "agent_notes.registries.cli_registry.load_registry",
188
+ lambda *a, **kw: MagicMock(),
189
+ )
190
+ monkeypatch.setattr("agent_notes.commands.wizard._safe_input", lambda prompt, default: "Y")
191
+
192
+ def exploding_plan_install(**kw):
193
+ raise ValueError("boom")
194
+
195
+ monkeypatch.setattr("agent_notes.services.installer.plan_install", exploding_plan_install)
196
+
197
+ with caplog.at_level(logging.DEBUG, logger="agent_notes.commands.wizard"):
198
+ from agent_notes.commands.wizard import _confirm_install
199
+ _confirm_install(
200
+ clis={"claude"},
201
+ scope="local",
202
+ copy_mode=False,
203
+ selected_skills=[],
204
+ role_models={},
205
+ )
206
+
207
+ assert any(
208
+ "plan_install" in r.message.lower() or "pre-flight" in r.message.lower()
209
+ for r in caplog.records
210
+ ), f"Expected a debug log mentioning plan_install/pre-flight, got: {caplog.records}"
211
+
212
+
213
+ class TestConfirmInstallAbort:
214
+ def test_returns_false_when_user_answers_n(self, monkeypatch):
215
+ """When user answers 'n', _confirm_install must return False."""
216
+ manifest = [_make_install_action("install", "a.md")]
217
+ result, _output = _run_confirm_install(monkeypatch, manifest, user_input="n")
218
+ assert result is False
219
+
220
+ def test_returns_true_when_user_answers_y(self, monkeypatch):
221
+ manifest = [_make_install_action("install", "a.md")]
222
+ result, _output = _run_confirm_install(monkeypatch, manifest, user_input="Y")
223
+ assert result is True
224
+
225
+ def test_no_install_called_when_user_aborts(self, monkeypatch):
226
+ """Confirming 'n' must not trigger any install side-effects.
227
+ _confirm_install only returns a bool — actual install is the caller's job.
228
+ We verify the return value is False (caller must check it).
229
+ """
230
+ manifest = [_make_install_action("install", "a.md")]
231
+ result, _output = _run_confirm_install(monkeypatch, manifest, user_input="n")
232
+ assert result is False, "Return value False signals caller should not proceed"