cafe-engine 0.1.2__tar.gz → 0.1.4__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 (210) hide show
  1. {cafe_engine-0.1.2 → cafe_engine-0.1.4}/CHANGELOG.md +11 -0
  2. {cafe_engine-0.1.2 → cafe_engine-0.1.4}/PKG-INFO +28 -2
  3. {cafe_engine-0.1.2 → cafe_engine-0.1.4}/README.md +27 -1
  4. {cafe_engine-0.1.2 → cafe_engine-0.1.4}/pyproject.toml +1 -1
  5. {cafe_engine-0.1.2 → cafe_engine-0.1.4}/src/cafe/agents/cli/copilot.py +17 -13
  6. {cafe_engine-0.1.2 → cafe_engine-0.1.4}/src/cafe/agents/executor.py +16 -1
  7. {cafe_engine-0.1.2 → cafe_engine-0.1.4}/src/cafe/agents/manager.py +123 -3
  8. {cafe_engine-0.1.2 → cafe_engine-0.1.4}/src/cafe/core/phase.py +75 -2
  9. {cafe_engine-0.1.2 → cafe_engine-0.1.4}/src/cafe/core/status_codes.py +15 -2
  10. {cafe_engine-0.1.2 → cafe_engine-0.1.4}/src/cafe/core/types.py +25 -1
  11. {cafe_engine-0.1.2 → cafe_engine-0.1.4}/src/cafe/phases/develop_phase.py +156 -88
  12. {cafe_engine-0.1.2 → cafe_engine-0.1.4}/src/cafe/phases/plan_phase.py +50 -3
  13. {cafe_engine-0.1.2 → cafe_engine-0.1.4}/src/cafe/phases/pr_phase.py +134 -32
  14. {cafe_engine-0.1.2 → cafe_engine-0.1.4}/src/cafe/phases/spec_phase.py +0 -62
  15. {cafe_engine-0.1.2 → cafe_engine-0.1.4}/src/cafe/ui/cli.py +187 -10
  16. {cafe_engine-0.1.2 → cafe_engine-0.1.4}/src/cafe/utils/checklist_generator.py +22 -1
  17. {cafe_engine-0.1.2 → cafe_engine-0.1.4}/src/cafe/utils/checklist_templates.py +8 -5
  18. {cafe_engine-0.1.2 → cafe_engine-0.1.4}/src/cafe/utils/config.py +50 -0
  19. cafe_engine-0.1.4/tests/unit/test_backup_agent.py +455 -0
  20. {cafe_engine-0.1.2 → cafe_engine-0.1.4}/tests/unit/test_cli.py +188 -0
  21. {cafe_engine-0.1.2 → cafe_engine-0.1.4}/tests/unit/test_cli_pr_command.py +87 -1
  22. {cafe_engine-0.1.2 → cafe_engine-0.1.4}/tests/unit/test_cli_prepare.py +186 -0
  23. {cafe_engine-0.1.2 → cafe_engine-0.1.4}/tests/unit/test_copilot_cli.py +43 -0
  24. cafe_engine-0.1.4/tests/unit/test_develop_no_changes_needed.py +420 -0
  25. {cafe_engine-0.1.2 → cafe_engine-0.1.4}/tests/unit/test_develop_phase_prompt.py +105 -6
  26. cafe_engine-0.1.4/tests/unit/test_plan_checklist_xml.py +80 -0
  27. cafe_engine-0.1.4/tests/unit/test_plan_phase_execute_xml.py +93 -0
  28. cafe_engine-0.1.4/tests/unit/test_plan_phase_qa.py +227 -0
  29. {cafe_engine-0.1.2 → cafe_engine-0.1.4}/tests/unit/test_pr_phase_iteration_logic.py +243 -0
  30. cafe_engine-0.1.4/tests/unit/test_pr_phase_output_todo_only.py +194 -0
  31. cafe_engine-0.1.4/tests/unit/test_pr_phase_post_todo_comment.py +317 -0
  32. {cafe_engine-0.1.2 → cafe_engine-0.1.4}/tests/unit/test_status_codes.py +7 -7
  33. {cafe_engine-0.1.2 → cafe_engine-0.1.4}/uv.lock +1 -1
  34. cafe_engine-0.1.2/tests/unit/test_develop_no_changes_needed.py +0 -205
  35. {cafe_engine-0.1.2 → cafe_engine-0.1.4}/.claude/settings.local.json +0 -0
  36. {cafe_engine-0.1.2 → cafe_engine-0.1.4}/.geminiignore +0 -0
  37. {cafe_engine-0.1.2 → cafe_engine-0.1.4}/.githooks/pre-commit +0 -0
  38. {cafe_engine-0.1.2 → cafe_engine-0.1.4}/.gitignore +0 -0
  39. {cafe_engine-0.1.2 → cafe_engine-0.1.4}/CONTRIBUTING.md +0 -0
  40. {cafe_engine-0.1.2 → cafe_engine-0.1.4}/LICENSE +0 -0
  41. {cafe_engine-0.1.2 → cafe_engine-0.1.4}/images/cafe-flow.png +0 -0
  42. {cafe_engine-0.1.2 → cafe_engine-0.1.4}/scripts/simulate_agent_test.sh +0 -0
  43. {cafe_engine-0.1.2 → cafe_engine-0.1.4}/setup-hooks.sh +0 -0
  44. {cafe_engine-0.1.2 → cafe_engine-0.1.4}/src/cafe/__init__.py +0 -0
  45. {cafe_engine-0.1.2 → cafe_engine-0.1.4}/src/cafe/agents/__init__.py +0 -0
  46. {cafe_engine-0.1.2 → cafe_engine-0.1.4}/src/cafe/agents/cli/__init__.py +0 -0
  47. {cafe_engine-0.1.2 → cafe_engine-0.1.4}/src/cafe/agents/cli/abstract.py +0 -0
  48. {cafe_engine-0.1.2 → cafe_engine-0.1.4}/src/cafe/agents/cli/claude.py +0 -0
  49. {cafe_engine-0.1.2 → cafe_engine-0.1.4}/src/cafe/agents/cli/cursor.py +0 -0
  50. {cafe_engine-0.1.2 → cafe_engine-0.1.4}/src/cafe/agents/cli/gemini.py +0 -0
  51. {cafe_engine-0.1.2 → cafe_engine-0.1.4}/src/cafe/agents/mock_executor.py +0 -0
  52. {cafe_engine-0.1.2 → cafe_engine-0.1.4}/src/cafe/core/__init__.py +0 -0
  53. {cafe_engine-0.1.2 → cafe_engine-0.1.4}/src/cafe/core/git.py +0 -0
  54. {cafe_engine-0.1.2 → cafe_engine-0.1.4}/src/cafe/core/permission.py +0 -0
  55. {cafe_engine-0.1.2 → cafe_engine-0.1.4}/src/cafe/core/phase_cache.py +0 -0
  56. {cafe_engine-0.1.2 → cafe_engine-0.1.4}/src/cafe/core/questions_schema.py +0 -0
  57. {cafe_engine-0.1.2 → cafe_engine-0.1.4}/src/cafe/core/session.py +0 -0
  58. {cafe_engine-0.1.2 → cafe_engine-0.1.4}/src/cafe/data/agents/developer/David.md +0 -0
  59. {cafe_engine-0.1.2 → cafe_engine-0.1.4}/src/cafe/data/agents/developer/Nick.md +0 -0
  60. {cafe_engine-0.1.2 → cafe_engine-0.1.4}/src/cafe/data/agents/developer//347/276/205/345/215/232/351/253/230.md" +0 -0
  61. {cafe_engine-0.1.2 → cafe_engine-0.1.4}/src/cafe/data/agents/developer//351/255/257/346/237/224/345/207/241.md" +0 -0
  62. {cafe_engine-0.1.2 → cafe_engine-0.1.4}/src/cafe/data/agents/pm/Roger.md +0 -0
  63. {cafe_engine-0.1.2 → cafe_engine-0.1.4}/src/cafe/data/agents/pm//350/214/203/346/233/231/347/207/201.md" +0 -0
  64. {cafe_engine-0.1.2 → cafe_engine-0.1.4}/src/cafe/data/agents/reviewer/Richard.md +0 -0
  65. {cafe_engine-0.1.2 → cafe_engine-0.1.4}/src/cafe/data/agents/reviewer//346/236/227/350/220/214/350/212/235.md" +0 -0
  66. {cafe_engine-0.1.2 → cafe_engine-0.1.4}/src/cafe/data/templates/plan/bug.md +0 -0
  67. {cafe_engine-0.1.2 → cafe_engine-0.1.4}/src/cafe/data/templates/plan/default.md +0 -0
  68. {cafe_engine-0.1.2 → cafe_engine-0.1.4}/src/cafe/data/templates/plan/simple.md +0 -0
  69. {cafe_engine-0.1.2 → cafe_engine-0.1.4}/src/cafe/data/templates/spec/default.md +0 -0
  70. {cafe_engine-0.1.2 → cafe_engine-0.1.4}/src/cafe/data/templates/spec/detailed.md +0 -0
  71. {cafe_engine-0.1.2 → cafe_engine-0.1.4}/src/cafe/data/templates/spec/simple.md +0 -0
  72. {cafe_engine-0.1.2 → cafe_engine-0.1.4}/src/cafe/phases/__init__.py +0 -0
  73. {cafe_engine-0.1.2 → cafe_engine-0.1.4}/src/cafe/phases/review_phase.py +0 -0
  74. {cafe_engine-0.1.2 → cafe_engine-0.1.4}/src/cafe/services/__init__.py +0 -0
  75. {cafe_engine-0.1.2 → cafe_engine-0.1.4}/src/cafe/services/delta_display.py +0 -0
  76. {cafe_engine-0.1.2 → cafe_engine-0.1.4}/src/cafe/services/summary_display.py +0 -0
  77. {cafe_engine-0.1.2 → cafe_engine-0.1.4}/src/cafe/services/summary_service.py +0 -0
  78. {cafe_engine-0.1.2 → cafe_engine-0.1.4}/src/cafe/services/time_formatter.py +0 -0
  79. {cafe_engine-0.1.2 → cafe_engine-0.1.4}/src/cafe/services/timeline_builder.py +0 -0
  80. {cafe_engine-0.1.2 → cafe_engine-0.1.4}/src/cafe/templates/__init__.py +0 -0
  81. {cafe_engine-0.1.2 → cafe_engine-0.1.4}/src/cafe/templates/manager.py +0 -0
  82. {cafe_engine-0.1.2 → cafe_engine-0.1.4}/src/cafe/ui/__init__.py +0 -0
  83. {cafe_engine-0.1.2 → cafe_engine-0.1.4}/src/cafe/ui/display.py +0 -0
  84. {cafe_engine-0.1.2 → cafe_engine-0.1.4}/src/cafe/ui/init_helpers.py +0 -0
  85. {cafe_engine-0.1.2 → cafe_engine-0.1.4}/src/cafe/ui/inquirer_prompts.py +0 -0
  86. {cafe_engine-0.1.2 → cafe_engine-0.1.4}/src/cafe/ui/interactive_qa.py +0 -0
  87. {cafe_engine-0.1.2 → cafe_engine-0.1.4}/src/cafe/ui/phase_prompts.py +0 -0
  88. {cafe_engine-0.1.2 → cafe_engine-0.1.4}/src/cafe/ui/template_selector.py +0 -0
  89. {cafe_engine-0.1.2 → cafe_engine-0.1.4}/src/cafe/utils/__init__.py +0 -0
  90. {cafe_engine-0.1.2 → cafe_engine-0.1.4}/src/cafe/utils/checklist_utils.py +0 -0
  91. {cafe_engine-0.1.2 → cafe_engine-0.1.4}/src/cafe/utils/checklist_validator.py +0 -0
  92. {cafe_engine-0.1.2 → cafe_engine-0.1.4}/src/cafe/utils/git_utils.py +0 -0
  93. {cafe_engine-0.1.2 → cafe_engine-0.1.4}/src/cafe/utils/github.py +0 -0
  94. {cafe_engine-0.1.2 → cafe_engine-0.1.4}/src/cafe/utils/prompt_utils.py +0 -0
  95. {cafe_engine-0.1.2 → cafe_engine-0.1.4}/tests/.gitignore +0 -0
  96. {cafe_engine-0.1.2 → cafe_engine-0.1.4}/tests/__init__.py +0 -0
  97. {cafe_engine-0.1.2 → cafe_engine-0.1.4}/tests/conftest.py +0 -0
  98. {cafe_engine-0.1.2 → cafe_engine-0.1.4}/tests/fixtures/minimal_config.yaml +0 -0
  99. {cafe_engine-0.1.2 → cafe_engine-0.1.4}/tests/integration/__init__.py +0 -0
  100. {cafe_engine-0.1.2 → cafe_engine-0.1.4}/tests/integration/conftest.py +0 -0
  101. {cafe_engine-0.1.2 → cafe_engine-0.1.4}/tests/integration/test_gemini_session_integration.py +0 -0
  102. {cafe_engine-0.1.2 → cafe_engine-0.1.4}/tests/integration/test_global_config_integration.py +0 -0
  103. {cafe_engine-0.1.2 → cafe_engine-0.1.4}/tests/integration/test_pr_command_non_interactive.py +0 -0
  104. {cafe_engine-0.1.2 → cafe_engine-0.1.4}/tests/integration/test_pr_e2e_with_mock.py +0 -0
  105. {cafe_engine-0.1.2 → cafe_engine-0.1.4}/tests/integration/test_prepare_non_github.py +0 -0
  106. {cafe_engine-0.1.2 → cafe_engine-0.1.4}/tests/integration/test_spec_interactive_qa_e2e.py +0 -0
  107. {cafe_engine-0.1.2 → cafe_engine-0.1.4}/tests/unit/__init__.py +0 -0
  108. {cafe_engine-0.1.2 → cafe_engine-0.1.4}/tests/unit/test_abstract_cli.py +0 -0
  109. {cafe_engine-0.1.2 → cafe_engine-0.1.4}/tests/unit/test_agent_edit_auto_sync.py +0 -0
  110. {cafe_engine-0.1.2 → cafe_engine-0.1.4}/tests/unit/test_agent_executor.py +0 -0
  111. {cafe_engine-0.1.2 → cafe_engine-0.1.4}/tests/unit/test_agent_executor_path_formats.py +0 -0
  112. {cafe_engine-0.1.2 → cafe_engine-0.1.4}/tests/unit/test_agent_loading.py +0 -0
  113. {cafe_engine-0.1.2 → cafe_engine-0.1.4}/tests/unit/test_agent_manager.py +0 -0
  114. {cafe_engine-0.1.2 → cafe_engine-0.1.4}/tests/unit/test_agent_manager_mock_mode.py +0 -0
  115. {cafe_engine-0.1.2 → cafe_engine-0.1.4}/tests/unit/test_check_precommit_env.py +0 -0
  116. {cafe_engine-0.1.2 → cafe_engine-0.1.4}/tests/unit/test_checklist_generator.py +0 -0
  117. {cafe_engine-0.1.2 → cafe_engine-0.1.4}/tests/unit/test_checklist_utils.py +0 -0
  118. {cafe_engine-0.1.2 → cafe_engine-0.1.4}/tests/unit/test_claude_cli.py +0 -0
  119. {cafe_engine-0.1.2 → cafe_engine-0.1.4}/tests/unit/test_cli_agent_commands.py +0 -0
  120. {cafe_engine-0.1.2 → cafe_engine-0.1.4}/tests/unit/test_cli_agent_defaults.py +0 -0
  121. {cafe_engine-0.1.2 → cafe_engine-0.1.4}/tests/unit/test_cli_agent_sync.py +0 -0
  122. {cafe_engine-0.1.2 → cafe_engine-0.1.4}/tests/unit/test_cli_auto_mode.py +0 -0
  123. {cafe_engine-0.1.2 → cafe_engine-0.1.4}/tests/unit/test_cli_close.py +0 -0
  124. {cafe_engine-0.1.2 → cafe_engine-0.1.4}/tests/unit/test_cli_close_pr_mode.py +0 -0
  125. {cafe_engine-0.1.2 → cafe_engine-0.1.4}/tests/unit/test_cli_init.py +0 -0
  126. {cafe_engine-0.1.2 → cafe_engine-0.1.4}/tests/unit/test_cli_init_phase_config.py +0 -0
  127. {cafe_engine-0.1.2 → cafe_engine-0.1.4}/tests/unit/test_cli_invoke_command.py +0 -0
  128. {cafe_engine-0.1.2 → cafe_engine-0.1.4}/tests/unit/test_cli_ls_rm.py +0 -0
  129. {cafe_engine-0.1.2 → cafe_engine-0.1.4}/tests/unit/test_cli_make.py +0 -0
  130. {cafe_engine-0.1.2 → cafe_engine-0.1.4}/tests/unit/test_cli_module_init.py +0 -0
  131. {cafe_engine-0.1.2 → cafe_engine-0.1.4}/tests/unit/test_cli_phase_commands.py +0 -0
  132. {cafe_engine-0.1.2 → cafe_engine-0.1.4}/tests/unit/test_cli_prepare_init.py +0 -0
  133. {cafe_engine-0.1.2 → cafe_engine-0.1.4}/tests/unit/test_cli_restore.py +0 -0
  134. {cafe_engine-0.1.2 → cafe_engine-0.1.4}/tests/unit/test_cli_setup_agents.py +0 -0
  135. {cafe_engine-0.1.2 → cafe_engine-0.1.4}/tests/unit/test_cli_show.py +0 -0
  136. {cafe_engine-0.1.2 → cafe_engine-0.1.4}/tests/unit/test_cli_spec_auto.py +0 -0
  137. {cafe_engine-0.1.2 → cafe_engine-0.1.4}/tests/unit/test_cli_spec_output.py +0 -0
  138. {cafe_engine-0.1.2 → cafe_engine-0.1.4}/tests/unit/test_cli_template.py +0 -0
  139. {cafe_engine-0.1.2 → cafe_engine-0.1.4}/tests/unit/test_cli_template_sync.py +0 -0
  140. {cafe_engine-0.1.2 → cafe_engine-0.1.4}/tests/unit/test_config.py +0 -0
  141. {cafe_engine-0.1.2 → cafe_engine-0.1.4}/tests/unit/test_context_endtime.py +0 -0
  142. {cafe_engine-0.1.2 → cafe_engine-0.1.4}/tests/unit/test_context_session_id_update.py +0 -0
  143. {cafe_engine-0.1.2 → cafe_engine-0.1.4}/tests/unit/test_copilot_parse_response.py +0 -0
  144. {cafe_engine-0.1.2 → cafe_engine-0.1.4}/tests/unit/test_cursor_cli.py +0 -0
  145. {cafe_engine-0.1.2 → cafe_engine-0.1.4}/tests/unit/test_delta_display.py +0 -0
  146. {cafe_engine-0.1.2 → cafe_engine-0.1.4}/tests/unit/test_develop_clarification.py +0 -0
  147. {cafe_engine-0.1.2 → cafe_engine-0.1.4}/tests/unit/test_directory_structure.py +0 -0
  148. {cafe_engine-0.1.2 → cafe_engine-0.1.4}/tests/unit/test_display.py +0 -0
  149. {cafe_engine-0.1.2 → cafe_engine-0.1.4}/tests/unit/test_ensure_agent_file_exists.py +0 -0
  150. {cafe_engine-0.1.2 → cafe_engine-0.1.4}/tests/unit/test_ensure_default_content.py +0 -0
  151. {cafe_engine-0.1.2 → cafe_engine-0.1.4}/tests/unit/test_executor_session_recovery_generic.py +0 -0
  152. {cafe_engine-0.1.2 → cafe_engine-0.1.4}/tests/unit/test_gemini_cli.py +0 -0
  153. {cafe_engine-0.1.2 → cafe_engine-0.1.4}/tests/unit/test_gemini_content_extractor.py +0 -0
  154. {cafe_engine-0.1.2 → cafe_engine-0.1.4}/tests/unit/test_git.py +0 -0
  155. {cafe_engine-0.1.2 → cafe_engine-0.1.4}/tests/unit/test_git_utils.py +0 -0
  156. {cafe_engine-0.1.2 → cafe_engine-0.1.4}/tests/unit/test_git_utils_ceiling.py +0 -0
  157. {cafe_engine-0.1.2 → cafe_engine-0.1.4}/tests/unit/test_git_utils_repo_root.py +0 -0
  158. {cafe_engine-0.1.2 → cafe_engine-0.1.4}/tests/unit/test_git_worktree.py +0 -0
  159. {cafe_engine-0.1.2 → cafe_engine-0.1.4}/tests/unit/test_github.py +0 -0
  160. {cafe_engine-0.1.2 → cafe_engine-0.1.4}/tests/unit/test_github_pr_comments.py +0 -0
  161. {cafe_engine-0.1.2 → cafe_engine-0.1.4}/tests/unit/test_github_utils.py +0 -0
  162. {cafe_engine-0.1.2 → cafe_engine-0.1.4}/tests/unit/test_global_config.py +0 -0
  163. {cafe_engine-0.1.2 → cafe_engine-0.1.4}/tests/unit/test_image_download.py +0 -0
  164. {cafe_engine-0.1.2 → cafe_engine-0.1.4}/tests/unit/test_init_helpers.py +0 -0
  165. {cafe_engine-0.1.2 → cafe_engine-0.1.4}/tests/unit/test_inquirer_prompts.py +0 -0
  166. {cafe_engine-0.1.2 → cafe_engine-0.1.4}/tests/unit/test_interactive_qa.py +0 -0
  167. {cafe_engine-0.1.2 → cafe_engine-0.1.4}/tests/unit/test_mock_agent_executor.py +0 -0
  168. {cafe_engine-0.1.2 → cafe_engine-0.1.4}/tests/unit/test_permission.py +0 -0
  169. {cafe_engine-0.1.2 → cafe_engine-0.1.4}/tests/unit/test_phase_cache.py +0 -0
  170. {cafe_engine-0.1.2 → cafe_engine-0.1.4}/tests/unit/test_phase_get_issue_config_value.py +0 -0
  171. {cafe_engine-0.1.2 → cafe_engine-0.1.4}/tests/unit/test_phase_interactive.py +0 -0
  172. {cafe_engine-0.1.2 → cafe_engine-0.1.4}/tests/unit/test_phase_iteration_structure.py +0 -0
  173. {cafe_engine-0.1.2 → cafe_engine-0.1.4}/tests/unit/test_phase_non_interactive_behavior.py +0 -0
  174. {cafe_engine-0.1.2 → cafe_engine-0.1.4}/tests/unit/test_phase_progress.py +0 -0
  175. {cafe_engine-0.1.2 → cafe_engine-0.1.4}/tests/unit/test_phase_prompts.py +0 -0
  176. {cafe_engine-0.1.2 → cafe_engine-0.1.4}/tests/unit/test_phase_status_analysis_with_tools.py +0 -0
  177. {cafe_engine-0.1.2 → cafe_engine-0.1.4}/tests/unit/test_plan_phase_github_sync.py +0 -0
  178. {cafe_engine-0.1.2 → cafe_engine-0.1.4}/tests/unit/test_plan_phase_status_codes.py +0 -0
  179. {cafe_engine-0.1.2 → cafe_engine-0.1.4}/tests/unit/test_plan_phase_sync_github_config.py +0 -0
  180. {cafe_engine-0.1.2 → cafe_engine-0.1.4}/tests/unit/test_plan_phase_template_mode.py +0 -0
  181. {cafe_engine-0.1.2 → cafe_engine-0.1.4}/tests/unit/test_pr_comment_images.py +0 -0
  182. {cafe_engine-0.1.2 → cafe_engine-0.1.4}/tests/unit/test_pr_phase_context.py +0 -0
  183. {cafe_engine-0.1.2 → cafe_engine-0.1.4}/tests/unit/test_pr_phase_generate_content.py +0 -0
  184. {cafe_engine-0.1.2 → cafe_engine-0.1.4}/tests/unit/test_pr_phase_github_mode.py +0 -0
  185. {cafe_engine-0.1.2 → cafe_engine-0.1.4}/tests/unit/test_pr_phase_issue_comment.py +0 -0
  186. {cafe_engine-0.1.2 → cafe_engine-0.1.4}/tests/unit/test_pr_phase_parsing.py +0 -0
  187. {cafe_engine-0.1.2 → cafe_engine-0.1.4}/tests/unit/test_pr_phase_prepare_content.py +0 -0
  188. {cafe_engine-0.1.2 → cafe_engine-0.1.4}/tests/unit/test_pr_phase_status_analysis.py +0 -0
  189. {cafe_engine-0.1.2 → cafe_engine-0.1.4}/tests/unit/test_prepare_auto_sync.py +0 -0
  190. {cafe_engine-0.1.2 → cafe_engine-0.1.4}/tests/unit/test_prompt_utils.py +0 -0
  191. {cafe_engine-0.1.2 → cafe_engine-0.1.4}/tests/unit/test_questions_schema.py +0 -0
  192. {cafe_engine-0.1.2 → cafe_engine-0.1.4}/tests/unit/test_reset_command.py +0 -0
  193. {cafe_engine-0.1.2 → cafe_engine-0.1.4}/tests/unit/test_resolve_sync_github.py +0 -0
  194. {cafe_engine-0.1.2 → cafe_engine-0.1.4}/tests/unit/test_session.py +0 -0
  195. {cafe_engine-0.1.2 → cafe_engine-0.1.4}/tests/unit/test_spec_github_sync.py +0 -0
  196. {cafe_engine-0.1.2 → cafe_engine-0.1.4}/tests/unit/test_spec_image_download.py +0 -0
  197. {cafe_engine-0.1.2 → cafe_engine-0.1.4}/tests/unit/test_spec_phase_qa.py +0 -0
  198. {cafe_engine-0.1.2 → cafe_engine-0.1.4}/tests/unit/test_spec_phase_sync_github_config.py +0 -0
  199. {cafe_engine-0.1.2 → cafe_engine-0.1.4}/tests/unit/test_spec_prompt_with_images.py +0 -0
  200. {cafe_engine-0.1.2 → cafe_engine-0.1.4}/tests/unit/test_summary_display.py +0 -0
  201. {cafe_engine-0.1.2 → cafe_engine-0.1.4}/tests/unit/test_summary_service.py +0 -0
  202. {cafe_engine-0.1.2 → cafe_engine-0.1.4}/tests/unit/test_sync_helpers.py +0 -0
  203. {cafe_engine-0.1.2 → cafe_engine-0.1.4}/tests/unit/test_template_edit_auto_sync.py +0 -0
  204. {cafe_engine-0.1.2 → cafe_engine-0.1.4}/tests/unit/test_template_manager.py +0 -0
  205. {cafe_engine-0.1.2 → cafe_engine-0.1.4}/tests/unit/test_template_manager_global.py +0 -0
  206. {cafe_engine-0.1.2 → cafe_engine-0.1.4}/tests/unit/test_template_selector_auto.py +0 -0
  207. {cafe_engine-0.1.2 → cafe_engine-0.1.4}/tests/unit/test_time_formatter.py +0 -0
  208. {cafe_engine-0.1.2 → cafe_engine-0.1.4}/tests/unit/test_timeline_builder.py +0 -0
  209. {cafe_engine-0.1.2 → cafe_engine-0.1.4}/tests/unit/test_types.py +0 -0
  210. {cafe_engine-0.1.2 → cafe_engine-0.1.4}/tests/unit/utils/test_checklist_validator.py +0 -0
@@ -2,6 +2,17 @@
2
2
 
3
3
  All notable changes to this project will be documented in this file.
4
4
 
5
+ ## [0.1.3]
6
+
7
+ ### Added
8
+ - Added `--post-todo-list` option to `cafe pr` command to post PR comment todo list as GitHub PR comment
9
+ - Added `--post-pr-todo-list` option to `cafe prepare` command to set post-PR-todo-list mode for prepare phase
10
+ - Added token usage summary display after PR comment organization completes
11
+
12
+ ### Fixed
13
+ - Fixed copilot session_id detection to recognize both file-based (.jsonl) and directory-based session formats
14
+ - Fixed `_print_token_usage_summary` to gracefully handle mocked agent_manager in tests
15
+
5
16
  ## [0.1.2]
6
17
 
7
18
  ### Fixed
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cafe-engine
3
- Version: 0.1.2
3
+ Version: 0.1.4
4
4
  Summary: CLI Agent Flow Engine - Automated development workflow with AI agents
5
5
  License-File: LICENSE
6
6
  Requires-Python: >=3.10
@@ -82,9 +82,35 @@ CAFE supports `git worktree`, enabling parallel development across multiple issu
82
82
 
83
83
  ### Flexible Integration with CLI Agent Tools
84
84
 
85
- CAFE integrates with multiple CLI-based AI agents (e.g., Claude Code, Cursor CLI).
85
+ CAFE integrates with multiple CLI-based AI agents (e.g., Claude Code, Cursor CLI).
86
86
  Different agents and models can be assigned per role, allowing you to balance cost, performance, and reasoning depth.
87
87
 
88
+ ### Automatic Backup Agent Switching on Rate Limits
89
+
90
+ When a primary agent hits an API rate limit, CAFE automatically switches to backup agents in configured order—without stopping the workflow. If the backup agent also hits a rate limit, CAFE continues to the next backup until one succeeds or all are exhausted.
91
+
92
+ You can configure backup agents and their per-phase models in `.cafe/config.yaml`:
93
+
94
+ ```yaml
95
+ agents:
96
+ developer:
97
+ name: David
98
+ cli: claude # Primary CLI agent
99
+ backup: # Backup agents (tried in order on rate limit)
100
+ - gemini
101
+ - copilot
102
+ models: # Per-CLI, per-phase model configuration
103
+ claude:
104
+ plan: opus
105
+ develop: sonnet
106
+ gemini:
107
+ plan: gemini-2.5-pro-preview
108
+ develop: gemini-2-flash-preview
109
+ copilot: {} # Use CLI default model
110
+ ```
111
+
112
+ If all agents (primary + backups) are exhausted, the workflow stops with a clear error message listing which agents were tried. You can add more backup agents with `cafe config edit`.
113
+
88
114
  ---
89
115
 
90
116
  ### GitHub-Native Workflow
@@ -58,9 +58,35 @@ CAFE supports `git worktree`, enabling parallel development across multiple issu
58
58
 
59
59
  ### Flexible Integration with CLI Agent Tools
60
60
 
61
- CAFE integrates with multiple CLI-based AI agents (e.g., Claude Code, Cursor CLI).
61
+ CAFE integrates with multiple CLI-based AI agents (e.g., Claude Code, Cursor CLI).
62
62
  Different agents and models can be assigned per role, allowing you to balance cost, performance, and reasoning depth.
63
63
 
64
+ ### Automatic Backup Agent Switching on Rate Limits
65
+
66
+ When a primary agent hits an API rate limit, CAFE automatically switches to backup agents in configured order—without stopping the workflow. If the backup agent also hits a rate limit, CAFE continues to the next backup until one succeeds or all are exhausted.
67
+
68
+ You can configure backup agents and their per-phase models in `.cafe/config.yaml`:
69
+
70
+ ```yaml
71
+ agents:
72
+ developer:
73
+ name: David
74
+ cli: claude # Primary CLI agent
75
+ backup: # Backup agents (tried in order on rate limit)
76
+ - gemini
77
+ - copilot
78
+ models: # Per-CLI, per-phase model configuration
79
+ claude:
80
+ plan: opus
81
+ develop: sonnet
82
+ gemini:
83
+ plan: gemini-2.5-pro-preview
84
+ develop: gemini-2-flash-preview
85
+ copilot: {} # Use CLI default model
86
+ ```
87
+
88
+ If all agents (primary + backups) are exhausted, the workflow stops with a clear error message listing which agents were tried. You can add more backup agents with `cafe config edit`.
89
+
64
90
  ---
65
91
 
66
92
  ### GitHub-Native Workflow
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "cafe-engine"
3
- version = "0.1.2"
3
+ version = "0.1.4"
4
4
  description = "CLI Agent Flow Engine - Automated development workflow with AI agents"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.10"
@@ -97,13 +97,15 @@ class CopilotCLI(AbstractCLI):
97
97
  # claude-sonnet-4.5 14.3k input, 39 output, 10.2k cache read (Est. 1 Premium request)
98
98
  # New: Breakdown by AI model:
99
99
  # claude-sonnet-4.5 16.0k in, 74 out, 0 cached (Est. 1 Premium request)
100
+ # claude-sonnet-4.5 1.2m in, 4.6k out, 1.1m cached (Est. 1 Premium request)
101
+ # Token counts can have k (thousands) or m (millions) suffix
100
102
 
101
103
  # Try new format first
102
- usage_match = re.search(r'Breakdown by AI model:\s*\n\s+([\w.-]+)\s+([\d.]+k?)\s+in,\s+([\d.]+k?)\s+out(?:,\s+([\d.]+k?)\s+cached)?', full_output)
104
+ usage_match = re.search(r'Breakdown by AI model:\s*\n\s+([\w.-]+)\s+([\d.]+[km]?)\s+in,\s+([\d.]+[km]?)\s+out(?:,\s+([\d.]+[km]?)\s+cached)?', full_output)
103
105
 
104
106
  # Fall back to old format if new format not found
105
107
  if not usage_match:
106
- usage_match = re.search(r'Usage by model:\s*\n\s+([\w.-]+)\s+([\d.]+k?)\s+input,\s+([\d.]+k?)\s+output(?:,\s+([\d.]+k?)\s+cache read)?', full_output)
108
+ usage_match = re.search(r'Usage by model:\s*\n\s+([\w.-]+)\s+([\d.]+[km]?)\s+input,\s+([\d.]+[km]?)\s+output(?:,\s+([\d.]+[km]?)\s+cache read)?', full_output)
107
109
 
108
110
  if usage_match:
109
111
  # Extract model name
@@ -147,17 +149,19 @@ class CopilotCLI(AbstractCLI):
147
149
  return response, token_usage, permission_denials, model
148
150
 
149
151
  def _parse_token_count(self, token_str: str) -> int:
150
- """Parse token count string (e.g., '14.3k' or '39') to integer.
151
-
152
+ """Parse token count string (e.g., '14.3k', '1.2m', or '39') to integer.
153
+
152
154
  Args:
153
- token_str: Token count string
154
-
155
+ token_str: Token count string with optional k (thousands) or m (millions) suffix
156
+
155
157
  Returns:
156
158
  Token count as integer
157
159
  """
158
160
  token_str = token_str.strip()
159
- if token_str.endswith('k'):
160
- # Convert 'k' notation to thousands
161
+ if token_str.endswith('m'):
162
+ number = float(token_str[:-1])
163
+ return int(number * 1_000_000)
164
+ elif token_str.endswith('k'):
161
165
  number = float(token_str[:-1])
162
166
  return int(number * 1000)
163
167
  else:
@@ -221,7 +225,7 @@ class CopilotCLI(AbstractCLI):
221
225
 
222
226
  if copilot_session_dir.exists():
223
227
  self._existing_sessions = {
224
- f.name for f in copilot_session_dir.iterdir() if f.is_file()
228
+ f.name for f in copilot_session_dir.iterdir() if f.is_file() or f.is_dir()
225
229
  }
226
230
  else:
227
231
  self._existing_sessions = set()
@@ -229,7 +233,7 @@ class CopilotCLI(AbstractCLI):
229
233
  def extract_session_id(self, output_lines: List[str]) -> Optional[str]:
230
234
  """Extract newly created session ID from file system.
231
235
 
232
- Copilot automatically creates session files, need to detect from file system.
236
+ Copilot automatically creates session directories, need to detect from file system.
233
237
 
234
238
  Args:
235
239
  output_lines: List of lines from CLI output (not used here)
@@ -245,9 +249,9 @@ class CopilotCLI(AbstractCLI):
245
249
  # Wait for file system to update
246
250
  time.sleep(0.1)
247
251
 
248
- # Get current session files
252
+ # Get current session files and directories
249
253
  current_sessions = {
250
- f.name for f in copilot_session_dir.iterdir() if f.is_file()
254
+ f.name for f in copilot_session_dir.iterdir() if f.is_file() or f.is_dir()
251
255
  }
252
256
 
253
257
  # Find newly created sessions
@@ -256,7 +260,7 @@ class CopilotCLI(AbstractCLI):
256
260
  if new_sessions:
257
261
  # Get newest session (sorted by name)
258
262
  newest_session = sorted(new_sessions)[-1]
259
- # Remove .jsonl extension
263
+ # Remove .jsonl extension if present (for file-based sessions)
260
264
  session_id = newest_session.replace(".jsonl", "")
261
265
  return session_id
262
266
 
@@ -820,6 +820,21 @@ class AgentExecutor:
820
820
  err.cli_command_args = cmd[1:]
821
821
  raise err
822
822
 
823
+ # Append stderr to streaming output file (for debugging token usage parsing)
824
+ if streaming_file_handle and stderr_output:
825
+ try:
826
+ from datetime import datetime
827
+ stderr_obj = {
828
+ "index": streaming_line_index,
829
+ "timestamp": datetime.now().astimezone().isoformat(),
830
+ "type": "stderr",
831
+ "content": stderr_output.rstrip('\n'),
832
+ }
833
+ streaming_file_handle.write(json.dumps(stderr_obj, ensure_ascii=False) + "\n")
834
+ streaming_file_handle.flush()
835
+ except Exception as e:
836
+ print(f"⚠️ Failed to write stderr to streaming output file: {e}")
837
+
823
838
  # Close streaming output file
824
839
  if streaming_file_handle:
825
840
  try:
@@ -886,5 +901,5 @@ class AgentExecutor:
886
901
  token_usage=token_usage,
887
902
  permission_denials=permission_denials,
888
903
  streaming_log=final_streaming_log,
889
- model=model
904
+ model=model,
890
905
  )
@@ -7,7 +7,7 @@ from typing import Any, Dict, List, Optional, Tuple
7
7
 
8
8
  from cafe.agents.executor import AgentExecutor, AgentExecutionError
9
9
  from cafe.core.session import SessionManager
10
- from cafe.core.types import AgentConfig, PermissionDenial, TokenUsage
10
+ from cafe.core.types import AgentConfig, AgentResponse, PermissionDenial, TokenUsage
11
11
 
12
12
 
13
13
  class AgentNotFoundError(Exception):
@@ -63,12 +63,14 @@ class AgentManager:
63
63
  session_id = session_data.session_id if session_data else None
64
64
  # Note: Don't create session here - let executor handle it on first use
65
65
 
66
- # Update config with session ID (may be None)
66
+ # Update config with session ID (may be None), preserve backup and models config
67
67
  config_with_session = AgentConfig(
68
68
  name=config.name,
69
69
  cli=config.cli,
70
70
  session_id=session_id,
71
71
  model=config.model,
72
+ backup_clis=config.backup_clis,
73
+ models_config=config.models_config,
72
74
  )
73
75
 
74
76
  # Create executor
@@ -120,7 +122,8 @@ class AgentManager:
120
122
  prompt: str,
121
123
  allowed_tools: Optional[List[str]] = None,
122
124
  allowed_directories: Optional[List[str]] = None,
123
- streaming_output_file: Optional[str] = None
125
+ streaming_output_file: Optional[str] = None,
126
+ phase_name: Optional[str] = None,
124
127
  ) -> Tuple[str, TokenUsage, List, Optional[List[str]], List[str], Optional[str]]:
125
128
  """Execute prompt with specified agent.
126
129
 
@@ -130,12 +133,14 @@ class AgentManager:
130
133
  allowed_tools: List of allowed tools (using Claude naming convention)
131
134
  allowed_directories: List of allowed directories
132
135
  streaming_output_file: Optional file path to write streaming output line-by-line
136
+ phase_name: Current phase name for phase-specific model lookup in backup agents
133
137
 
134
138
  Returns:
135
139
  Tuple of (agent's response, token usage, permission denials, cli_command_args, streaming_log, model)
136
140
 
137
141
  Raises:
138
142
  AgentNotFoundError: If agent not found
143
+ AgentExecutionError: If all agents (primary + backups) fail
139
144
  """
140
145
  executor = self.get_agent(agent_name)
141
146
 
@@ -163,6 +168,18 @@ class AgentManager:
163
168
  executor.config.session_id = None
164
169
 
165
170
  # Loop will retry (with no session ID, a new one will be created)
171
+ elif hasattr(e, 'error_type') and e.error_type in ("rate_limit", "cli_not_found"):
172
+ # Try backup agents
173
+ agent_response = self._try_backup_agents(
174
+ primary_error=e,
175
+ primary_executor=executor,
176
+ prompt=prompt,
177
+ allowed_tools=allowed_tools,
178
+ allowed_directories=allowed_directories,
179
+ streaming_output_file=streaming_output_file,
180
+ phase_name=phase_name,
181
+ )
182
+ break # Backup succeeded, exit loop
166
183
  else:
167
184
  # Not a session conflict, or already retried - re-raise
168
185
  raise
@@ -206,6 +223,109 @@ class AgentManager:
206
223
 
207
224
  return response, token_usage, permission_denials, cli_command_args, streaming_log, model
208
225
 
226
+ def _try_backup_agents(
227
+ self,
228
+ primary_error: "AgentExecutionError",
229
+ primary_executor: AgentExecutor,
230
+ prompt: str,
231
+ allowed_tools: Optional[List[str]] = None,
232
+ allowed_directories: Optional[List[str]] = None,
233
+ streaming_output_file: Optional[str] = None,
234
+ phase_name: Optional[str] = None,
235
+ ) -> "AgentResponse":
236
+ """Try backup agents in order until one succeeds or all fail.
237
+
238
+ Backup retry flow:
239
+ 1. If no backup CLIs are configured, re-raise the original error immediately
240
+ 2. Try each backup CLI in backup_clis in order
241
+ 3. If a backup CLI also hits rate_limit or cli_not_found, continue to the next
242
+ 4. If a backup CLI raises any other error, re-raise it immediately (do not continue)
243
+ 5. If all backups fail, raise an error listing all attempted CLIs
244
+
245
+ phase_name is used to look up the model for the backup CLI from models_config,
246
+ e.g. during the "develop" phase with gemini backup: models_config["gemini"]["develop"].
247
+
248
+ Args:
249
+ primary_error: Error raised by the primary agent
250
+ primary_executor: Primary agent executor
251
+ prompt: Prompt to execute
252
+ allowed_tools: List of allowed tools
253
+ allowed_directories: List of allowed directories
254
+ streaming_output_file: File path for streaming output
255
+ phase_name: Current phase name, used to look up model configuration
256
+
257
+ Returns:
258
+ AgentResponse: Response from the first successful backup agent
259
+
260
+ Raises:
261
+ AgentExecutionError: Raised when all agents (primary + backups) fail
262
+ """
263
+ config = primary_executor.config
264
+ backup_clis = config.backup_clis
265
+ models_config = config.models_config
266
+
267
+ if not backup_clis:
268
+ # No backup agents configured, re-raise the original error
269
+ raise primary_error
270
+
271
+ primary_cli_name = config.cli.value
272
+ print(f"❌ {primary_cli_name} API rate limit reached, trying backup agent...")
273
+
274
+ # Track tried CLIs to avoid duplicates (primary CLI is included from the start)
275
+ tried_clis = {config.cli}
276
+ failed_agents: List[str] = [f"{primary_cli_name} ({primary_error})"]
277
+
278
+ for backup_cli in backup_clis:
279
+ # Skip CLIs already tried (e.g. backup_clis contains the same CLI as primary)
280
+ if backup_cli in tried_clis:
281
+ continue
282
+ tried_clis.add(backup_cli)
283
+
284
+ # Look up the model for this backup CLI in the current phase
285
+ # e.g. models_config = {"gemini": {"develop": "gemini-2-flash-preview"}}
286
+ # If not configured or empty string, use None (CLI default model)
287
+ backup_model: Optional[str] = None
288
+ if phase_name and models_config:
289
+ backup_model = models_config.get(backup_cli.value, {}).get(phase_name) or None
290
+
291
+ # Treat empty string as None
292
+ if backup_model == "":
293
+ backup_model = None
294
+
295
+ print(f"Trying {backup_cli.value}...")
296
+
297
+ # Create a new executor for the backup CLI (fresh session to avoid session contamination)
298
+ backup_config = AgentConfig(
299
+ name=config.name,
300
+ cli=backup_cli,
301
+ model=backup_model,
302
+ )
303
+ backup_executor = AgentExecutor(backup_config)
304
+
305
+ try:
306
+ agent_response = backup_executor.execute(
307
+ prompt, allowed_tools, allowed_directories, streaming_output_file
308
+ )
309
+ print(f"✅ Successfully completed with {backup_cli.value}")
310
+ return agent_response
311
+ except AgentExecutionError as backup_error:
312
+ if hasattr(backup_error, 'error_type') and backup_error.error_type in ("rate_limit", "cli_not_found"):
313
+ # rate_limit or cli_not_found: record and continue to next backup
314
+ failed_agents.append(f"{backup_cli.value} ({backup_error})")
315
+ print(f"❌ {backup_cli.value} also hit rate limit, trying next agent...")
316
+ continue
317
+ else:
318
+ # Other errors (e.g. invalid prompt format): re-raise immediately
319
+ raise
320
+
321
+ # All agents (primary + all backups) failed, compose error message
322
+ tried_list = ", ".join(failed_agents)
323
+ raise AgentExecutionError(
324
+ f"All agents failed. Tried: {tried_list}. "
325
+ f"Please wait for rate limits to reset or add more backup agents.",
326
+ error_type="rate_limit",
327
+ )
328
+
209
329
  def execute_current(self, prompt: str) -> str:
210
330
  """Execute prompt with current agent.
211
331
 
@@ -848,6 +848,72 @@ class Phase(ABC):
848
848
 
849
849
  return prompt_multiline("Please answer the question")
850
850
 
851
+ def _validate_and_retry_questions_xml(
852
+ self,
853
+ xml_path: Path,
854
+ agent_name: str,
855
+ allowed_tools: List[str],
856
+ max_retries: int = 3,
857
+ ) -> bool:
858
+ """Validate questions.xml and retry with agent if invalid.
859
+
860
+ Args:
861
+ xml_path: Path to questions.xml file
862
+ agent_name: Agent name for retry execution
863
+ allowed_tools: Tools allowed for agent
864
+ max_retries: Maximum number of retry attempts (default: 3)
865
+
866
+ Returns:
867
+ True if XML is valid (or was fixed), False otherwise
868
+ """
869
+ from cafe.core.questions_schema import validate_questions_xml
870
+
871
+ # Require agent_manager attribute
872
+ if not hasattr(self, 'agent_manager'):
873
+ raise AttributeError("Phase must have 'agent_manager' attribute to validate questions XML")
874
+
875
+ # Check if file exists
876
+ if not xml_path.exists():
877
+ return False
878
+
879
+ # First validation attempt
880
+ if validate_questions_xml(xml_path):
881
+ return True
882
+
883
+ # Retry loop: ask agent to fix invalid XML
884
+ for retry in range(max_retries):
885
+ print(f"\n⚠️ questions.xml format is invalid, asking agent to fix... (attempt {retry + 1}/{max_retries})")
886
+
887
+ retry_prompt = (
888
+ f"The questions XML file at {xml_path} has invalid format. "
889
+ f"Please fix it so that:\n"
890
+ f"- Root element is <questions>\n"
891
+ f"- Each <question> has a unique id attribute, a <title>, and <options> with at least one <option>\n"
892
+ f"- The file is well-formed XML\n\n"
893
+ f"Read the file, fix the issues, and write the corrected XML back to {xml_path}."
894
+ )
895
+
896
+ try:
897
+ self.agent_manager.execute(
898
+ agent_name,
899
+ retry_prompt,
900
+ allowed_tools=allowed_tools,
901
+ )
902
+ except Exception as e:
903
+ print(f"⚠️ Error during XML fix retry: {e}")
904
+ continue
905
+
906
+ # Check if fixed
907
+ if xml_path.exists() and validate_questions_xml(xml_path):
908
+ print(f"✓ Agent successfully fixed questions.xml")
909
+ return True
910
+
911
+ # All retries failed - delete invalid file and fallback
912
+ print(f"\n❌ questions.xml still invalid after {max_retries} attempts, falling back to original Q&A")
913
+ if xml_path.exists():
914
+ xml_path.unlink()
915
+ return False
916
+
851
917
  def _process_review_decision(
852
918
  self,
853
919
  choice: str,
@@ -1136,8 +1202,15 @@ class Phase(ABC):
1136
1202
  if not hasattr(self, "agent_manager"):
1137
1203
  return
1138
1204
 
1139
- token_usage = self.agent_manager.get_total_token_usage()
1140
- model = self.agent_manager.get_last_model()
1205
+ try:
1206
+ token_usage = self.agent_manager.get_total_token_usage()
1207
+ model = self.agent_manager.get_last_model()
1208
+
1209
+ # Verify we have real token usage data (not mocks)
1210
+ if not isinstance(token_usage.input_tokens, int):
1211
+ return
1212
+ except (AttributeError, TypeError):
1213
+ return
1141
1214
 
1142
1215
  print()
1143
1216
  print("=" * 60)
@@ -28,7 +28,7 @@ class PhaseStatusCode(str, Enum):
28
28
 
29
29
  # ========== Develop Phase ==========
30
30
  NO_CHANGES_NEEDED = "CAFE_NO_CHANGES_NEEDED" # Developer believes no changes needed (dispute reviewer)
31
- SKIP_REVIEW = "CAFE_SKIP_REVIEW" # User agreed with developer, skip review phase
31
+ CONFIRMED_SKIP_REVIEW = "CAFE_CONFIRMED_SKIP_REVIEW" # User confirmed to skip review phase (development completed)
32
32
 
33
33
  # ========== Authorization ==========
34
34
  NEED_PERMISSION = "CAFE_NEED_PERMISSION" # Need user permission
@@ -130,9 +130,22 @@ class StatusCodeParser:
130
130
  response_upper = response.upper()
131
131
 
132
132
  # Search for all status codes in the response
133
- codes_to_check = valid_codes if valid_codes else list(PhaseStatusCode)
133
+ # Sort by length descending so longer (more specific) codes are checked first
134
+ codes_to_check = sorted(
135
+ valid_codes if valid_codes else list(PhaseStatusCode),
136
+ key=lambda x: len(x.value),
137
+ reverse=True,
138
+ )
134
139
  for code in codes_to_check:
135
140
  if code.value in response_upper:
141
+ # Skip if this code is a prefix of an already-found longer code
142
+ if any(found.value.startswith(code.value) for found in found_codes):
143
+ continue
144
+ # Remove any already-found codes that are prefixes of this code
145
+ found_codes = {
146
+ found for found in found_codes
147
+ if not code.value.startswith(found.value)
148
+ }
136
149
  found_codes.add(code)
137
150
 
138
151
  return found_codes
@@ -56,12 +56,36 @@ class PermissionAction(str, Enum):
56
56
 
57
57
 
58
58
  class AgentConfig(BaseModel):
59
- """Configuration for an AI agent."""
59
+ """Configuration for an AI agent.
60
+
61
+ backup_clis and models_config support automatic backup agent switching.
62
+ When the primary CLI hits a rate limit, AgentManager tries each backup CLI in order.
63
+
64
+ Example config.yaml::
65
+
66
+ agents:
67
+ developer:
68
+ name: David
69
+ cli: claude # Primary CLI
70
+ backup: # Backup CLIs (tried in order)
71
+ - gemini
72
+ - copilot
73
+ models: # Per-CLI per-phase model configuration
74
+ claude:
75
+ plan: opus
76
+ develop: sonnet
77
+ gemini:
78
+ plan: gemini-2.5-pro-preview
79
+ develop: gemini-2-flash-preview
80
+ copilot: {} # Use CLI default model
81
+ """
60
82
 
61
83
  name: str
62
84
  cli: AgentCLI
63
85
  session_id: Optional[str] = None
64
86
  model: Optional[str] = None # Optional model name for CLI (e.g., "sonnet", "opus")
87
+ backup_clis: List["AgentCLI"] = Field(default_factory=list) # Ordered list of backup CLIs
88
+ models_config: Dict[str, Dict[str, str]] = Field(default_factory=dict) # Per-CLI per-phase model configuration
65
89
 
66
90
 
67
91
  class TokenUsage(BaseModel):