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.
- {cafe_engine-0.1.2 → cafe_engine-0.1.4}/CHANGELOG.md +11 -0
- {cafe_engine-0.1.2 → cafe_engine-0.1.4}/PKG-INFO +28 -2
- {cafe_engine-0.1.2 → cafe_engine-0.1.4}/README.md +27 -1
- {cafe_engine-0.1.2 → cafe_engine-0.1.4}/pyproject.toml +1 -1
- {cafe_engine-0.1.2 → cafe_engine-0.1.4}/src/cafe/agents/cli/copilot.py +17 -13
- {cafe_engine-0.1.2 → cafe_engine-0.1.4}/src/cafe/agents/executor.py +16 -1
- {cafe_engine-0.1.2 → cafe_engine-0.1.4}/src/cafe/agents/manager.py +123 -3
- {cafe_engine-0.1.2 → cafe_engine-0.1.4}/src/cafe/core/phase.py +75 -2
- {cafe_engine-0.1.2 → cafe_engine-0.1.4}/src/cafe/core/status_codes.py +15 -2
- {cafe_engine-0.1.2 → cafe_engine-0.1.4}/src/cafe/core/types.py +25 -1
- {cafe_engine-0.1.2 → cafe_engine-0.1.4}/src/cafe/phases/develop_phase.py +156 -88
- {cafe_engine-0.1.2 → cafe_engine-0.1.4}/src/cafe/phases/plan_phase.py +50 -3
- {cafe_engine-0.1.2 → cafe_engine-0.1.4}/src/cafe/phases/pr_phase.py +134 -32
- {cafe_engine-0.1.2 → cafe_engine-0.1.4}/src/cafe/phases/spec_phase.py +0 -62
- {cafe_engine-0.1.2 → cafe_engine-0.1.4}/src/cafe/ui/cli.py +187 -10
- {cafe_engine-0.1.2 → cafe_engine-0.1.4}/src/cafe/utils/checklist_generator.py +22 -1
- {cafe_engine-0.1.2 → cafe_engine-0.1.4}/src/cafe/utils/checklist_templates.py +8 -5
- {cafe_engine-0.1.2 → cafe_engine-0.1.4}/src/cafe/utils/config.py +50 -0
- cafe_engine-0.1.4/tests/unit/test_backup_agent.py +455 -0
- {cafe_engine-0.1.2 → cafe_engine-0.1.4}/tests/unit/test_cli.py +188 -0
- {cafe_engine-0.1.2 → cafe_engine-0.1.4}/tests/unit/test_cli_pr_command.py +87 -1
- {cafe_engine-0.1.2 → cafe_engine-0.1.4}/tests/unit/test_cli_prepare.py +186 -0
- {cafe_engine-0.1.2 → cafe_engine-0.1.4}/tests/unit/test_copilot_cli.py +43 -0
- cafe_engine-0.1.4/tests/unit/test_develop_no_changes_needed.py +420 -0
- {cafe_engine-0.1.2 → cafe_engine-0.1.4}/tests/unit/test_develop_phase_prompt.py +105 -6
- cafe_engine-0.1.4/tests/unit/test_plan_checklist_xml.py +80 -0
- cafe_engine-0.1.4/tests/unit/test_plan_phase_execute_xml.py +93 -0
- cafe_engine-0.1.4/tests/unit/test_plan_phase_qa.py +227 -0
- {cafe_engine-0.1.2 → cafe_engine-0.1.4}/tests/unit/test_pr_phase_iteration_logic.py +243 -0
- cafe_engine-0.1.4/tests/unit/test_pr_phase_output_todo_only.py +194 -0
- cafe_engine-0.1.4/tests/unit/test_pr_phase_post_todo_comment.py +317 -0
- {cafe_engine-0.1.2 → cafe_engine-0.1.4}/tests/unit/test_status_codes.py +7 -7
- {cafe_engine-0.1.2 → cafe_engine-0.1.4}/uv.lock +1 -1
- cafe_engine-0.1.2/tests/unit/test_develop_no_changes_needed.py +0 -205
- {cafe_engine-0.1.2 → cafe_engine-0.1.4}/.claude/settings.local.json +0 -0
- {cafe_engine-0.1.2 → cafe_engine-0.1.4}/.geminiignore +0 -0
- {cafe_engine-0.1.2 → cafe_engine-0.1.4}/.githooks/pre-commit +0 -0
- {cafe_engine-0.1.2 → cafe_engine-0.1.4}/.gitignore +0 -0
- {cafe_engine-0.1.2 → cafe_engine-0.1.4}/CONTRIBUTING.md +0 -0
- {cafe_engine-0.1.2 → cafe_engine-0.1.4}/LICENSE +0 -0
- {cafe_engine-0.1.2 → cafe_engine-0.1.4}/images/cafe-flow.png +0 -0
- {cafe_engine-0.1.2 → cafe_engine-0.1.4}/scripts/simulate_agent_test.sh +0 -0
- {cafe_engine-0.1.2 → cafe_engine-0.1.4}/setup-hooks.sh +0 -0
- {cafe_engine-0.1.2 → cafe_engine-0.1.4}/src/cafe/__init__.py +0 -0
- {cafe_engine-0.1.2 → cafe_engine-0.1.4}/src/cafe/agents/__init__.py +0 -0
- {cafe_engine-0.1.2 → cafe_engine-0.1.4}/src/cafe/agents/cli/__init__.py +0 -0
- {cafe_engine-0.1.2 → cafe_engine-0.1.4}/src/cafe/agents/cli/abstract.py +0 -0
- {cafe_engine-0.1.2 → cafe_engine-0.1.4}/src/cafe/agents/cli/claude.py +0 -0
- {cafe_engine-0.1.2 → cafe_engine-0.1.4}/src/cafe/agents/cli/cursor.py +0 -0
- {cafe_engine-0.1.2 → cafe_engine-0.1.4}/src/cafe/agents/cli/gemini.py +0 -0
- {cafe_engine-0.1.2 → cafe_engine-0.1.4}/src/cafe/agents/mock_executor.py +0 -0
- {cafe_engine-0.1.2 → cafe_engine-0.1.4}/src/cafe/core/__init__.py +0 -0
- {cafe_engine-0.1.2 → cafe_engine-0.1.4}/src/cafe/core/git.py +0 -0
- {cafe_engine-0.1.2 → cafe_engine-0.1.4}/src/cafe/core/permission.py +0 -0
- {cafe_engine-0.1.2 → cafe_engine-0.1.4}/src/cafe/core/phase_cache.py +0 -0
- {cafe_engine-0.1.2 → cafe_engine-0.1.4}/src/cafe/core/questions_schema.py +0 -0
- {cafe_engine-0.1.2 → cafe_engine-0.1.4}/src/cafe/core/session.py +0 -0
- {cafe_engine-0.1.2 → cafe_engine-0.1.4}/src/cafe/data/agents/developer/David.md +0 -0
- {cafe_engine-0.1.2 → cafe_engine-0.1.4}/src/cafe/data/agents/developer/Nick.md +0 -0
- {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
- {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
- {cafe_engine-0.1.2 → cafe_engine-0.1.4}/src/cafe/data/agents/pm/Roger.md +0 -0
- {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
- {cafe_engine-0.1.2 → cafe_engine-0.1.4}/src/cafe/data/agents/reviewer/Richard.md +0 -0
- {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
- {cafe_engine-0.1.2 → cafe_engine-0.1.4}/src/cafe/data/templates/plan/bug.md +0 -0
- {cafe_engine-0.1.2 → cafe_engine-0.1.4}/src/cafe/data/templates/plan/default.md +0 -0
- {cafe_engine-0.1.2 → cafe_engine-0.1.4}/src/cafe/data/templates/plan/simple.md +0 -0
- {cafe_engine-0.1.2 → cafe_engine-0.1.4}/src/cafe/data/templates/spec/default.md +0 -0
- {cafe_engine-0.1.2 → cafe_engine-0.1.4}/src/cafe/data/templates/spec/detailed.md +0 -0
- {cafe_engine-0.1.2 → cafe_engine-0.1.4}/src/cafe/data/templates/spec/simple.md +0 -0
- {cafe_engine-0.1.2 → cafe_engine-0.1.4}/src/cafe/phases/__init__.py +0 -0
- {cafe_engine-0.1.2 → cafe_engine-0.1.4}/src/cafe/phases/review_phase.py +0 -0
- {cafe_engine-0.1.2 → cafe_engine-0.1.4}/src/cafe/services/__init__.py +0 -0
- {cafe_engine-0.1.2 → cafe_engine-0.1.4}/src/cafe/services/delta_display.py +0 -0
- {cafe_engine-0.1.2 → cafe_engine-0.1.4}/src/cafe/services/summary_display.py +0 -0
- {cafe_engine-0.1.2 → cafe_engine-0.1.4}/src/cafe/services/summary_service.py +0 -0
- {cafe_engine-0.1.2 → cafe_engine-0.1.4}/src/cafe/services/time_formatter.py +0 -0
- {cafe_engine-0.1.2 → cafe_engine-0.1.4}/src/cafe/services/timeline_builder.py +0 -0
- {cafe_engine-0.1.2 → cafe_engine-0.1.4}/src/cafe/templates/__init__.py +0 -0
- {cafe_engine-0.1.2 → cafe_engine-0.1.4}/src/cafe/templates/manager.py +0 -0
- {cafe_engine-0.1.2 → cafe_engine-0.1.4}/src/cafe/ui/__init__.py +0 -0
- {cafe_engine-0.1.2 → cafe_engine-0.1.4}/src/cafe/ui/display.py +0 -0
- {cafe_engine-0.1.2 → cafe_engine-0.1.4}/src/cafe/ui/init_helpers.py +0 -0
- {cafe_engine-0.1.2 → cafe_engine-0.1.4}/src/cafe/ui/inquirer_prompts.py +0 -0
- {cafe_engine-0.1.2 → cafe_engine-0.1.4}/src/cafe/ui/interactive_qa.py +0 -0
- {cafe_engine-0.1.2 → cafe_engine-0.1.4}/src/cafe/ui/phase_prompts.py +0 -0
- {cafe_engine-0.1.2 → cafe_engine-0.1.4}/src/cafe/ui/template_selector.py +0 -0
- {cafe_engine-0.1.2 → cafe_engine-0.1.4}/src/cafe/utils/__init__.py +0 -0
- {cafe_engine-0.1.2 → cafe_engine-0.1.4}/src/cafe/utils/checklist_utils.py +0 -0
- {cafe_engine-0.1.2 → cafe_engine-0.1.4}/src/cafe/utils/checklist_validator.py +0 -0
- {cafe_engine-0.1.2 → cafe_engine-0.1.4}/src/cafe/utils/git_utils.py +0 -0
- {cafe_engine-0.1.2 → cafe_engine-0.1.4}/src/cafe/utils/github.py +0 -0
- {cafe_engine-0.1.2 → cafe_engine-0.1.4}/src/cafe/utils/prompt_utils.py +0 -0
- {cafe_engine-0.1.2 → cafe_engine-0.1.4}/tests/.gitignore +0 -0
- {cafe_engine-0.1.2 → cafe_engine-0.1.4}/tests/__init__.py +0 -0
- {cafe_engine-0.1.2 → cafe_engine-0.1.4}/tests/conftest.py +0 -0
- {cafe_engine-0.1.2 → cafe_engine-0.1.4}/tests/fixtures/minimal_config.yaml +0 -0
- {cafe_engine-0.1.2 → cafe_engine-0.1.4}/tests/integration/__init__.py +0 -0
- {cafe_engine-0.1.2 → cafe_engine-0.1.4}/tests/integration/conftest.py +0 -0
- {cafe_engine-0.1.2 → cafe_engine-0.1.4}/tests/integration/test_gemini_session_integration.py +0 -0
- {cafe_engine-0.1.2 → cafe_engine-0.1.4}/tests/integration/test_global_config_integration.py +0 -0
- {cafe_engine-0.1.2 → cafe_engine-0.1.4}/tests/integration/test_pr_command_non_interactive.py +0 -0
- {cafe_engine-0.1.2 → cafe_engine-0.1.4}/tests/integration/test_pr_e2e_with_mock.py +0 -0
- {cafe_engine-0.1.2 → cafe_engine-0.1.4}/tests/integration/test_prepare_non_github.py +0 -0
- {cafe_engine-0.1.2 → cafe_engine-0.1.4}/tests/integration/test_spec_interactive_qa_e2e.py +0 -0
- {cafe_engine-0.1.2 → cafe_engine-0.1.4}/tests/unit/__init__.py +0 -0
- {cafe_engine-0.1.2 → cafe_engine-0.1.4}/tests/unit/test_abstract_cli.py +0 -0
- {cafe_engine-0.1.2 → cafe_engine-0.1.4}/tests/unit/test_agent_edit_auto_sync.py +0 -0
- {cafe_engine-0.1.2 → cafe_engine-0.1.4}/tests/unit/test_agent_executor.py +0 -0
- {cafe_engine-0.1.2 → cafe_engine-0.1.4}/tests/unit/test_agent_executor_path_formats.py +0 -0
- {cafe_engine-0.1.2 → cafe_engine-0.1.4}/tests/unit/test_agent_loading.py +0 -0
- {cafe_engine-0.1.2 → cafe_engine-0.1.4}/tests/unit/test_agent_manager.py +0 -0
- {cafe_engine-0.1.2 → cafe_engine-0.1.4}/tests/unit/test_agent_manager_mock_mode.py +0 -0
- {cafe_engine-0.1.2 → cafe_engine-0.1.4}/tests/unit/test_check_precommit_env.py +0 -0
- {cafe_engine-0.1.2 → cafe_engine-0.1.4}/tests/unit/test_checklist_generator.py +0 -0
- {cafe_engine-0.1.2 → cafe_engine-0.1.4}/tests/unit/test_checklist_utils.py +0 -0
- {cafe_engine-0.1.2 → cafe_engine-0.1.4}/tests/unit/test_claude_cli.py +0 -0
- {cafe_engine-0.1.2 → cafe_engine-0.1.4}/tests/unit/test_cli_agent_commands.py +0 -0
- {cafe_engine-0.1.2 → cafe_engine-0.1.4}/tests/unit/test_cli_agent_defaults.py +0 -0
- {cafe_engine-0.1.2 → cafe_engine-0.1.4}/tests/unit/test_cli_agent_sync.py +0 -0
- {cafe_engine-0.1.2 → cafe_engine-0.1.4}/tests/unit/test_cli_auto_mode.py +0 -0
- {cafe_engine-0.1.2 → cafe_engine-0.1.4}/tests/unit/test_cli_close.py +0 -0
- {cafe_engine-0.1.2 → cafe_engine-0.1.4}/tests/unit/test_cli_close_pr_mode.py +0 -0
- {cafe_engine-0.1.2 → cafe_engine-0.1.4}/tests/unit/test_cli_init.py +0 -0
- {cafe_engine-0.1.2 → cafe_engine-0.1.4}/tests/unit/test_cli_init_phase_config.py +0 -0
- {cafe_engine-0.1.2 → cafe_engine-0.1.4}/tests/unit/test_cli_invoke_command.py +0 -0
- {cafe_engine-0.1.2 → cafe_engine-0.1.4}/tests/unit/test_cli_ls_rm.py +0 -0
- {cafe_engine-0.1.2 → cafe_engine-0.1.4}/tests/unit/test_cli_make.py +0 -0
- {cafe_engine-0.1.2 → cafe_engine-0.1.4}/tests/unit/test_cli_module_init.py +0 -0
- {cafe_engine-0.1.2 → cafe_engine-0.1.4}/tests/unit/test_cli_phase_commands.py +0 -0
- {cafe_engine-0.1.2 → cafe_engine-0.1.4}/tests/unit/test_cli_prepare_init.py +0 -0
- {cafe_engine-0.1.2 → cafe_engine-0.1.4}/tests/unit/test_cli_restore.py +0 -0
- {cafe_engine-0.1.2 → cafe_engine-0.1.4}/tests/unit/test_cli_setup_agents.py +0 -0
- {cafe_engine-0.1.2 → cafe_engine-0.1.4}/tests/unit/test_cli_show.py +0 -0
- {cafe_engine-0.1.2 → cafe_engine-0.1.4}/tests/unit/test_cli_spec_auto.py +0 -0
- {cafe_engine-0.1.2 → cafe_engine-0.1.4}/tests/unit/test_cli_spec_output.py +0 -0
- {cafe_engine-0.1.2 → cafe_engine-0.1.4}/tests/unit/test_cli_template.py +0 -0
- {cafe_engine-0.1.2 → cafe_engine-0.1.4}/tests/unit/test_cli_template_sync.py +0 -0
- {cafe_engine-0.1.2 → cafe_engine-0.1.4}/tests/unit/test_config.py +0 -0
- {cafe_engine-0.1.2 → cafe_engine-0.1.4}/tests/unit/test_context_endtime.py +0 -0
- {cafe_engine-0.1.2 → cafe_engine-0.1.4}/tests/unit/test_context_session_id_update.py +0 -0
- {cafe_engine-0.1.2 → cafe_engine-0.1.4}/tests/unit/test_copilot_parse_response.py +0 -0
- {cafe_engine-0.1.2 → cafe_engine-0.1.4}/tests/unit/test_cursor_cli.py +0 -0
- {cafe_engine-0.1.2 → cafe_engine-0.1.4}/tests/unit/test_delta_display.py +0 -0
- {cafe_engine-0.1.2 → cafe_engine-0.1.4}/tests/unit/test_develop_clarification.py +0 -0
- {cafe_engine-0.1.2 → cafe_engine-0.1.4}/tests/unit/test_directory_structure.py +0 -0
- {cafe_engine-0.1.2 → cafe_engine-0.1.4}/tests/unit/test_display.py +0 -0
- {cafe_engine-0.1.2 → cafe_engine-0.1.4}/tests/unit/test_ensure_agent_file_exists.py +0 -0
- {cafe_engine-0.1.2 → cafe_engine-0.1.4}/tests/unit/test_ensure_default_content.py +0 -0
- {cafe_engine-0.1.2 → cafe_engine-0.1.4}/tests/unit/test_executor_session_recovery_generic.py +0 -0
- {cafe_engine-0.1.2 → cafe_engine-0.1.4}/tests/unit/test_gemini_cli.py +0 -0
- {cafe_engine-0.1.2 → cafe_engine-0.1.4}/tests/unit/test_gemini_content_extractor.py +0 -0
- {cafe_engine-0.1.2 → cafe_engine-0.1.4}/tests/unit/test_git.py +0 -0
- {cafe_engine-0.1.2 → cafe_engine-0.1.4}/tests/unit/test_git_utils.py +0 -0
- {cafe_engine-0.1.2 → cafe_engine-0.1.4}/tests/unit/test_git_utils_ceiling.py +0 -0
- {cafe_engine-0.1.2 → cafe_engine-0.1.4}/tests/unit/test_git_utils_repo_root.py +0 -0
- {cafe_engine-0.1.2 → cafe_engine-0.1.4}/tests/unit/test_git_worktree.py +0 -0
- {cafe_engine-0.1.2 → cafe_engine-0.1.4}/tests/unit/test_github.py +0 -0
- {cafe_engine-0.1.2 → cafe_engine-0.1.4}/tests/unit/test_github_pr_comments.py +0 -0
- {cafe_engine-0.1.2 → cafe_engine-0.1.4}/tests/unit/test_github_utils.py +0 -0
- {cafe_engine-0.1.2 → cafe_engine-0.1.4}/tests/unit/test_global_config.py +0 -0
- {cafe_engine-0.1.2 → cafe_engine-0.1.4}/tests/unit/test_image_download.py +0 -0
- {cafe_engine-0.1.2 → cafe_engine-0.1.4}/tests/unit/test_init_helpers.py +0 -0
- {cafe_engine-0.1.2 → cafe_engine-0.1.4}/tests/unit/test_inquirer_prompts.py +0 -0
- {cafe_engine-0.1.2 → cafe_engine-0.1.4}/tests/unit/test_interactive_qa.py +0 -0
- {cafe_engine-0.1.2 → cafe_engine-0.1.4}/tests/unit/test_mock_agent_executor.py +0 -0
- {cafe_engine-0.1.2 → cafe_engine-0.1.4}/tests/unit/test_permission.py +0 -0
- {cafe_engine-0.1.2 → cafe_engine-0.1.4}/tests/unit/test_phase_cache.py +0 -0
- {cafe_engine-0.1.2 → cafe_engine-0.1.4}/tests/unit/test_phase_get_issue_config_value.py +0 -0
- {cafe_engine-0.1.2 → cafe_engine-0.1.4}/tests/unit/test_phase_interactive.py +0 -0
- {cafe_engine-0.1.2 → cafe_engine-0.1.4}/tests/unit/test_phase_iteration_structure.py +0 -0
- {cafe_engine-0.1.2 → cafe_engine-0.1.4}/tests/unit/test_phase_non_interactive_behavior.py +0 -0
- {cafe_engine-0.1.2 → cafe_engine-0.1.4}/tests/unit/test_phase_progress.py +0 -0
- {cafe_engine-0.1.2 → cafe_engine-0.1.4}/tests/unit/test_phase_prompts.py +0 -0
- {cafe_engine-0.1.2 → cafe_engine-0.1.4}/tests/unit/test_phase_status_analysis_with_tools.py +0 -0
- {cafe_engine-0.1.2 → cafe_engine-0.1.4}/tests/unit/test_plan_phase_github_sync.py +0 -0
- {cafe_engine-0.1.2 → cafe_engine-0.1.4}/tests/unit/test_plan_phase_status_codes.py +0 -0
- {cafe_engine-0.1.2 → cafe_engine-0.1.4}/tests/unit/test_plan_phase_sync_github_config.py +0 -0
- {cafe_engine-0.1.2 → cafe_engine-0.1.4}/tests/unit/test_plan_phase_template_mode.py +0 -0
- {cafe_engine-0.1.2 → cafe_engine-0.1.4}/tests/unit/test_pr_comment_images.py +0 -0
- {cafe_engine-0.1.2 → cafe_engine-0.1.4}/tests/unit/test_pr_phase_context.py +0 -0
- {cafe_engine-0.1.2 → cafe_engine-0.1.4}/tests/unit/test_pr_phase_generate_content.py +0 -0
- {cafe_engine-0.1.2 → cafe_engine-0.1.4}/tests/unit/test_pr_phase_github_mode.py +0 -0
- {cafe_engine-0.1.2 → cafe_engine-0.1.4}/tests/unit/test_pr_phase_issue_comment.py +0 -0
- {cafe_engine-0.1.2 → cafe_engine-0.1.4}/tests/unit/test_pr_phase_parsing.py +0 -0
- {cafe_engine-0.1.2 → cafe_engine-0.1.4}/tests/unit/test_pr_phase_prepare_content.py +0 -0
- {cafe_engine-0.1.2 → cafe_engine-0.1.4}/tests/unit/test_pr_phase_status_analysis.py +0 -0
- {cafe_engine-0.1.2 → cafe_engine-0.1.4}/tests/unit/test_prepare_auto_sync.py +0 -0
- {cafe_engine-0.1.2 → cafe_engine-0.1.4}/tests/unit/test_prompt_utils.py +0 -0
- {cafe_engine-0.1.2 → cafe_engine-0.1.4}/tests/unit/test_questions_schema.py +0 -0
- {cafe_engine-0.1.2 → cafe_engine-0.1.4}/tests/unit/test_reset_command.py +0 -0
- {cafe_engine-0.1.2 → cafe_engine-0.1.4}/tests/unit/test_resolve_sync_github.py +0 -0
- {cafe_engine-0.1.2 → cafe_engine-0.1.4}/tests/unit/test_session.py +0 -0
- {cafe_engine-0.1.2 → cafe_engine-0.1.4}/tests/unit/test_spec_github_sync.py +0 -0
- {cafe_engine-0.1.2 → cafe_engine-0.1.4}/tests/unit/test_spec_image_download.py +0 -0
- {cafe_engine-0.1.2 → cafe_engine-0.1.4}/tests/unit/test_spec_phase_qa.py +0 -0
- {cafe_engine-0.1.2 → cafe_engine-0.1.4}/tests/unit/test_spec_phase_sync_github_config.py +0 -0
- {cafe_engine-0.1.2 → cafe_engine-0.1.4}/tests/unit/test_spec_prompt_with_images.py +0 -0
- {cafe_engine-0.1.2 → cafe_engine-0.1.4}/tests/unit/test_summary_display.py +0 -0
- {cafe_engine-0.1.2 → cafe_engine-0.1.4}/tests/unit/test_summary_service.py +0 -0
- {cafe_engine-0.1.2 → cafe_engine-0.1.4}/tests/unit/test_sync_helpers.py +0 -0
- {cafe_engine-0.1.2 → cafe_engine-0.1.4}/tests/unit/test_template_edit_auto_sync.py +0 -0
- {cafe_engine-0.1.2 → cafe_engine-0.1.4}/tests/unit/test_template_manager.py +0 -0
- {cafe_engine-0.1.2 → cafe_engine-0.1.4}/tests/unit/test_template_manager_global.py +0 -0
- {cafe_engine-0.1.2 → cafe_engine-0.1.4}/tests/unit/test_template_selector_auto.py +0 -0
- {cafe_engine-0.1.2 → cafe_engine-0.1.4}/tests/unit/test_time_formatter.py +0 -0
- {cafe_engine-0.1.2 → cafe_engine-0.1.4}/tests/unit/test_timeline_builder.py +0 -0
- {cafe_engine-0.1.2 → cafe_engine-0.1.4}/tests/unit/test_types.py +0 -0
- {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.
|
|
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
|
|
@@ -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.]+
|
|
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.]+
|
|
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('
|
|
160
|
-
|
|
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
|
|
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
|
-
|
|
1140
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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):
|